Null Safety #

Null reference error — atau yang sering disebut “billion-dollar mistake” — adalah salah satu penyebab crash aplikasi paling umum di dunia. Dart menyelesaikan masalah ini secara fundamental melalui Sound Null Safety: sistem yang menjamin sebuah variabel tidak pernah bernilai null kecuali kamu secara eksplisit mengizinkannya, dan jaminan ini berlaku hingga runtime — bukan hanya static analysis.

Masalah yang Diselesaikan Null Safety #

Sebelum null safety ada, bug seperti ini bisa lolos tanpa peringatan apapun:

// Sebelum null safety (Dart < 2.12)
String name = fetchUserName(); // bisa mengembalikan null!
print(name.length);            // CRASH saat runtime jika name null
                               // NoSuchMethodError: null.length

Crash ini hanya terdeteksi saat aplikasi sudah di tangan pengguna — bukan saat kamu menulis kode. Dengan null safety, masalah ini terdeteksi saat compile time:

// Dengan null safety (Dart 2.12+)
String name = fetchUserName(); // ERROR compile time jika fungsi bisa return null
print(name.length);            // Aman — compiler menjamin name tidak null

Non-Nullable by Default #

Prinsip pertama dan paling penting: semua tipe di Dart bersifat non-nullable secara default. Ini berarti variabel tidak bisa menerima nilai null kecuali kamu secara eksplisit menandainya dengan ?.

// NON-NULLABLE (default) — tidak bisa null
String nama = 'Flutter';
int tahun = 2025;
bool aktif = true;
List<String> tags = ['dart', 'flutter'];

// Semua ini adalah ERROR compile time:
// nama = null;      // ERROR
// tahun = null;     // ERROR
// aktif = null;     // ERROR

// NULLABLE — tambahkan ? untuk mengizinkan null
String? judul = null;     // OK
int? umur;                // OK, null by default
bool? terverifikasi;      // OK, null by default
List<String>? kategori;   // OK, null by default

Perubahan di Type Hierarchy #

Sebelum null safety, Null adalah subtype dari semua tipe — artinya null bisa dimasukkan ke variabel bertipe apapun. Setelah null safety, hierarki berubah fundamental:

SEBELUM null safety:
  Object
    ├── String
    ├── int
    ├── bool
    └── Null    <-- subtype dari semua, bisa masuk ke mana saja

SETELAH null safety:
  Object?          <-- top type (nullable)
    ├── Object     <-- non-nullable
    │     ├── String
    │     ├── int
    │     └── bool
    └── Null       <-- HANYA kompatibel dengan tipe nullable (?)

  Never            <-- bottom type baru (tidak pernah punya nilai)

Null-Aware Operators #

Dart menyediakan serangkaian operator khusus untuk bekerja dengan nilai nullable secara aman dan ekspresif:

Operator ?. — Null-Aware Access #

Akses property atau method hanya jika objek tidak null. Jika null, hasilnya adalah null (bukan crash):

String? nama = getNama();  // bisa null

// Tanpa null-aware (perlu if check):
int? panjang;
if (nama != null) {
  panjang = nama.length;
}

// Dengan ?. (lebih ringkas):
int? panjang = nama?.length;   // null jika nama null
String? upper = nama?.toUpperCase();

// Bisa di-chain:
String? kota = pengguna?.alamat?.kota;

Operator ?? — Null Coalescing #

Berikan nilai default jika ekspresi di kiri adalah null:

String? input = getInput();

// Gunakan 'Tidak diketahui' jika input null
String label = input ?? 'Tidak diketahui';

// Berguna untuk parameter dengan default
int timeout = config.timeout ?? 30;
double harga = produk.diskon ?? produk.hargaNormal;

Operator ??= — Null-Aware Assignment #

Assign nilai hanya jika variabel saat ini null:

String? cache;

// Hanya isi jika masih null
cache ??= 'nilai default';   // cache = 'nilai default'
cache ??= 'nilai lain';      // tidak berubah, cache sudah ada nilainya
print(cache);                // 'nilai default'

Operator ! — Null Assertion #

Paksa Dart memperlakukan ekspresi nullable sebagai non-nullable. Jika nilai ternyata null saat runtime, akan throw Null check operator used on a null value:

String? mungkinNull = getMungkinNull();

// Gunakan ! hanya jika YAKIN tidak null
String pastiAda = mungkinNull!;   // throw jika mungkinNull == null

// Kasus umum yang valid: setelah validasi manual
if (validateToken(token)) {
  // Kita tahu token valid dan tidak null
  processToken(token!);
}
Hindari overuse ! operator. Setiap ! adalah potensi runtime crash. Jika kamu terlalu sering menggunakannya, itu sinyal bahwa desain nullability-mu perlu dievaluasi ulang. Preferensikan ??, ?., atau restructuring logic untuk menghindari null daripada menegasikannya dengan !.

Operator ?[] — Null-Aware Index #

Akses elemen collection hanya jika collection tidak null:

List<String>? items = getItems();

// Tanpa null-aware:
String? first;
if (items != null) {
  first = items[0];
}

// Dengan ?[]:
String? first = items?[0];

Operator ?.. — Null-Aware Cascade #

Jalankan cascade operation hanya jika objek tidak null:

Path? path = getPath();

// Seluruh cascade di-skip jika path null
path
  ?..moveTo(0, 0)
  ..lineTo(100, 0)
  ..lineTo(100, 100)
  ..close();

Type Promotion #

Salah satu fitur paling cerdas dari null safety Dart adalah type promotion — kemampuan compiler untuk secara otomatis “mempromosikan” tipe nullable menjadi non-nullable setelah pengecekan null:

String? nama = getNama();

// Sebelum pengecekan: nama bertipe String? (nullable)
// nama.length --> ERROR, belum tentu tidak null

if (nama != null) {
  // Di dalam if: nama DIPROMOSIKAN ke String (non-nullable)!
  // Compiler tahu: jika sampai di sini, nama pasti tidak null
  print(nama.length);     // OK, tidak perlu ?. lagi
  print(nama.toUpperCase()); // OK
}

// Di luar if: kembali ke String? (nullable)

Promotion dengan Early Return #

String processInput(String? input) {
  // Early return jika null
  if (input == null) return 'default';

  // Setelah baris ini, input dipromosikan ke String
  // Tidak perlu null-check lagi di bawah
  return input.trim().toUpperCase();
}

Promotion dengan Pattern Matching (Dart 3) #

void handleResult(Result? result) {
  // Pattern matching dengan null check
  if (result case final r?) {
    // r dipromosikan ke Result (non-nullable)
    print(r.value);
  }

  // Switch expression dengan null handling
  final message = switch (result) {
    null => 'Tidak ada hasil',
    Result(:final value) when value > 0 => 'Positif: $value',
    _ => 'Negatif atau nol',
  };
}

Promotion pada Private Final Fields (Dart 3.2+) #

class UserService {
  final String? _cachedToken;  // private final field

  UserService(this._cachedToken);

  void useToken() {
    // Dart 3.2+: promotion bekerja pada private final field!
    if (_cachedToken != null) {
      // _cachedToken dipromosikan ke String di sini
      print(_cachedToken.length);  // OK tanpa ?. atau !
    }
  }
}

Mengapa promotion tidak bekerja pada non-private atau non-final field?

Dart hanya mempromosikan private final field karena itu satu-satunya yang bisa dijamin tidak berubah antara pengecekan dan penggunaan. Public field bisa diubah dari luar class. Non-final field bisa diubah kapan saja. Dengan private final, compiler yakin nilai tidak akan berubah di antara if (field != null) dan penggunaan field.


Keyword late #

Keyword late digunakan untuk dua tujuan berbeda:

Late Initialization #

Mendeklarasikan variabel non-nullable yang diinisialisasi setelah deklarasi, bukan saat deklarasi:

class DatabaseService {
  // Tidak bisa diinisialisasi di deklarasi karena butuh async setup
  late final Database _db;

  Future<void> init() async {
    _db = await openDatabase('app.db');  // inisialisasi di sini
  }

  Future<List<User>> getUsers() async {
    // _db sudah tersedia karena init() dipanggil sebelumnya
    return await _db.query('users');
  }
}
late menambahkan runtime check. Jika kamu mengakses variabel late sebelum diinisialisasi, Dart akan throw LateInitializationError saat runtime. Gunakan late hanya jika kamu yakin variabel akan selalu diinisialisasi sebelum diakses.

Late dengan Lazy Initialization #

late juga mendukung lazy initialization — nilai dihitung hanya saat pertama kali diakses:

class Config {
  // Nilai ini MAHAL untuk dihitung (misal: parse file besar)
  // Dengan late, hanya dihitung jika benar-benar digunakan
  late final List<String> _allowedDomains = _loadDomains();

  List<String> _loadDomains() {
    // Operasi mahal -- hanya terjadi sekali, saat pertama diakses
    return File('domains.txt').readAsLinesSync();
  }

  bool isAllowed(String domain) {
    return _allowedDomains.contains(domain);
    // _loadDomains() dipanggil di sini (jika belum pernah)
  }
}

late dalam Context Flutter #

class _MyScreenState extends State<MyScreen> {
  // AnimationController tidak bisa diinisialisasi sebelum initState()
  // karena butuh vsync dari State
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

Null Safety dan Performa #

Sound null safety bukan hanya tentang keamanan — ia juga berdampak nyata pada performa:

TANPA null safety:
  Compiler harus asumsikan nilai BISA null kapan saja
  --> Setiap akses perlu null check implisit
  --> Banyak null check di machine code yang dihasilkan
  --> Binary lebih besar, startup lebih lambat

DENGAN null safety (sound):
  Compiler TAHU mana yang non-nullable
  --> Null check yang tidak perlu dieliminasi
  --> Machine code lebih ringkas
  --> Binary lebih kecil, performa lebih baik

Manfaat konkret dari sound null safety yang dilaporkan:

  • Ukuran binary lebih kecil — null check yang tidak perlu dihapus dari output AOT
  • Startup lebih cepat — lebih sedikit operasi overhead saat inisialisasi
  • Performa runtime lebih baik — AOT compiler bisa melakukan optimasi yang sebelumnya tidak aman

Pola Umum Null Safety di Flutter #

Widget yang Menerima Nullable #

class UserCard extends StatelessWidget {
  final String nama;
  final String? foto;      // nullable: pengguna mungkin belum upload foto
  final String? bio;       // nullable: bio opsional

  const UserCard({
    super.key,
    required this.nama,    // wajib, non-nullable
    this.foto,             // opsional, nullable
    this.bio,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Tampilkan foto jika ada, placeholder jika tidak
        foto != null
            ? Image.network(foto!)     // ! aman karena sudah dicek
            : const Icon(Icons.person),

        Text(nama),

        // Tampilkan bio hanya jika ada
        if (bio != null) Text(bio!),

        // Atau lebih idiomatic:
        if (bio case final b?) Text(b),  // Dart 3 pattern
      ],
    );
  }
}

Null Safety dengan Future dan Stream #

// Future yang bisa return null
Future<String?> fetchToken() async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getString('token');  // null jika belum login
}

// Penggunaan
Future<void> checkAuth() async {
  final token = await fetchToken();

  if (token == null) {
    // Arahkan ke login screen
    navigateTo('/login');
    return;
  }

  // token dipromosikan ke String di sini
  validateToken(token);
}

// Stream nullable
Stream<User?> watchCurrentUser() {
  return authService.userChanges();  // null saat logout
}

Ringkasan #

  • Dart null safety bersifat sound — jaminan non-null berlaku hingga runtime, bukan hanya static analysis.
  • Semua tipe di Dart non-nullable by default — tambahkan ? untuk mengizinkan null.
  • Dart memiliki serangkaian null-aware operators: ?. (safe access), ?? (default value), ??= (conditional assign), ! (assertion), ?[] (safe index), ?.. (safe cascade).
  • Type promotion secara otomatis mempromosikan tipe nullable ke non-nullable setelah null check — menghilangkan kebutuhan ?. berulang di dalam blok yang sudah aman.
  • Promotion bekerja pada local variable, dan sejak Dart 3.2 juga pada private final fields.
  • Keyword late digunakan untuk deklarasi variabel non-nullable yang diinisialisasi setelah deklarasi, atau untuk lazy initialization.
  • Sound null safety memungkinkan compiler menghilangkan null check yang tidak perlu — menghasilkan binary yang lebih kecil dan performa yang lebih baik.

← Sebelumnya: Dart Overview   Berikutnya: Asynchronous Programming →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact