Skip to content

Bab 2 — Dart Esensial untuk Flutter

Kamu tidak perlu menguasai seluruh Dart untuk mulai produktif dengan Flutter. Tetapi ada satu kumpulan konsep yang muncul di hampir setiap baris kode Flutter, dan kalau kamu samar-samar memahaminya, kamu akan terus menebak-nebak. Bab ini membahas kumpulan itu — tidak lebih, tidak kurang.

Fungsi: unit kerja terkecil

Segala sesuatu di Dart dimulai dari fungsi. Bentuk dasarnya:

dart
// tipe_kembalian namaFungsi(parameter) { badan fungsi }
int tambah(int a, int b) {
  return a + b;
}

void sapa(String nama) {
  print('Halo, $nama!');
}

void berarti fungsi tidak mengembalikan apa pun. Perhatikan juga $nama di dalam string — itu string interpolation, cara Dart menyisipkan nilai ke dalam teks. Untuk ekspresi yang lebih rumit, bungkus dengan kurung kurawal:

dart
final harga = 15000;
final jumlah = 3;
print('Total: Rp${harga * jumlah}');   // Total: Rp45000

Kalau badan fungsi hanya satu ekspresi, ada bentuk singkat memakai panah. Ini sangat sering muncul di kode Flutter:

dart
// Bentuk panjang
int kuadrat(int x) {
  return x * x;
}

// Bentuk panah — persis sama artinya
int kuadratRingkas(int x) => x * x;

Bentuk panah paling sering kamu lihat pada callback tombol:

dart
ElevatedButton(
  onPressed: () => print('Tombol ditekan'),
  child: const Text('Tekan saya'),
)

Argumen posisional dan bernama

Ini pembedaan yang wajib kamu kuasai, karena seluruh API Flutter dibangun di atasnya.

Argumen posisional ditentukan oleh urutan. Kamu harus mengingat urutannya:

dart
double luasPersegiPanjang(double panjang, double lebar) => panjang * lebar;

luasPersegiPanjang(10, 5);   // 50 — tapi mana panjang, mana lebar?

Argumen bernama ditulis dalam kurung kurawal dan dipanggil dengan menyebut namanya. Urutannya bebas:

dart
double luas({required double panjang, required double lebar}) => panjang * lebar;

luas(lebar: 5, panjang: 10);   // 50 — jelas dan tidak ambigu

Kata kunci required berarti argumen itu wajib diisi. Tanpanya, argumen bernama bersifat opsional dan boleh dilewati.

Inilah sebabnya kode Flutter terlihat seperti ini:

dart
Container(
  width: 200,
  height: 100,
  color: Colors.blue,
  child: const Text('Halo'),
)

Bayangkan kalau Container memakai argumen posisional — kamu harus mengingat bahwa argumen kelima adalah warna dan kedelapan adalah padding. Argumen bernama membuat kode Flutter bisa dibaca seperti kalimat, meskipun sebuah widget punya puluhan properti yang bisa dikonfigurasi.

Variabel, tipe, dan tiga kata kunci

Dart bertipe kuat: setiap nilai punya tipe, dan compiler memeriksanya.

dart
String nama = 'Firnanda';
int umur = 25;
double tinggi = 172.5;
bool aktif = true;

Kalau tipenya sudah jelas dari nilai awalnya, kamu boleh memakai var dan membiarkan Dart menyimpulkannya sendiri:

dart
var nama = 'Firnanda';   // Dart tahu ini String
var umur = 25;           // Dart tahu ini int

Perlu ditekankan: var bukan berarti tipenya bebas. Setelah disimpulkan, tipenya terkunci. Baris berikut akan ditolak compiler:

dart
var umur = 25;
umur = 'dua puluh lima';   // ❌ Error: String tidak bisa masuk ke int

Kalau kamu benar-benar butuh wadah yang bisa diisi tipe apa pun, gunakan dynamic — tetapi pakailah sehemat mungkin, karena kamu membuang seluruh perlindungan compiler.

var, final, dan const

Ketiganya sering membingungkan, padahal perbedaannya bisa diringkas jadi dua pertanyaan: boleh diubah setelah diisi? dan nilainya sudah diketahui saat kompilasi?

dart
var jumlah = 0;
jumlah = 5;              // ✅ boleh, var bisa ditugasi ulang

final namaPengguna = 'Firnanda';
namaPengguna = 'Budi';   // ❌ error, final hanya bisa diisi sekali

const pi = 3.14159;
const sekarang = DateTime.now();   // ❌ error, waktu belum diketahui saat kompilasi

final sekarangOk = DateTime.now(); // ✅ boleh, final diisi saat runtime
Bisa ditugasi ulangNilai harus diketahui saat kompilasi
varYaTidak
finalTidakTidak
constTidakYa

Aturan praktis yang dipakai hampir semua pengembang Flutter:

Mulai dengan const. Kalau tidak bisa, pakai final. Kalau tetap tidak bisa, baru pakai var.

Alasannya bukan sekadar kerapian. Widget yang ditandai const tidak dibangun ulang ketika induknya dibangun ulang, karena Flutter tahu nilainya mustahil berubah. Menaburkan const di tempat yang tepat adalah optimasi performa termurah yang tersedia.

dart
// Setiap kali build() dipanggil, Text ini dibuat ulang
Text('Judul')

// Text ini dibuat sekali, lalu digunakan ulang selamanya
const Text('Judul')

Perbedaan halus yang penting

final mengunci variabelnya, bukan isinya. Kamu tetap bisa mengubah isi sebuah list yang dideklarasikan final:

dart
final angka = [1, 2, 3];
angka.add(4);          // ✅ boleh — isinya berubah
angka = [5, 6];        // ❌ error — variabelnya yang dikunci

const angkaTetap = [1, 2, 3];
angkaTetap.add(4);     // ❌ error saat runtime — const mengunci sampai ke isi

Null safety

Ini fitur Dart yang paling menyelamatkan, dan sekaligus yang paling sering membuat pemula terjebak di awal.

Secara bawaan, tidak ada variabel yang boleh bernilai null:

dart
String nama = null;   // ❌ Error: null tidak bisa masuk ke String

Kalau sebuah nilai memang mungkin kosong, kamu harus menyatakannya secara eksplisit dengan tanda tanya:

dart
String? nama;          // ✅ boleh null, dan nilai awalnya memang null
nama = 'Firnanda';     // ✅ boleh diisi
nama = null;           // ✅ boleh dikosongkan lagi

Konsekuensinya, kamu tidak bisa langsung memakai nilai yang mungkin null:

dart
String? nama;
print(nama.length);    // ❌ Error: nama mungkin null

Dart menyediakan beberapa cara menanganinya, dari yang paling aman sampai yang paling berbahaya:

dart
String? nama;

// 1. Periksa dulu — paling aman
if (nama != null) {
  print(nama.length);   // Dart sekarang yakin nama tidak null
}

// 2. Akses aman (?.) — hasilnya null kalau nama null
print(nama?.length);    // null, bukan crash

// 3. Nilai cadangan (??) — pakai ini kalau kiri null
print(nama ?? 'Tanpa Nama');

// 4. Isi kalau masih null (??=)
nama ??= 'Bawaan';

// 5. Paksa (!) — "aku jamin ini tidak null"
print(nama!.length);    // 💥 crash kalau ternyata null

Tanda seru itu disebut bang operator, dan ia adalah janjimu kepada compiler. Kalau janjimu salah, aplikasi crash. Gunakan hanya ketika kamu benar-benar bisa membuktikan nilainya tidak mungkin null — dan kalau ragu, pakai if atau ??.

Di Flutter, null safety paling sering muncul pada properti opsional widget:

dart
class KartuProfil extends StatelessWidget {
  const KartuProfil({
    super.key,
    required this.nama,
    this.deskripsi,          // opsional — boleh tidak diisi
  });

  final String nama;
  final String? deskripsi;   // karena opsional, tipenya nullable

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(nama, style: const TextStyle(fontWeight: FontWeight.bold)),
        // Tampilkan deskripsi hanya kalau ada
        if (deskripsi != null) Text(deskripsi!),
      ],
    );
  }
}

Kelas dan objek

Widget adalah kelas. Model data adalah kelas. Layanan jaringan adalah kelas. Memahami kelas berarti memahami hampir seluruh kode Flutter.

dart
class Mahasiswa {
  // Properti (instance variable)
  final String nama;
  final String nim;
  final int semester;

  // Konstruktor
  Mahasiswa({
    required this.nama,
    required this.nim,
    required this.semester,
  });

  // Getter — properti yang dihitung, bukan disimpan
  bool get sudahTingkatAkhir => semester >= 7;

  // Metode
  String deskripsi() => '$nama ($nim) — semester $semester';
}

Cara memakainya:

dart
final mhs = Mahasiswa(nama: 'Firnanda', nim: '2101', semester: 7);

print(mhs.nama);                // Firnanda
print(mhs.sudahTingkatAkhir);   // true
print(mhs.deskripsi());         // Firnanda (2101) — semester 7

Perhatikan required this.nama di konstruktor. Ini pintasan Dart: alih-alih menulis Mahasiswa({required String nama}) { this.nama = nama; }, kamu cukup menyebut this.nama dan Dart otomatis menugaskannya ke properti.

Initializer list

Kadang sebuah properti harus dihitung sebelum badan konstruktor berjalan. Untuk itu ada initializer list — bagian setelah tanda titik dua:

dart
import 'package:uuid/uuid.dart';

const uuid = Uuid();

class Pengeluaran {
  Pengeluaran({
    required this.judul,
    required this.jumlah,
    required this.tanggal,
  }) : id = uuid.v4();     // ← initializer list

  final String id;
  final String judul;
  final double jumlah;
  final DateTime tanggal;
}

Setiap Pengeluaran yang dibuat langsung mendapat id unik tanpa pemanggil harus memikirkannya. Ini pola yang akan kamu pakai lagi di Bab 8 ketika membangun Expense Tracker.

Konstruktor bernama

Sebuah kelas boleh punya lebih dari satu konstruktor, dibedakan oleh namanya:

dart
class Pengeluaran {
  Pengeluaran({required this.judul, required this.jumlah, required this.tanggal})
      : id = uuid.v4();

  // Konstruktor bernama untuk membuat objek dari JSON
  Pengeluaran.dariJson(Map<String, dynamic> json)
      : id = json['id'] as String,
        judul = json['judul'] as String,
        jumlah = (json['jumlah'] as num).toDouble(),
        tanggal = DateTime.parse(json['tanggal'] as String);

  final String id;
  final String judul;
  final double jumlah;
  final DateTime tanggal;

  Map<String, dynamic> toJson() => {
        'id': id,
        'judul': judul,
        'jumlah': jumlah,
        'tanggal': tanggal.toIso8601String(),
      };
}

Pola dariJson / toJson ini akan kamu pakai terus-menerus mulai dari Bab 14 sampai seluruh Bagian IV.

Enum

Ketika sebuah nilai hanya boleh berupa satu dari sekumpulan pilihan tetap, enum jauh lebih aman daripada string.

dart
enum Kategori { makanan, transportasi, hiburan, kerja }

Bandingkan dua pendekatan ini:

dart
// ❌ Rawan salah ketik, dan compiler tidak bisa membantu
String kategori = 'makanan';
if (kategori == 'makanann') { }   // typo lolos begitu saja

// ✅ Salah ketik langsung ditolak compiler
Kategori kategori = Kategori.makanan;
if (kategori == Kategori.makanan) { }

Enum berpasangan sangat baik dengan Map untuk memetakan setiap nilai ke sesuatu — misalnya ikon:

dart
const ikonKategori = {
  Kategori.makanan: Icons.lunch_dining,
  Kategori.transportasi: Icons.directions_car,
  Kategori.hiburan: Icons.movie,
  Kategori.kerja: Icons.work,
};

// Dipakai di widget
Icon(ikonKategori[pengeluaran.kategori]);

Enum modern di Dart bahkan bisa punya properti dan metode sendiri:

dart
enum Kategori {
  makanan('Makanan', Icons.lunch_dining),
  transportasi('Transportasi', Icons.directions_car),
  hiburan('Hiburan', Icons.movie),
  kerja('Kerja', Icons.work);

  const Kategori(this.label, this.ikon);

  final String label;
  final IconData ikon;
}

// Sekarang jauh lebih rapi
Text(Kategori.makanan.label);
Icon(Kategori.makanan.ikon);

Koleksi: List dan Map

List

dart
final angka = <int>[1, 2, 3, 4, 5];
final nama = <String>['Ani', 'Budi', 'Citra'];

print(angka.length);      // 5
print(angka.first);       // 1
print(angka.last);        // 5
print(angka[2]);          // 3 — indeks dimulai dari 0

angka.add(6);
angka.remove(1);
angka.removeAt(0);

Notasi <int> itu generic — cara memberi tahu Dart tipe apa yang boleh masuk ke dalam list. List<int> hanya menerima bilangan bulat, List<Widget> hanya menerima widget. Inilah alasan children: sebuah Column bertipe List<Widget>.

Operasi list yang paling sering dipakai di Flutter:

dart
final pengeluaran = <Pengeluaran>[/* ... */];

// map — ubah setiap elemen menjadi sesuatu yang lain
final judulSemua = pengeluaran.map((p) => p.judul).toList();

// where — saring berdasarkan syarat
final mahal = pengeluaran.where((p) => p.jumlah > 100000).toList();

// fold — akumulasi menjadi satu nilai
final total = pengeluaran.fold<double>(0, (jumlah, p) => jumlah + p.jumlah);

// any / every — pemeriksaan boolean
final adaYangMahal = pengeluaran.any((p) => p.jumlah > 1000000);
final semuaTerisi = pengeluaran.every((p) => p.judul.isNotEmpty);

map() khususnya adalah jembatan antara data dan UI. Pola berikut muncul di hampir setiap aplikasi Flutter:

dart
Column(
  children: pengeluaran.map((p) => KartuPengeluaran(data: p)).toList(),
)

Tanda titik tiga (...) adalah spread operator — ia membongkar sebuah list ke dalam list lain:

dart
Column(
  children: [
    const Text('Daftar Pengeluaran'),
    const SizedBox(height: 12),
    ...pengeluaran.map((p) => KartuPengeluaran(data: p)),   // dibongkar di sini
    const SizedBox(height: 12),
    const Text('Selesai'),
  ],
)

Tanpa spread operator kamu akan menyisipkan sebuah List<Widget> ke dalam List<Widget> — yang tentu saja ditolak.

Map

Map menyimpan pasangan kunci dan nilai:

dart
final skor = <String, int>{
  'Ani': 90,
  'Budi': 85,
  'Citra': 95,
};

print(skor['Ani']);          // 90
print(skor['Dedi']);         // null — kunci tidak ada
print(skor['Dedi'] ?? 0);    // 0 — dengan nilai cadangan

skor['Dedi'] = 70;           // menambah entri
skor.remove('Budi');

for (final entri in skor.entries) {
  print('${entri.key}: ${entri.value}');
}

Map bertipe Map<String, dynamic> adalah bentuk baku data JSON di Dart, jadi kamu akan sangat sering menemuinya di bab networking dan Firebase.

Alur kendali

Kondisi

dart
if (nilai >= 85) {
  print('A');
} else if (nilai >= 70) {
  print('B');
} else {
  print('C');
}

Untuk kondisi sederhana, ekspresi ternari lebih ringkas dan bisa dipakai langsung di dalam widget tree:

dart
// kondisi ? nilaiKalauBenar : nilaiKalauSalah
final status = umur >= 17 ? 'Dewasa' : 'Anak-anak';

// Di dalam widget
Text(sudahLogin ? 'Selamat datang' : 'Silakan masuk')

Operator logika menggabungkan beberapa kondisi:

dart
// && — DAN, keduanya harus benar
if (judul.isNotEmpty && jumlah > 0) { }

// || — ATAU, cukup salah satu benar
if (jumlah == null || jumlah <= 0) { }

// ! — TIDAK, membalik nilai
if (!sudahLogin) { }

Contoh nyata validasi form, yang akan kamu temui lagi di Bab 6:

dart
final jumlahMasuk = double.tryParse(_pengendaliJumlah.text);
final jumlahTidakValid = jumlahMasuk == null || jumlahMasuk <= 0;

if (_pengendaliJudul.text.trim().isEmpty ||
    jumlahTidakValid ||
    _tanggalTerpilih == null) {
  // Tampilkan dialog kesalahan
  return;
}

Perhatikan double.tryParse — berbeda dari double.parse, ia mengembalikan null alih-alih melempar error ketika teks tidak bisa diubah menjadi angka. Untuk memvalidasi input pengguna, tryParse hampir selalu pilihan yang benar.

Perulangan

dart
// for klasik
for (var i = 0; i < 5; i++) {
  print(i);
}

// for-in — lebih disukai ketika kamu tidak butuh indeksnya
for (final nama in daftarNama) {
  print(nama);
}

// while
var sisa = 3;
while (sisa > 0) {
  print('Sisa $sisa');
  sisa--;
}

Dart juga punya for di dalam literal koleksi, yang sangat berguna di widget tree:

dart
Column(
  children: [
    for (final p in pengeluaran) KartuPengeluaran(data: p),
  ],
)

Ini sering lebih mudah dibaca daripada .map().toList(), terutama kalau kamu juga butuh if di tempat yang sama:

dart
Column(
  children: [
    const Text('Ringkasan'),
    if (pengeluaran.isEmpty)
      const Text('Belum ada data')
    else
      for (final p in pengeluaran) KartuPengeluaran(data: p),
  ],
)

Asinkron: Future dan async/await

Banyak hal di aplikasi tidak selesai seketika: mengambil data dari internet, membaca berkas, menunggu pengguna memilih tanggal. Dart memodelkan "hasil yang belum ada, tetapi akan ada" sebagai Future.

dart
Future<String> ambilNamaPengguna() async {
  // Simulasi penundaan jaringan
  await Future.delayed(const Duration(seconds: 2));
  return 'Firnanda';
}

Dua kata kunci yang bekerja berpasangan:

  • async menandai bahwa fungsi ini asinkron dan akan mengembalikan Future.
  • await menghentikan sementara eksekusi fungsi sampai Future selesai, lalu melanjutkan dengan hasilnya.
dart
void muatData() async {
  print('Mulai memuat...');
  final nama = await ambilNamaPengguna();   // menunggu di sini
  print('Selesai: $nama');                  // baru berjalan setelah 2 detik
}

Tanpa await, kamu mendapat objek Future itu sendiri, bukan isinya:

dart
final hasil = ambilNamaPengguna();
print(hasil);   // Instance of 'Future<String>' — bukan yang kamu mau

Menangani kegagalan memakai try/catch:

dart
Future<void> muatData() async {
  try {
    final nama = await ambilNamaPengguna();
    print('Berhasil: $nama');
  } catch (kesalahan) {
    print('Gagal: $kesalahan');
  } finally {
    print('Selalu dijalankan, berhasil atau gagal');
  }
}

Di Flutter, pola ini muncul misalnya ketika menampilkan pemilih tanggal:

dart
void _bukaPemilihTanggal() async {
  final sekarang = DateTime.now();
  final tanggalAwal = DateTime(sekarang.year - 1, sekarang.month, sekarang.day);

  final tanggalDipilih = await showDatePicker(
    context: context,
    initialDate: sekarang,
    firstDate: tanggalAwal,
    lastDate: sekarang,
  );

  // showDatePicker mengembalikan null kalau pengguna membatalkan
  setState(() {
    _tanggalTerpilih = tanggalDipilih;
  });
}

Peringatan penting

Setelah sebuah await, widget yang memicu operasi itu mungkin sudah dilepas dari layar. Mengakses context sesudahnya bisa menyebabkan error. Karena itu Flutter menyediakan pemeriksaan mounted:

dart
void _kirimData() async {
  final hasil = await layanan.kirim();
  if (!mounted) return;              // widget sudah tidak ada, hentikan
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text(hasil)),
  );
}

Stream adalah kerabat Future untuk nilai yang datang berkali-kali, bukan sekali. Ia dibahas tuntas di Bab 20.

Fungsi sebagai nilai

Di Dart, fungsi adalah nilai biasa — bisa disimpan di variabel, dioper sebagai argumen, dan dikembalikan dari fungsi lain. Ini fondasi dari cara Flutter menangani interaksi.

dart
// Menyimpan fungsi di variabel
void Function() aksi = () => print('dijalankan');
aksi();

// Fungsi yang menerima fungsi
void jalankanDuaKali(void Function() tugas) {
  tugas();
  tugas();
}

jalankanDuaKali(() => print('halo'));   // dicetak dua kali

Tipe fungsi ditulis sebagai TipeKembalian Function(TipeParameter):

TipeArtinya
void Function()Tanpa parameter, tanpa nilai kembali
void Function(String)Menerima String, tanpa nilai kembali
bool Function(int)Menerima int, mengembalikan bool
String? Function(String?)Validator form — pola paling umum di Flutter

Di Flutter, mengoper fungsi adalah cara widget anak berkomunikasi ke atas dengan induknya:

dart
class TombolJawaban extends StatelessWidget {
  const TombolJawaban({
    super.key,
    required this.teks,
    required this.saatDipilih,
  });

  final String teks;
  final void Function() saatDipilih;   // fungsi dititipkan induk

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: saatDipilih,          // dipanggil saat ditekan
      child: Text(teks),
    );
  }
}

Induknya menentukan apa yang terjadi:

dart
TombolJawaban(
  teks: 'Jakarta',
  saatDipilih: () {
    setState(() => _indeksSoal++);
  },
)

Pola "anak melapor, induk memutuskan" ini adalah cara paling dasar mengelola state di Flutter, dan akan dibahas lebih dalam di Bab 5.

Fitur Dart lain yang berguna

Cascade notation

Tanda .. memungkinkan kamu memanggil beberapa metode pada objek yang sama tanpa mengulang nama variabelnya:

dart
final pengendali = TextEditingController()
  ..text = 'Nilai awal'
  ..selection = const TextSelection.collapsed(offset: 0);

Pattern matching dengan switch

Dart modern punya switch yang jauh lebih ekspresif:

dart
String labelKategori(Kategori k) => switch (k) {
      Kategori.makanan => 'Makanan & Minuman',
      Kategori.transportasi => 'Transportasi',
      Kategori.hiburan => 'Hiburan',
      Kategori.kerja => 'Pekerjaan',
    };

Karena semua nilai enum tercakup, compiler tidak menuntut klausa default — dan kalau suatu saat kamu menambah nilai enum baru, compiler akan mengingatkanmu bahwa switch ini belum lengkap. Perlindungan gratis.

Type casting

Ketika kamu tahu tipe sebenarnya dari sebuah nilai dynamic, gunakan as:

dart
final Map<String, dynamic> json = /* dari API */;
final judul = json['judul'] as String;
final jumlah = (json['jumlah'] as num).toDouble();

Perhatikan as num sebelum .toDouble(). JSON kadang mengirim 10 (int) dan kadang 10.5 (double); melakukan cast langsung ke double akan gagal untuk kasus pertama. Melalui num dulu menyelamatkanmu dari bug yang sangat menyebalkan untuk dilacak.

Latihan Mandiri

Kerjakan salah satu, beberapa, atau semuanya secara berurutan untuk melatih pemahamanmu sampai benar-benar lekat.

Variasi 1: Katalog Film — ⭐⭐ · 30–45 menit

Tantangan: Tulis satu berkas Dart murni (tanpa Flutter, jalankan dengan dart run) yang memodelkan katalog film sederhana: sebuah kelas Film dengan judul, tahun, durasi menit, dan genre bertipe enum. Lalu buat daftar berisi minimal enam film dan hasilkan tiga ringkasan darinya — film terpanjang, jumlah film per genre, dan daftar judul film di atas tahun 2015 yang terurut.

Kriteria selesai:

  • Film memakai argumen bernama dengan required, seluruh propertinya final, dan punya konstruktor const.
  • Genre adalah enum, bukan String. Sinopsis boleh kosong dan dimodelkan sebagai String? yang ditangani tanpa memakai !.
  • Ketiga ringkasan dihitung dengan map, where, fold, atau sort — bukan dengan perulangan for yang menumpuk hasil ke variabel sementara.
  • Ada satu fungsi yang menerima fungsi lain sebagai parameter, misalnya tampilkanFilm(List<Film> film, bool Function(Film) syarat).

Petunjuk: Untuk "film terpanjang", reduce lebih cocok daripada fold karena hasilnya bertipe sama dengan isinya. Untuk "jumlah per genre", perhatikan bahwa Map<Genre, int> yang diisi bertahap butuh nilai awal — operator ??= atau metode putIfAbsent menyelesaikannya dalam satu baris. Ingat bahwa sort() mengubah list aslinya, jadi salin dulu dengan List.from() kalau urutan aslinya masih kamu butuhkan. Dan untuk sinopsis yang boleh kosong, ?? di titik pemakaian hampir selalu lebih baik daripada memeriksa null di banyak tempat.

Variasi 2: Tarif Parkir — ⭐⭐ · 30–45 menit

Tantangan: Modelkan sistem tarif parkir: kelas Kendaraan dengan nomor pelat, jenis (enum: motor, mobil, truk), dan waktu masuk. Hitung tarifnya berdasarkan aturan berjenjang — satu jam pertama harga tetap, jam berikutnya per jam, dan tarif maksimum harian yang tidak boleh dilewati.

Kriteria selesai:

  • Tarif per jenis kendaraan disimpan sebagai Map konstan di tingkat teratas, bukan rantai if di dalam fungsi hitung.
  • Waktu keluar bertipe DateTime? — kendaraan yang masih parkir ditangani tanpa !.
  • Ada satu switch pada enum yang diperiksa kelengkapannya oleh compiler, tanpa cabang default.

Petunjuk: Hilangkan default dari switch-mu dan tambahkan satu nilai baru ke enum — compiler akan langsung menunjukkan setiap tempat yang perlu diperbarui. Itulah keuntungan enum yang tidak pernah kamu dapat dari String. Untuk membulatkan durasi ke atas per jam, perhatikan bahwa Duration punya inMinutes yang lebih aman dijadikan dasar perhitungan daripada inHours yang sudah membulatkan ke bawah.

Variasi 3: Ongkos Kirim Bertingkat — ⭐⭐ · 40–60 menit

Tantangan: Bangun kalkulator ongkos kirim yang menerima daftar aturan sebagai fungsi. Tiap aturan memeriksa satu hal (berat, jarak, barang mudah pecah, pengiriman ekspres) dan mengembalikan tambahan biayanya. Total ongkos adalah hasil menjalankan seluruh aturan pada satu paket.

Kriteria selesai:

  • Aturan disimpan sebagai List<double Function(Paket)>, dan menambah aturan baru tidak menyentuh fungsi penghitung total.
  • Total dihitung dengan fold, bukan perulangan yang menumpuk ke variabel.
  • Ada minimal satu aturan yang dibuat lewat fungsi lain yang mengembalikan fungsi — misalnya diskonPersen(10) yang menghasilkan aturan potongan sepuluh persen.

Petunjuk: Fungsi yang mengembalikan fungsi terasa aneh sampai kamu membutuhkannya: double Function(Paket) diskonPersen(int persen) menutup nilai persen di dalam fungsi yang dikembalikannya, sehingga kamu bisa membuat sepuluh aturan diskon berbeda dari satu definisi. Ini pola yang akan kamu temui lagi di seluruh Flutter, karena hampir semua callback widget adalah fungsi yang dioper sebagai nilai.

Variasi 4: Klasemen Liga — ⭐⭐⭐ · 45–60 menit

Tantangan: Dari daftar hasil pertandingan (tim kandang, tim tandang, skor masing-masing), hasilkan klasemen lengkap: main, menang, seri, kalah, selisih gol, dan poin. Urutkan berdasarkan poin, lalu selisih gol, lalu nama tim secara alfabet sebagai penentu terakhir.

Kriteria selesai:

  • Klasemen dibangun dari daftar pertandingan lewat transformasi koleksi — tidak ada tabel klasemen yang ditulis manual.
  • Pengurutan bertingkat tiga tingkat bekerja benar, dan kamu sudah mengujinya dengan dua tim yang poin dan selisih golnya identik.
  • Kelas klasemen punya copyWith, dan penambahan hasil pertandingan menghasilkan objek baru alih-alih mengubah yang lama.

Petunjuk: Pengurutan bertingkat di Dart memanfaatkan fakta bahwa compareTo mengembalikan nol ketika dua nilai sama — jadi kamu bisa memeriksa hasilnya dan lanjut ke kriteria berikutnya hanya kalau nol. Untuk membangun klasemen, satu Map<String, Klasemen> yang diisi sambil menelusuri daftar pertandingan jauh lebih sederhana daripada mencoba melakukannya dalam satu rantai map.

Variasi 5: Antrean Layanan — ⭐⭐⭐ · 45–60 menit

Tantangan: Simulasikan antrean loket yang memproses permintaan secara asinkron. Tiap permintaan butuh waktu berbeda, sebagian gagal. Proses seluruh antrean, kumpulkan hasilnya, dan laporkan berapa yang berhasil, berapa yang gagal, dan total waktu yang dibutuhkan.

Kriteria selesai:

  • Ada satu bagian yang memproses permintaan berurutan dan satu yang memprosesnya bersamaan, dan kamu bisa menunjukkan selisih waktunya.
  • Kegagalan satu permintaan tidak menghentikan sisanya.
  • Ada satu permintaan yang sengaja dibuat menggantung, dan ia dihentikan oleh batas waktu alih-alih menahan seluruh program.

Petunjuk: await di dalam perulangan memproses satu per satu; Future.wait pada daftar Future memprosesnya bersamaan. Perbedaan waktunya akan mencolok, dan memahami kapan memilih yang mana adalah salah satu keputusan paling sering muncul di kode Flutter sungguhan. Untuk kegagalan yang tidak menghentikan sisanya, perhatikan bahwa Future.wait punya parameter yang mengatur perilaku itu — atau bungkus tiap Future dengan penangkap error sebelum menunggunya.

Variasi 6: Pembaca Konfigurasi — ⭐⭐⭐ · 45–60 menit

Tantangan: Tulis pembaca konfigurasi yang menerima Map<String, dynamic> bersarang (bayangkan hasil jsonDecode) dan mengubahnya menjadi objek bertipe kuat. Setiap field boleh hilang, boleh bertipe salah, dan pembaca harus tetap menghasilkan objek yang valid dengan nilai bawaan.

Kriteria selesai:

  • Tidak ada satu pun ! di seluruh berkas.
  • Field bersarang yang induknya hilang tetap menghasilkan nilai bawaan, bukan error.
  • Angka yang datang sebagai int padahal diminta double tetap terbaca benar.
  • Ada daftar peringatan yang mencatat field mana saja yang memakai nilai bawaan, sehingga kesalahan konfigurasi bisa dilaporkan.

Petunjuk: Rantai ?. dan ?? menyelesaikan sebagian besar kasus dalam satu ekspresi, tetapi untuk pemeriksaan tipe kamu butuh is yang sekaligus mempromosikan tipenya — dan ingat bahwa promosi tipe tidak bekerja pada properti kelas, hanya pada variabel lokal. Untuk angka, as num? diikuti konversi menyelamatkanmu dari kasus JSON yang menyimpan 14 untuk nilai 14.0; ini jebakan yang akan kamu temui berkali-kali di seluruh situs ini.

Variasi 7: Wadah Generik — ⭐⭐⭐⭐ · 60–90 menit

Tantangan: Buat kelas generik Hasil<T> yang mewakili operasi yang bisa berhasil membawa nilai T atau gagal membawa pesan. Lalu pakai ia untuk membungkus beberapa operasi berantai, di mana kegagalan di tengah menghentikan rantai tanpa satu pun try/catch di sisi pemanggil.

Kriteria selesai:

  • Hasil<T> punya dua bentuk yang bisa dibedakan compiler lewat switch tanpa cabang default.
  • Ada metode yang mengubah nilai di dalamnya kalau berhasil dan meneruskan kegagalan apa adanya kalau gagal.
  • Merantai empat operasi di mana operasi kedua gagal menghasilkan pesan kegagalan operasi kedua, dan operasi ketiga serta keempat tidak pernah dijalankan.

Petunjuk: sealed class dengan dua subkelas memberimu tepat yang kamu butuhkan: compiler tahu hanya ada dua kemungkinan, sehingga switch-mu diperiksa kelengkapannya. Pola ini adalah versi sederhana dari apa yang dipakai pustaka seperti dartz dan fpdart, dan juga fondasi konseptual dari AsyncValue di Riverpod yang akan kamu temui di Bagian III.

Variasi 8: Mesin Keadaan Kopi — ⭐⭐⭐⭐ · 60–90 menit

Tantangan: Modelkan mesin kopi sebagai mesin keadaan: siap, menggiling, menyeduh, membersihkan, dan butuh perawatan. Setiap perintah hanya sah pada keadaan tertentu, dan mesin harus menolak perintah yang tidak sah dengan alasan yang jelas — bukan diam atau melempar error mentah.

Kriteria selesai:

  • Tabel transisi keadaan dinyatakan sebagai data (Map atau serupa), bukan sebagai rantai if yang panjang.
  • Menambahkan satu keadaan baru menyebabkan compiler menunjukkan setiap tempat yang perlu diperbarui.
  • Mesin mencatat riwayat transisi, dan riwayat itu tidak bisa diubah dari luar.
  • Perintah yang tidak sah mengembalikan alasan, bukan melempar exception.

Petunjuk: Mengembalikan List.unmodifiable dari getter riwayat adalah cara termurah mencegah kode lain merusak state internalmu — pola yang akan muncul lagi persis seperti ini di Bab 19 saat membahas ChangeNotifier. Untuk tabel transisi, Map<Keadaan, Set<Perintah>> cukup untuk memeriksa keabsahan, dan Map<(Keadaan, Perintah), Keadaan> dengan record sebagai kunci untuk sekaligus menentukan tujuannya.

Ikhtisar

  • Argumen bernama dengan required adalah tulang punggung API Flutter — ia membuat widget dengan puluhan properti tetap terbaca.
  • Pilih constfinalvar, dalam urutan itu. Widget const tidak dibangun ulang, jadi ini optimasi performa yang gratis.
  • final mengunci variabelnya, const mengunci sampai ke isinya.
  • Null safety memaksamu menyatakan nilai yang boleh kosong dengan ?. Gunakan ?., ??, dan pemeriksaan if — hindari ! kecuali kamu bisa membuktikan nilainya pasti ada.
  • Konstruktor dengan required this.x menyingkat penugasan properti; initializer list (: id = uuid.v4()) untuk nilai yang harus dihitung.
  • Enum menggantikan string ajaib dan memberi perlindungan compiler.
  • map(), where(), fold(), spread (...), dan for di dalam literal adalah jembatan utama dari data menuju widget tree.
  • async/await mengubah kode asinkron menjadi terbaca seperti kode biasa. Selalu periksa mounted sebelum menyentuh context setelah sebuah await.
  • Fungsi adalah nilai. Mengoper fungsi dari induk ke anak adalah cara paling dasar widget berkomunikasi ke atas.
  • Saat mengurai JSON, lewati as num sebelum .toDouble() agar aman terhadap nilai bilangan bulat.

Berikutnya: Bab 3 — Widget & Widget Tree, tempat semua konsep Dart ini mulai berbentuk antarmuka.

Transkrip asli

Disintesis dari 1_flutter_core/2_basics.md (video 5–33, 49–72), 1_flutter_zero-to-hero/1_key-concepts.md, 1_flutter_zero-to-hero/4_fixing-bugs-2-step-process.md, dan 1_flutter_zero-to-hero/6_user-input.md (arrow function). Lihat PDF Flutter Core dan PDF Zero to Hero.

Rangkuman pembelajaran pribadi, disusun ulang dari beberapa kursus Flutter.