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 penggunaanfield.
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');
}
}
latemenambahkan runtime check. Jika kamu mengakses variabellatesebelum diinisialisasi, Dart akan throwLateInitializationErrorsaat runtime. Gunakanlatehanya 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
latedigunakan 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 →