Fitur Dart 3 #

Dart 3 yang dirilis bersamaan dengan Flutter 3.10 di Google I/O 2023 adalah rilis terbesar dalam sejarah Dart. Ia memperkenalkan beberapa fitur bahasa yang secara fundamental mengubah cara kamu memodelkan data, menangani state, dan menulis logika bisnis. Artikel ini membahas semua fitur besar Dart 3 beserta cara menggunakannya secara praktis.

Records — Multiple Return Values #

Sebelum Dart 3, mengembalikan beberapa nilai dari sebuah fungsi membutuhkan cara-cara yang kurang elegan:

// Sebelum Dart 3 -- opsi yang ada:

// Opsi 1: List (tidak type-safe)
List<dynamic> getUser() => ['Budi', 25, 'Jakarta'];

// Opsi 2: Map (tidak type-safe)
Map<String, dynamic> getUser() => {'nama': 'Budi', 'umur': 25};

// Opsi 3: Buat class khusus (verbose untuk kasus sederhana)
class UserResult {
  final String nama;
  final int umur;
  UserResult(this.nama, this.umur);
}
UserResult getUser() => UserResult('Budi', 25);

Dart 3 memperkenalkan Records — tipe data yang memungkinkan menggabungkan beberapa nilai dengan tipe berbeda dalam satu struktur yang ringan dan type-safe:

// Dart 3: Records -- ringkas dan type-safe
(String, int) getUser() {
  return ('Budi', 25);
}

// Destructuring saat menggunakan
final (nama, umur) = getUser();
print('$nama, $umur tahun');  // Budi, 25 tahun

Named Records #

Records juga mendukung field yang diberi nama untuk keterbacaan yang lebih baik:

// Named record
({String nama, int umur, String kota}) getProfile() {
  return (nama: 'Sari', umur: 30, kota: 'Bandung');
}

final profile = getProfile();
print(profile.nama);   // Sari
print(profile.umur);   // 30
print(profile.kota);   // Bandung

// Campuran positional dan named
(int, {String label, bool aktif}) getStatus() {
  return (1, label: 'Admin', aktif: true);
}

final status = getStatus();
print(status.$1);       // 1 (positional diakses dengan $1, $2, dst)
print(status.label);    // Admin
print(status.aktif);    // true

Records sebagai Tuple untuk Equality #

Records di Dart memiliki structural equality bawaan — dua record dengan nilai yang sama dianggap equal:

final a = ('flutter', 42);
final b = ('flutter', 42);

print(a == b);   // true! (berbeda dengan List yang selalu false)

// Ini memungkinkan penggunaan record untuk equality yang sederhana
class Produk {
  final String id;
  final String kategori;
  Produk(this.id, this.kategori);

  // Implementasi equality yang ringkas menggunakan record
  @override
  bool operator ==(Object other) =>
      other is Produk && (id, kategori) == (other.id, other.kategori);

  @override
  int get hashCode => (id, kategori).hashCode;
}

Records di Flutter — Contoh Praktis #

// Mengembalikan status loading + data + error sekaligus
(bool loading, List<Product>? data, String? error) get state {
  if (_isLoading) return (true, null, null);
  if (_error != null) return (false, null, _error);
  return (false, _products, null);
}

// Di widget:
final (loading, products, error) = viewModel.state;

if (loading) return const CircularProgressIndicator();
if (error != null) return ErrorView(message: error);
return ProductList(products: products!);

Patterns — Destructuring dan Matching #

Patterns adalah cara baru untuk mencocokkan dan mendekonstruksi nilai di Dart 3. Mereka bisa digunakan di switch, if case, variable declaration, dan assignment.

Variable Patterns #

// Destructuring langsung saat assignment
final (x, y) = (10, 20);           // dari record
final [first, ...rest] = [1,2,3,4]; // dari List
final {'nama': nama} = json;        // dari Map

print('x=$x, y=$y');          // x=10, y=20
print(first);                  // 1
print(rest);                   // [2, 3, 4]
print(nama);                   // nilai dari json['nama']

if-case — Pattern di Conditional #

// Sebelum Dart 3: verbose
dynamic json = {'tipe': 'user', 'nama': 'Budi'};

if (json is Map<String, dynamic>) {
  final tipe = json['tipe'];
  if (tipe is String && tipe == 'user') {
    final nama = json['nama'];
    if (nama is String) {
      print('User: $nama');
    }
  }
}

// Dart 3: if-case dengan pattern
if (json case {'tipe': 'user', 'nama': String nama}) {
  print('User: $nama');    // hanya jika map cocok dengan pattern
}

Object Patterns #

class Point {
  final double x;
  final double y;
  Point(this.x, this.y);
}

void describe(Object shape) {
  if (shape case Point(x: var x, y: var y) when x == y) {
    print('Titik diagonal: ($x, $y)');
  } else if (shape case Point(x: var x, y: var y)) {
    print('Titik biasa: ($x, $y)');
  }
}

List dan Map Patterns #

// List patterns dengan rest pattern (...)
final numbers = [1, 2, 3, 4, 5];

switch (numbers) {
  case []:
    print('Kosong');
  case [final single]:
    print('Satu elemen: $single');
  case [final first, final second]:
    print('Dua elemen: $first dan $second');
  case [final first, ...final rest]:
    print('Pertama: $first, sisanya: $rest');
}
// Output: Pertama: 1, sisanya: [2, 3, 4, 5]

// Map patterns -- hanya cocok jika key ada
void parseResponse(Map<String, dynamic> response) {
  if (response case {'status': 'ok', 'data': final List data}) {
    processData(data);
  } else if (response case {'status': 'error', 'message': String msg}) {
    showError(msg);
  }
}

Switch Expressions — Switch sebagai Ekspresi #

Sebelum Dart 3, switch hanya bisa digunakan sebagai statement. Dart 3 memperkenalkan switch expressions yang bisa digunakan di mana ekspresi diharapkan:

// Switch statement (lama)
String label;
switch (status) {
  case 'active':
    label = 'Aktif';
    break;
  case 'inactive':
    label = 'Tidak Aktif';
    break;
  default:
    label = 'Tidak Diketahui';
}

// Switch expression (Dart 3) -- jauh lebih ringkas
final label = switch (status) {
  'active'   => 'Aktif',
  'inactive' => 'Tidak Aktif',
  _          => 'Tidak Diketahui',     // _ adalah wildcard/default
};

Switch Expression dengan Guard Clause #

// Guard clause dengan 'when'
String kategoriUmur(int umur) => switch (umur) {
  < 0   => throw ArgumentError('Umur tidak valid'),
  < 13  => 'Anak-anak',
  < 18  => 'Remaja',
  < 60  when umur.isEven => 'Dewasa (genap)',   // guard!
  < 60  => 'Dewasa',
  _     => 'Lansia',
};

Switch Expression di Flutter #

// Sangat berguna untuk menentukan widget berdasarkan state
Widget build(BuildContext context) {
  return switch (viewModel.state) {
    LoadingState()          => const CircularProgressIndicator(),
    ErrorState(:final msg)  => ErrorWidget(message: msg),
    EmptyState()            => const EmptyView(),
    DataState(:final items) => ItemList(items: items),
  };
}

Sealed Classes — Exhaustive Type Hierarchy #

Sealed classes memungkinkan kamu mendefinisikan hierarki tipe yang tertutup (closed) — semua subtype harus dideklarasikan di library yang sama. Sebagai imbalannya, compiler bisa memeriksa apakah semua kemungkinan subtype sudah ditangani.

// Mendefinisikan sealed class
sealed class Result<T> {}

class Success<T> extends Result<T> {
  final T data;
  Success(this.data);
}

class Failure<T> extends Result<T> {
  final String message;
  final Exception? exception;
  Failure(this.message, [this.exception]);
}

class Loading<T> extends Result<T> {}

Exhaustiveness Checking #

Manfaat utama sealed class: jika kamu lupa menangani salah satu subtype, compiler akan memberi error:

Future<Result<User>> fetchUser(String id) async {
  try {
    final user = await api.getUser(id);
    return Success(user);
  } catch (e) {
    return Failure('Gagal memuat user', e as Exception);
  }
}

// Menggunakan dengan switch expression
Widget buildUserWidget(Result<User> result) {
  return switch (result) {
    Success(:final data) => UserCard(user: data),
    Failure(:final message) => ErrorView(message: message),
    Loading() => const CircularProgressIndicator(),
    // Jika salah satu case di atas dihapus --> ERROR compile time!
    // "The type 'Result<User>' is not exhaustively matched"
  };
}

Sealed Class untuk State Management #

Ini adalah use case paling umum sealed class di Flutter:

// state.dart -- semua state di satu file
sealed class ProductState {}

class ProductInitial extends ProductState {}

class ProductLoading extends ProductState {}

class ProductLoaded extends ProductState {
  final List<Product> products;
  final int totalCount;
  ProductLoaded({required this.products, required this.totalCount});
}

class ProductError extends ProductState {
  final String message;
  ProductError(this.message);
}

class ProductEmpty extends ProductState {}

// Di ViewModel:
ProductState _state = ProductInitial();

// Di Widget -- compiler menjamin semua state ditangani:
Widget build(BuildContext context) {
  return switch (state) {
    ProductInitial()          => const SizedBox.shrink(),
    ProductLoading()          => const LoadingSpinner(),
    ProductLoaded(:final products, :final totalCount) =>
        ProductGrid(products: products, total: totalCount),
    ProductError(:final message) => ErrorView(message: message),
    ProductEmpty()            => const EmptyProductView(),
  };
}
Sealed class adalah cara terbaik untuk memodelkan state di Flutter. Kombinasi sealed class + switch expression membuat penanganan state exhaustive — compiler memastikan kamu tidak pernah lupa menangani satu pun kondisi yang mungkin terjadi.

Class Modifiers — Kontrol Penggunaan Class #

Dart 3 memperkenalkan beberapa modifier baru untuk mengontrol bagaimana sebuah class bisa digunakan:

// base -- bisa di-extend tapi tidak di-implement
base class Animal {
  void breathe() => print('Bernapas');
}
// class Anjing extends Animal {}   // OK
// class Robot implements Animal {} // ERROR

// interface -- bisa di-implement tapi tidak di-extend
interface class Serializable {
  Map<String, dynamic> toJson();
}
// class Produk implements Serializable { ... }  // OK
// class JsonData extends Serializable {}         // ERROR

// final -- tidak bisa di-extend maupun di-implement dari luar library
final class ImmutableConfig {
  final String apiUrl;
  final int timeout;
  const ImmutableConfig(this.apiUrl, this.timeout);
}

// mixin class -- bisa digunakan sebagai class dan sebagai mixin
mixin class Logging {
  void log(String message) => print('[LOG] $message');
}
class Service with Logging { }   // sebagai mixin
class Logger extends Logging { } // sebagai class

Tabel Perbandingan Class Modifiers #

ModifierConstruct?Extend?Implement?Mix-in?
class
abstract
base
interface
final
sealed✅*✅*
mixin class

*hanya di library yang sama


Extension Types — Zero-Cost Wrapper #

Extension Types (diperkenalkan di Dart 3.3) adalah cara membuat wrapper di atas tipe yang sudah ada tanpa overhead runtime. Berbeda dengan wrapper class biasa, extension type di-compile away — tidak ada allocasi objek tambahan saat runtime.

// Wrapper class biasa (ada overhead alokasi)
class Meter {
  final double value;
  const Meter(this.value);
  Meter operator +(Meter other) => Meter(value + other.value);
}

// Extension type (zero-cost -- tidak ada alokasi runtime)
extension type Meters(double value) {
  Meters operator +(Meters other) => Meters(value + other.value);
  String get label => '${value}m';

  // Konversi ke tipe lain
  Kilometers get toKilometers => Kilometers(value / 1000);
}

extension type Kilometers(double value) {
  String get label => '${value}km';
}

void main() {
  final a = Meters(100);
  final b = Meters(50);
  final total = a + b;
  print(total.label);        // 150.0m

  // Type safety: Meters dan Kilometers tidak bisa dicampur
  // final invalid = a + Kilometers(1); // ERROR compile time!
}

Use Case Extension Types #

// Membuat ID types yang type-safe tanpa overhead
extension type UserId(String value) {}
extension type ProductId(String value) {}
extension type OrderId(int value) {}

Future<User> getUser(UserId id) async { ... }

void main() async {
  final userId = UserId('user-123');
  final productId = ProductId('prod-456');

  await getUser(userId);
  // await getUser(productId); // ERROR! productId bukan UserId
  // Meski keduanya berisi String, type system mencegah kekeliruan
}

if-case dan Pattern Guard #

Dart 3 memperluas if statement dengan kemampuan pattern matching:

// if-case: cek dan destructure sekaligus
final dynamic json = {'type': 'circle', 'radius': 5.0};

if (json case {'type': 'circle', 'radius': double r}) {
  print('Luas lingkaran: ${3.14 * r * r}');
} else if (json case {'type': 'square', 'side': double s}) {
  print('Luas persegi: ${s * s}');
}

// Pattern guard dengan 'when'
void processValue(Object? value) {
  if (value case int n when n > 0) {
    print('Bilangan positif: $n');
  } else if (value case String s when s.isNotEmpty) {
    print('String tidak kosong: $s');
  } else if (value case null) {
    print('Null value');
  }
}

Ringkasan #

  • Records memungkinkan fungsi mengembalikan beberapa nilai bertipe berbeda secara type-safe, ringkas, dan dengan structural equality bawaan.
  • Patterns adalah cara baru untuk mencocokkan dan mendekonstruksi nilai — bekerja di switch, if case, variable declaration, dan assignment.
  • Switch expressions mengubah switch dari statement menjadi ekspresi yang bisa digunakan di mana nilai diharapkan — jauh lebih ringkas dari switch statement.
  • Sealed classes mendefinisikan hierarki tipe yang tertutup dan memungkinkan exhaustiveness checking — compiler memastikan semua kemungkinan subtype ditangani.
  • Class modifiers (base, interface, final, mixin class) memberikan kontrol granular tentang bagaimana sebuah class bisa digunakan.
  • Extension types (Dart 3.3) adalah zero-cost wrapper — memberikan type safety tanpa overhead alokasi runtime.
  • Kombinasi sealed class + switch expression adalah cara terbaik memodelkan dan menangani state di aplikasi Flutter modern.

← Sebelumnya: Asynchronous Programming   Berikutnya: Collections →

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