Tampilan
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: Rp45000Kalau 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 ambiguKata 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 intPerlu 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 intKalau 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 ulang | Nilai harus diketahui saat kompilasi | |
|---|---|---|
var | Ya | Tidak |
final | Tidak | Tidak |
const | Tidak | Ya |
Aturan praktis yang dipakai hampir semua pengembang Flutter:
Mulai dengan
const. Kalau tidak bisa, pakaifinal. Kalau tetap tidak bisa, baru pakaivar.
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 isiNull 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 StringKalau 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 lagiKonsekuensinya, kamu tidak bisa langsung memakai nilai yang mungkin null:
dart
String? nama;
print(nama.length); // ❌ Error: nama mungkin nullDart 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 nullTanda 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 7Perhatikan 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:
asyncmenandai bahwa fungsi ini asinkron dan akan mengembalikanFuture.awaitmenghentikan sementara eksekusi fungsi sampaiFutureselesai, 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 mauMenangani 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 kaliTipe fungsi ditulis sebagai TipeKembalian Function(TipeParameter):
| Tipe | Artinya |
|---|---|
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:
Filmmemakai argumen bernama denganrequired, seluruh propertinyafinal, dan punya konstruktorconst.- Genre adalah
enum, bukanString. Sinopsis boleh kosong dan dimodelkan sebagaiString?yang ditangani tanpa memakai!. - Ketiga ringkasan dihitung dengan
map,where,fold, atausort— bukan dengan perulanganforyang 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
Mapkonstan di tingkat teratas, bukan rantaiifdi dalam fungsi hitung. - Waktu keluar bertipe
DateTime?— kendaraan yang masih parkir ditangani tanpa!. - Ada satu
switchpadaenumyang diperiksa kelengkapannya oleh compiler, tanpa cabangdefault.
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
intpadahal dimintadoubletetap 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 lewatswitchtanpa cabangdefault.- 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 (
Mapatau serupa), bukan sebagai rantaiifyang 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
requiredadalah tulang punggung API Flutter — ia membuat widget dengan puluhan properti tetap terbaca. - Pilih
const→final→var, dalam urutan itu. Widgetconsttidak dibangun ulang, jadi ini optimasi performa yang gratis. finalmengunci variabelnya,constmengunci sampai ke isinya.- Null safety memaksamu menyatakan nilai yang boleh kosong dengan
?. Gunakan?.,??, dan pemeriksaanif— hindari!kecuali kamu bisa membuktikan nilainya pasti ada. - Konstruktor dengan
required this.xmenyingkat penugasan properti; initializer list (: id = uuid.v4()) untuk nilai yang harus dihitung. - Enum menggantikan string ajaib dan memberi perlindungan compiler.
map(),where(),fold(), spread (...), danfordi dalam literal adalah jembatan utama dari data menuju widget tree.async/awaitmengubah kode asinkron menjadi terbaca seperti kode biasa. Selalu periksamountedsebelum menyentuhcontextsetelah sebuahawait.- Fungsi adalah nilai. Mengoper fungsi dari induk ke anak adalah cara paling dasar widget berkomunikasi ke atas.
- Saat mengurai JSON, lewati
as numsebelum.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.