Fitur Dart 3 #
Dart 3, yang dirilis bersamaan dengan Flutter 3.10 pada konferensi Google I/O 2023, menandai salah satu lompatan evolusioner terbesar dalam sejarah pengembangan bahasa Dart. Pembaruan ini tidak hanya memantapkan status bahasa yang 100% aman dari kesalahan referensi kosong (100% Sound Null Safety), tetapi juga memperkenalkan paradigma pemrograman modern yang mengubah cara kita memodelkan data, mengelola status (state), dan menyusun logika bisnis di aplikasi Flutter. Kita akan mengupas tuntas fitur-fitur pilar di Dart 3, mulai dari Records, Pattern Matching, Switch Expressions, Sealed Classes, Class Modifiers tingkat lanjut, hingga fitur Extension Types yang efisien tanpa overhead runtime.
Evolusi Besar: Sound Null Safety yang Mutlak #
Sejak dirilisnya Dart 3, compiler Dart telah resmi menghapus dukungan untuk menjalankan kode tanpa jaminan keamanan null (un-sound null safety). Ini berarti Dart 3 adalah bahasa pemrograman yang 100% Sound Null Safety.
Sebelum era ini, compiler masih memperbolehkan beberapa pengecualian konfigurasi untuk menjalankan paket-paket kode lama yang belum bermigrasi ke null safety. Dengan ditiadakannya toleransi tersebut, kita mendapatkan beberapa manfaat besar di bawah kap:
- Optimalisasi Kompilasi AOT/JIT: Compiler tidak perlu lagi menyisipkan pemeriksaan null tambahan (null check instructions) pada tingkat kode mesin untuk variabel non-nullable. Ini menghasilkan ukuran biner aplikasi yang lebih kecil dan kecepatan eksekusi yang lebih tinggi.
- Prediktabilitas Kode: Kita dijamin tidak akan pernah menemui kesalahan Null Pointer Exception pada saat runtime untuk variabel yang bertipe non-nullable.
Keputusan berani untuk mewajibkan soundness secara mutlak ini menjadi batu pijakan penting yang memungkinkan tim pengembang Dart merancang fitur-fitur baru seperti Records dan Patterns dengan performa maksimal.
Records — Mengembalikan Multi-Nilai Secara Efisien #
Dalam pemrograman, kita sering kali membuat fungsi yang perlu mengembalikan lebih dari satu nilai sekaligus. Sebelum adanya Dart 3, solusi yang bisa kita gunakan untuk memecahkan masalah ini memiliki keterbatasan masing-masing:
// ANTI-PATTERN: Beberapa pendekatan lama sebelum Dart 3 untuk mengembalikan multi-nilai
// Opsi 1: Menggunakan List (Kelemahan: Tidak type-safe, rentan salah indeks)
List<dynamic> getUserOldList() {
return ['Andi', 28, true];
}
// Opsi 2: Menggunakan Map (Kelemahan: Tidak type-safe, rawan saltik pada nama key)
Map<String, dynamic> getUserOldMap() {
return {'name': 'Andi', 'age': 28, 'isAdmin': true};
}
// Opsi 3: Membuat Kelas Kustom (Kelemahan: Terlalu verbose untuk struktur sederhana)
class UserResult {
final String name;
final int age;
final bool isAdmin;
UserResult(this.name, this.age, this.isAdmin);
}
UserResult getUserOldClass() => UserResult('Andi', 28, true);
Dart 3 menyelesaikan masalah ini secara elegan melalui Records. Records adalah tipe data koleksi anonim yang bersifat imut (immutable) dan memiliki tipe data yang pasti (type-safe). Berbeda dengan pembuatan kelas baru, Records dideklarasikan secara ringkas menggunakan tanda kurung ().
Records dengan Bidang Posisional (Positional Fields) #
Secara standar, kita bisa mendefinisikan bidang di dalam record berdasarkan urutan posisinya:
// Mendefinisikan fungsi yang mengembalikan Record bertipe (String, int, bool)
(String, int, bool) getUserInfo() {
return ('Budi', 25, true);
}
void main() {
final user = getUserInfo();
// Mengakses nilai posisional menggunakan sintaksis $1, $2, dst.
print('Nama: ${user.$1}'); // Output: Budi
print('Umur: ${user.$2}'); // Output: 25
print('Admin: ${user.$3}'); // Output: true
}
Records dengan Bidang Bernama (Named Fields) #
Untuk meningkatkan keterbacaan kode, kita bisa memberikan nama pada setiap bidang di dalam record, mirip dengan named parameters pada fungsi:
// Mendefinisikan Record dengan named fields
({String name, int age, bool isStaff}) getStaffInfo() {
return (name: 'Sari', age: 30, isStaff: false);
}
void main() {
final staff = getStaffInfo();
// Mengakses nilai menggunakan nama bidang secara langsung
print('Nama: ${staff.name}'); // Output: Sari
print('Umur: ${staff.age}'); // Output: 30
print('Staff: ${staff.isStaff}'); // Output: false
}
Kita juga bisa mencampur bidang posisional dan bernama di dalam satu record yang sama:
(int, {String label}) myRecord = (404, label: 'Not Found');
Structural Equality (Kesamaan Struktural) #
Berbeda dengan List atau Map yang membandingkan kesamaan objek berdasarkan referensi memori (identity), Records di Dart menggunakan perbandingan kesamaan nilai struktur (structural equality). Dua record dianggap sama jika tipe data dan nilai di setiap bidangnya identik:
void testEquality() {
final recordA = ('flutter', 42);
final recordB = ('flutter', 42);
// Membandingkan kesamaan nilai
print(recordA == recordB); // Output: true (Pada List biasa, ini akan mengembalikan false)
final mapRecordA = (x: 10, y: 20);
final mapRecordB = (y: 20, x: 10);
// Perbandingan tetap sukses meskipun urutan penulisan named fields berbeda
print(mapRecordA == mapRecordB); // Output: true
}
Mekanisme ini sangat menghemat penulisan kode boilerplate ketika kita ingin menerapkan pembandingan nilai koordinat, konfigurasi, atau status UI yang sederhana.
Patterns — Destrukturisasi dan Pencocokan Data #
Patterns adalah fitur pendamping Records yang sangat kuat di Dart 3. Fitur ini memiliki dua fungsi utama:
- Destructuring: Membongkar struktur data kompleks (seperti Record, List, Map, atau Objek kustom) menjadi variabel-variabel individual secara langsung.
- Matching: Memeriksa apakah suatu data memiliki bentuk, tipe, atau nilai tertentu sesuai dengan pola yang kita tentukan.
Berikut adalah diagram alir bagaimana proses destrukturisasi bekerja untuk memecah data Record secara aman:
flowchart LR
RecordData["Record: (status: 200, message: 'Sukses')"] --> Destructure{"Destructuring Pattern"}
Destructure -->|"Pengecekan Tipe & Nama Key"| VarStatus["Variabel status: 200"]
Destructure -->|"Pengecekan Tipe & Nama Key"| VarMsg["Variabel message: 'Sukses'"]Destrukturisasi Variabel (Variable Destructuring) #
Kita bisa membongkar berbagai jenis struktur data hanya dalam satu baris deklarasi variabel:
void destructureExamples() {
// 1. Destrukturisasi dari Record
final (name, age) = ('Deni', 32);
print('$name berumur $age tahun.'); // Output: Deni berumur 32 tahun.
// 2. Destrukturisasi dari List (Menggunakan rest pattern '...' untuk sisa elemen)
final [first, second, ...rest] = [10, 20, 30, 40, 50];
print('Pertama: $first, Kedua: $second, Sisa: $rest');
// Output: Pertama: 10, Kedua: 20, Sisa: [30, 40, 50]
// 3. Destrukturisasi dari Map
final json = {'id': 'user-99', 'role': 'editor'};
final {'id': userId, 'role': userRole} = json;
print('User $userId adalah seorang $userRole');
// Output: User user-99 adalah seorang editor
}
Pola Pencocokan Kondisi (if-case Pattern) #
Sebelum adanya Dart 3, memvalidasi struktur data JSON yang kompleks dari API membutuhkan banyak percabangan if bersarang yang sangat rentan terjadi kesalahan penulisan (runtime crash). Dengan if-case, kita bisa mencocokkan tipe dan nilai data sekaligus melakukan destrukturisasi:
// BENAR: Memproses data JSON secara aman dan ringkas menggunakan if-case pattern
void handleApiResponse(dynamic jsonResponse) {
if (jsonResponse case {'status': 'success', 'data': {'name': String name, 'age': int age}}) {
// Blok ini hanya berjalan jika jsonResponse adalah Map, status bernilai 'success',
// dan objek data mengandung field 'name' (bertipe String) serta 'age' (bertipe int).
// Variabel 'name' dan 'age' langsung tersedia untuk digunakan di dalam lingkup ini.
print('Data pengguna berhasil diverifikasi: $name, $age tahun.');
} else {
print('Struktur data JSON tidak valid.');
}
}
Pencocokan Properti Objek (Object Patterns) #
Patterns juga bekerja pada kelas buatan kita sendiri. Kita bisa mencocokkan properti dari sebuah instansi objek tanpa harus menulis kode manual getter berulang kali:
class UserProfile {
final String username;
final int level;
UserProfile(this.username, this.level);
}
void checkUserLevel(Object profile) {
// Mencocokkan apakah objek bertipe UserProfile dan mengekstrak nilai propertinya
if (profile case UserProfile(username: final name, level: int lvl) when lvl > 50) {
print('Pengguna elit: $name (Level $lvl)');
} else if (profile case UserProfile(username: final name)) {
print('Pengguna biasa: $name');
}
}
Switch Expressions — Ringkas, Deklaratif, dan Ekspresif #
Dart 3 merevolusi cara kerja switch dengan memperkenalkan Switch Expressions. Jika switch tradisional bertindak sebagai statement (pernyataan kontrol alur yang tidak menghasilkan nilai), switch expressions bertindak sebagai expression (menghasilkan nilai yang bisa langsung kita simpan ke variabel atau dikembalikan oleh fungsi).
Mari kita bandingkan perbedaannya secara langsung:
// ANTI-PATTERN: Menulis switch statement yang panjang dan berulang untuk menetapkan nilai
String getStatusLabelOld(String status) {
String label;
switch (status) {
case 'pending':
label = 'Menunggu Pembayaran';
break;
case 'processing':
label = 'Sedang Diproses';
break;
case 'shipped':
label = 'Dalam Pengiriman';
break;
default:
label = 'Status Tidak Dikenal';
}
return label;
}
// ====================================================================
// BENAR: Menulis switch expression yang sangat bersih dan bebas dari boilerplate 'break'
String getStatusLabelNew(String status) => switch (status) {
'pending' => 'Menunggu Pembayaran',
'processing' => 'Sedang Diproses',
'shipped' => 'Dalam Pengiriman',
_ => 'Status Tidak Dikenal', // Karakter '_' bertindak sebagai default case (wildcard)
};
Pengecekan Kondisi Tambahan (Guard Clause) #
Switch expression mendukung penambahan klausa pengaman (guard clause) menggunakan kata kunci when untuk mengevaluasi kondisi logika tambahan:
String evaluateScore(int score) => switch (score) {
>= 90 => 'Sangat Baik (A)',
>= 75 && < 90 => 'Baik (B)',
>= 60 when score.isEven => 'Cukup - Genap (C)', // Guard clause aktif
>= 60 => 'Cukup (C)',
_ => 'Kurang (D)',
};
Sealed Classes — Garansi Penanganan Tipe yang Exhaustive #
Dalam pengembangan aplikasi Flutter, salah satu skenario yang paling sering kita temui adalah pengelolaan status UI (UI State Management). Misalnya, sebuah halaman bisa berada dalam status Loading, Success, atau Error.
Sebelum Dart 3, kita biasanya menggunakan pewarisan kelas biasa atau enum untuk mewakili status ini. Kelemahannya adalah compiler tidak memiliki cara untuk mengetahui apakah kita telah menangani semua status yang mungkin terjadi di dalam kode antarmuka.
Dart 3 memperkenalkan kata kunci pengubah sealed untuk kelas. Kelas yang ditandai dengan sealed memiliki karakteristik sebagai berikut:
- Kelas tersebut tidak dapat diinstansiasi secara langsung (abstract secara implisit).
- Seluruh sub-kelas dari kelas
sealedtersebut wajib ditulis di dalam file (library) yang sama. - Compiler Dart menjamin pemeriksaan menyeluruh (exhaustiveness checking). Jika kita melewatkan satu saja sub-kelas saat melakukan pemeriksaan menggunakan
switch, aplikasi kita tidak akan bisa di-build (compile-time error).
Visualisasi alur pemeriksaan menyeluruh ini dapat dilihat pada diagram berikut:
flowchart TD
Sealed["Sealed Class: UIState"] --> Sub1["Class: Loading"]
Sealed --> Sub2["Class: Success"]
Sealed --> Sub3["Class: Error"]
Sub1 & Sub2 & Sub3 --> Compiler{"Compiler Exhaustiveness Check"}
Compiler -->|"Semua Sub-tipe Terpenuhi"| Valid["Kompilasi Sukses (Aman)"]
Compiler -->|"Ada Sub-tipe Terlewat"| Invalid["Error Compile-Time (Gagal Build)"]Contoh Implementasi Sealed Class #
Mari kita terapkan sealed class untuk merancang status produk yang aman:
// StatusProduk.dart - Seluruh kelas berada dalam satu file yang sama
sealed class ProductState {}
class ProductLoading extends ProductState {}
class ProductSuccess extends ProductState {
final List<String> items;
ProductSuccess(this.items);
}
class ProductError extends ProductState {
final String errorMessage;
ProductError(this.errorMessage);
}
Sekarang, mari kita konsumsi status di atas di dalam antarmuka UI Flutter menggunakan switch expression:
// Compiler akan memvalidasi bahwa semua sub-state ditangani
Widget buildProductList(ProductState state) {
return switch (state) {
ProductLoading() => const CircularProgressIndicator(),
ProductSuccess(:final items) => ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => Text(items[index]),
),
ProductError(:final errorMessage) => Text('Gagal memuat: $errorMessage'),
// Jika kita menghapus salah satu baris status di atas,
// compiler akan menampilkan error: "The type 'ProductState' is not exhaustively matched"
};
}
Mekanisme ini menghilangkan kebutuhan terhadap penulisan penanganan default _ => ... yang biasanya digunakan untuk membungkam error compiler. Ini sangat aman karena jika di masa depan kita menambahkan status baru (misalnya ProductEmpty), compiler akan memaksa kita memperbarui seluruh kode UI yang mengonsumsi status tersebut sebelum aplikasi bisa dijalankan.
Class Modifiers — Kendali Penuh Terhadap Hak Akses Kelas #
Dart 3 memberikan kendali yang jauh lebih granular kepada para pembuat pustaka (library authors) untuk mengatur bagaimana suatu kelas dapat dikonsumsi di luar pustaka mereka. Kita bisa menggunakan modifier kelas baru untuk membatasi pewarisan dan instansiasi.
Berikut adalah penjelasan dan aturan dari masing-masing modifier:
1. base #
Kelas yang ditandai dengan base membatasi agar kelas tersebut hanya bisa diwarisi (extends), dan melarang implementasi langsung (implements) dari luar file. Hal ini menjamin bahwa seluruh subclass mewarisi metode internal secara utuh.
// File: animal.dart
base class Animal {
void eat() => print('Makan');
}
// File: main.dart
class Dog extends Animal {} // VALID
// class Robot implements Animal {} // ERROR: base class tidak bisa di-implement
2. interface #
Kebalikan dari base, modifier interface melarang pewarisan (extends) dan hanya memperbolehkan implementasi ulang seluruh cetak biru kelas (implements) dari luar file. Ini berguna untuk memisahkan kontrak API dengan implementasi konkret.
// File: auth.dart
interface class AuthRepository {
void login() {}
}
// File: main.dart
class MyAuth implements AuthRepository {
@override
void login() => print('Login...');
} // VALID
// class ExtendedAuth extends AuthRepository {} // ERROR: interface class tidak bisa di-extend
3. final #
Modifier final menutup total kemungkinan perluasan kelas dari luar file. Kelas tidak dapat diwarisi (extends) maupun di-implementasikan ulang (implements).
// File: config.dart
final class AppConfig {
final String apiUrl = 'https://api.example.com';
}
// File: main.dart
// class CustomConfig extends AppConfig {} // ERROR: final class tidak bisa diwarisi
4. mixin class #
Memungkinkan sebuah kelas digunakan sebagai cetak biru pewarisan sekaligus dapat disisipkan menggunakan kata kunci with (sebagai mixin).
mixin class Logger {
void log(String msg) => print('[LOG]: $msg');
}
class AuthService with Logger {} // Digunakan sebagai mixin (VALID)
class ConsoleLogger extends Logger {} // Digunakan sebagai kelas biasa (VALID)
Ringkasan Tabel Karakteristik Modifier Kelas #
| Modifier | Dapat Diinstansiasi? | Dapat Diwarisi (extends)? | Dapat Diimplementasikan (implements)? | Dapat Digunakan sebagai Mixin (with)? |
|---|---|---|---|---|
class | ✅ | ✅ | ✅ | ❌ |
abstract | ❌ | ✅ | ✅ | ❌ |
base | ✅ | ✅ | ❌ | ❌ |
interface | ✅ | ❌ | ✅ | ❌ |
final | ✅ | ❌ | ❌ | ❌ |
sealed | ❌ | ✅* | ✅* | ❌ |
mixin class | ✅ | ✅ | ✅ | ✅ |
*Pengecualian khusus: Hanya diperbolehkan di dalam file/library yang sama.
Extension Types — Wrapper Bebas Biaya Runtime (Zero-Cost) #
Diperkenalkan secara resmi pada Dart versi 3.3, Extension Types adalah fitur canggih yang dirancang untuk membungkus (wrap) tipe data yang sudah ada guna memberikan antarmuka (interface) atau validasi statis baru, namun tanpa menimbulkan biaya alokasi memori tambahan pada saat aplikasi dijalankan (zero runtime overhead).
Sebelum adanya fitur ini, jika kita ingin membuat tipe data khusus (misalnya memisahkan ID pengguna dan ID produk untuk menghindari kesalahan input), kita harus membuat kelas pembungkus biasa:
// Pendekatan Klasik: Membuat kelas pembungkus (Mengakibatkan alokasi memori objek baru saat runtime)
class LegacyUserId {
final String value;
const LegacyUserId(this.value);
}
Meskipun aman saat penulisan kode, pendekatan di atas membebani performa aplikasi karena compiler harus mengalokasikan ruang memori untuk objek baru tersebut pada memori heap setiap kali instansiasi dilakukan.
Sintaksis Extension Types #
Dengan Extension Types, pembungkus tersebut hanya ada pada tahap analisis statis (compile-time). Ketika kode dikompilasi menjadi kode mesin, pembungkus tersebut akan dilebur (compiled away) kembali menjadi tipe data dasarnya:
// Mendefinisikan extension type
extension type UserId(String value) {
// Kita bisa menambahkan metode pembantu
bool get isValid => value.startsWith('usr_');
}
extension type ProductId(String value) {}
void showUserProfile(UserId id) {
print('Menampilkan profil user: ${id.value}');
}
void main() {
final myUserId = UserId('usr_12345');
final myProductId = ProductId('prod_9999');
showUserProfile(myUserId); // VALID
// ERROR COMPILE-TIME: Tipe data ProductId tidak kompatibel dengan UserId
// showUserProfile(myProductId);
// Pada saat runtime (aplikasi berjalan di HP):
// Variabel myUserId dan myProductId hanyalah objek String biasa ('usr_12345' dan 'prod_9999')
// Tanpa adanya alokasi kelas UserId atau ProductId sama sekali!
}
Fitur ini sangat bermanfaat ketika kita ingin berinteraksi dengan API Javascript (pada platform Web) atau ketika kita ingin memetakan data JSON eksternal ke dalam objek Dart yang type-safe tanpa harus mengeluarkan performa ekstra untuk pembuatan objek baru.
Ringkasan #
- Sound Null Safety Mutlak: Dart 3 menghapus toleransi terhadap kode non-null-safe, menghasilkan kompilasi AOT/JIT yang jauh lebih teroptimasi dan performa yang lebih gegas.
- Records: Memungkinkan kita mengembalikan beberapa nilai sekaligus dari sebuah fungsi secara terstruktur, type-safe, dan memiliki kesamaan struktural (structural equality) bawaan.
- Patterns: Menghadirkan kemampuan pembongkaran data (destructuring) dan pencocokan pola (matching) yang memotong kompleksitas pemrosesan data mentah seperti JSON dari API.
- Switch Expressions: Mengubah cara kerja
switchmenjadi bentuk ekspresi penghasil nilai yang deklaratif, imut, dan bersih tanpa memerlukan penulisanbreak.- Sealed Classes: Membuat hierarki kelas tertutup yang menjamin pemeriksaan menyeluruh (exhaustiveness checking) oleh compiler, sangat ideal untuk memodelkan UI State yang aman.
- Class Modifiers: Memberikan kendali akses yang ketat terhadap hak pewarisan kelas menggunakan kata kunci seperti
base,interface, danfinal.- Extension Types: Memungkinkan kita membuat tipe data pembungkus kustom yang aman secara statis (compile-time) tanpa membebani performa memori saat runtime (zero-cost abstraction).
← Sebelumnya: Asynchronous Programming Berikutnya: Collections →