Tampilan
Bab 13 — Debugging & DevTools
Menulis kode yang salah adalah bagian normal dari pekerjaan ini. Yang membedakan pengembang yang cepat dan yang lambat bukan seberapa sedikit bug yang mereka buat, melainkan seberapa cepat mereka menemukannya.
Bab ini menutup Bagian I dengan keterampilan yang akan kamu pakai setiap hari: membaca pesan error, memakai debugger, dan membaca DevTools.
Proses dua langkah
Ketika sesuatu tidak berjalan, ada dua jenis masalah yang perlu kamu bedakan sejak awal, karena penanganannya sangat berbeda.
Tipe 1 — kode tidak mau berjalan. Editor menandai baris dengan garis merah, atau terminal menolak melakukan kompilasi. Ini sebenarnya kabar baik: compiler sudah menunjukkan letak masalahnya.
Tipe 2 — kode berjalan, tetapi hasilnya salah. Tidak ada error, tetapi tombolnya tidak berfungsi, angkanya salah, atau layarnya kosong. Ini yang lebih sulit, karena tidak ada yang menunjuk ke mana pun.
Ada masalah
│
┌───────┴────────┐
▼ ▼
Ada pesan error? Tidak ada error,
tapi hasilnya salah
│ │
▼ ▼
TIPE 1 TIPE 2
Baca pesannya Persempit lokasinya
dari atas dengan print/breakpoint
│ │
▼ ▼
Perbaiki Periksa asumsimu
satu per satu satu per satuTipe 1: membaca pesan error
Pesan error Flutter panjang dan menakutkan pada pandangan pertama. Tetapi strukturnya konsisten, dan begitu kamu tahu bagian mana yang penting, membacanya menjadi cepat.
Contoh error yang hampir pasti pernah kamu temui:
════════ Exception caught by rendering library ═══════════════════════
The following assertion was thrown during performLayout():
RenderFlex children have non-zero flex but incoming height constraints
are unbounded.
When a column is in a parent that does not provide a finite height
constraint, for example if it is in a vertical scrollable, it will try
to shrink-wrap its children along the vertical axis.
The affected RenderFlex is: RenderFlex#a1b2c relayoutBoundary=up1
...
The relevant error-causing widget was:
Column Column:file:///C:/proyek/lib/screens/beranda.dart:42:16
════════════════════════════════════════════════════════════════════Yang perlu kamu baca, berurutan:
Baris pertama setelah judul — inti masalahnya. Di sini: "RenderFlex children have non-zero flex but incoming height constraints are unbounded." Artinya ada
Expandeddi dalamColumnyang tidak tahu tingginya berapa.Paragraf penjelasan — Flutter sering menjelaskan penyebab umumnya. Di sini ia menyebut "if it is in a vertical scrollable" — dan memang, ini biasanya terjadi ketika
Columnberada di dalamSingleChildScrollView."The relevant error-causing widget was" — inilah baris terpenting. Ia memberimu berkas dan nomor baris yang tepat.
beranda.dart:42:16.
Abaikan sisanya kecuali kamu benar-benar buntu.
Error yang paling sering ditemui
RenderFlex overflowed
A RenderFlex overflowed by 87 pixels on the bottom.Isi widget lebih besar dari ruang yang tersedia. Kamu juga akan melihat garis kuning-hitam di layar.
Solusinya bergantung pada konteks:
dart
// Bisa digulir?
SingleChildScrollView(child: Column(children: [...]))
// Berbagi ruang di dalam Row/Column?
Expanded(child: Text('teks panjang'))
// Teks terlalu panjang?
Text('...', maxLines: 2, overflow: TextOverflow.ellipsis)
// Item terlalu banyak dalam satu baris?
Wrap(children: [...])Unbounded height / width
Vertical viewport was given unbounded height.Biasanya terjadi ketika ListView ditaruh langsung di dalam Column.
dart
// ❌
Column(children: [Text('Judul'), ListView.builder(...)])
// ✅
Column(children: [
const Text('Judul'),
Expanded(child: ListView.builder(...)),
])Null check operator used on a null value
Null check operator used on a null valueKamu memakai ! pada sesuatu yang ternyata null. Cari tanda seru di baris yang ditunjukkan dan ganti dengan pemeriksaan yang benar:
dart
// ❌
Text(pengguna!.nama)
// ✅
if (pengguna != null) Text(pengguna.nama)
// ✅
Text(pengguna?.nama ?? 'Tanpa Nama')setState() called after dispose()
setState() called after dispose(): _HalamanState#a1b2cSebuah operasi asinkron selesai setelah widget dilepas dari pohon. Solusinya sudah kamu kenal dari Bab 5:
dart
Future<void> _muat() async {
final hasil = await api.ambil();
if (!mounted) return; // ← tambahkan ini
setState(() => _data = hasil);
}No Material widget found
No Material widget found.
InkWell widgets require a Material widget ancestor.Widget Material — InkWell, TextField, ListTile — butuh permukaan Material di atasnya. Biasanya Scaffold sudah menyediakannya; kalau kamu memakai widget itu di luar Scaffold, bungkus dengan Material:
dart
Material(
color: Colors.transparent,
child: InkWell(onTap: () {}, child: const Text('Ketuk')),
)Incorrect use of ParentDataWidget
Incorrect use of ParentDataWidget.
Expanded widgets must be placed inside Flex widgets.Expanded, Flexible, dan Positioned hanya boleh menjadi anak langsung dari widget yang tepat — Row/Column untuk dua yang pertama, Stack untuk yang terakhir. Sering terjadi ketika kamu menyisipkan Padding di antaranya:
dart
// ❌ Expanded bukan anak langsung Row
Row(children: [
Padding(padding: EdgeInsets.all(8), child: Expanded(child: Text('a'))),
])
// ✅ Balik urutannya
Row(children: [
Expanded(child: Padding(padding: EdgeInsets.all(8), child: Text('a'))),
])Null safety dalam praktik
Sebagian besar error Tipe 1 yang kamu temui di awal berkaitan dengan null safety. Ini pola-pola perbaikannya.
dart
// Nilai mungkin null
String? nama;
// 1. Nilai cadangan — paling sering dipakai
Text(nama ?? 'Tanpa Nama')
// 2. Akses aman
Text(nama?.toUpperCase() ?? '-')
// 3. Pemeriksaan eksplisit
if (nama != null) {
Text(nama.toUpperCase()) // Dart tahu nama tidak null di sini
}
// 4. Tampilkan hanya kalau ada
if (nama != null) Text(nama!),
// 5. late — "akan diisi sebelum dipakai"
late final TextEditingController _pengendali;Promosi tipe tidak bekerja pada properti kelas
Ini jebakan yang membingungkan. Pemeriksaan if (x != null) mempromosikan tipe untuk variabel lokal, tetapi tidak untuk properti kelas — karena properti bisa saja diubah oleh kode lain di antara pemeriksaan dan penggunaan.
dart
class _HalamanState extends State<Halaman> {
String? _pesan;
@override
Widget build(BuildContext context) {
// ❌ Error: _pesan bisa null
if (_pesan != null) {
return Text(_pesan);
}
// ✅ Salin ke variabel lokal dulu
final pesan = _pesan;
if (pesan != null) {
return Text(pesan);
}
return const SizedBox.shrink();
}
}Tipe 2: ketika tidak ada error
Ini yang lebih menantang. Tidak ada yang menunjuk ke mana pun, jadi kamu harus mempersempit sendiri.
Mulai dengan print
Cara paling sederhana, dan sering paling cepat:
dart
void _tambahItem(Item item) {
print('=== _tambahItem dipanggil ===');
print('Item: ${item.judul}, jumlah: ${item.jumlah}');
print('Panjang daftar sebelum: ${_daftar.length}');
setState(() {
_daftar.add(item);
});
print('Panjang daftar sesudah: ${_daftar.length}');
}Untuk objek yang kompleks, debugPrint lebih baik karena tidak memotong keluaran yang panjang:
dart
import 'package:flutter/foundation.dart';
debugPrint('Data respons: $data');Dan log dari dart:developer memberi keluaran terstruktur yang lebih rapi di DevTools:
dart
import 'dart:developer' as developer;
developer.log(
'Gagal memuat data',
name: 'api.berita',
error: kesalahan,
stackTrace: jejak,
);Bersihkan sebelum rilis
print yang tertinggal di kode produksi memperlambat aplikasi dan bisa membocorkan informasi. Bungkus dengan pemeriksaan mode debug:
dart
if (kDebugMode) {
print('Hanya tampil saat pengembangan');
}Linter Flutter akan memperingatkanmu tentang print yang tidak dibungkus.
Memakai debugger sungguhan
print cepat, tetapi debugger jauh lebih kuat karena kamu bisa memeriksa semua variabel pada satu titik, bukan hanya yang sempat kamu cetak.
Memasang breakpoint. Di VS Code, klik di sebelah kiri nomor baris. Titik merah muncul. Jalankan aplikasi dengan Run → Start Debugging (F5), bukan flutter run biasa.
Ketika eksekusi mencapai baris itu, aplikasi berhenti dan kamu bisa:
- Melihat Variables — seluruh variabel lokal dan propertinya, terbuka untuk ditelusuri.
- Melihat Call Stack — dari mana fungsi ini dipanggil, dan dari mana pemanggilnya dipanggil.
- Memakai Watch — pantau ekspresi tertentu, misalnya
_daftar.length. - Memakai Debug Console — jalankan ekspresi Dart apa pun pada konteks saat itu.
Tombol kendali:
| Tombol | Nama | Fungsi |
|---|---|---|
| ▶ | Continue | Lanjut sampai breakpoint berikutnya |
| ⤵ | Step Over | Jalankan baris ini, jangan masuk ke dalam fungsi |
| ⤓ | Step Into | Masuk ke dalam fungsi yang dipanggil |
| ⤴ | Step Out | Selesaikan fungsi ini, kembali ke pemanggil |
| ⟲ | Restart | Mulai ulang sesi debug |
| ■ | Stop | Hentikan |
Breakpoint bersyarat sangat berguna untuk daftar panjang. Klik kanan pada breakpoint, pilih "Edit Breakpoint", lalu masukkan syarat:
dart
item.id == 'abc-123'
indeks > 50
_daftar.isEmptyEksekusi hanya berhenti ketika syarat itu terpenuhi — jauh lebih efisien daripada menekan Continue lima puluh kali.
Mempersempit secara sistematis
Ketika kamu benar-benar tidak tahu di mana masalahnya, jangan menebak. Persempit secara terstruktur:
Pastikan gejalanya bisa diulang. Kalau kamu tidak bisa memicu bugnya sesuka hati, kamu tidak akan tahu kapan sudah diperbaiki.
Tentukan titik terakhir yang pasti benar. Taruh
printdi sana dan pastikan ia tercetak dengan nilai yang benar.Tentukan titik pertama yang pasti salah. Taruh
printdi sana juga.Bagi dua wilayah di antaranya. Taruh
printdi tengah. Salah satu separuhnya mengandung bugnya.Ulangi sampai wilayahnya tinggal beberapa baris.
Metode ini terasa lambat, tetapi hampir selalu lebih cepat daripada menatap kode dan berharap menemukan kesalahannya.
Flutter DevTools
DevTools adalah rangkaian perkakas visual yang jauh melampaui print.
Menjalankannya:
bash
flutter run
# Tekan 'v' di terminal, atau salin URL yang ditampilkan ke perambanDi VS Code: buka Command Palette, ketik "Flutter: Open DevTools".
Flutter Inspector
Tab yang paling sering kamu pakai. Ia menampilkan widget tree secara langsung.
Yang bisa kamu lakukan di sini:
Select Widget Mode. Aktifkan, lalu ketuk apa pun di aplikasi. Inspector langsung menyorot widget itu di pohon dan menampilkan seluruh propertinya. Sangat berguna ketika kamu tidak tahu widget mana yang menyebabkan warna atau jarak tertentu.
Show Guidelines. Menggambar garis di sekeliling setiap widget beserta ukurannya. Ini cara tercepat memahami kenapa ada jarak yang tidak kamu inginkan.
Highlight Repaints. Setiap widget yang digambar ulang dikelilingi garis berwarna yang berganti warna tiap frame. Kalau seluruh layar berkedip saat kamu mengetik di satu kolom, itu pertanda rebuild yang terlalu luas — kembali ke teknik pemecahan widget di Bab 12.
Show Baselines dan Highlight Oversized Images membantu masalah tipografi dan gambar yang jauh lebih besar dari ruang tampilnya.
Performance
Tab ini menampilkan grafik waktu pembangunan tiap frame.
Target: 16 ms per frame (60 FPS)
8 ms per frame (120 FPS)
┌────────────────────────────────────────────┐
│ ▁▁▁▂▁▁▁█████▁▁▁▂▁▁▁ │ ← lonjakan = tersendat
│ ─────────────────────────── 16 ms ──────── │
└────────────────────────────────────────────┘Batang yang melewati garis 16 ms berarti frame itu terlambat, dan pengguna merasakannya sebagai tersendat. Mengklik batang itu menunjukkan fungsi mana yang memakan waktu.
Penyebab tersendat yang paling umum:
build()yang terlalu berat — pecah widget, pakaiconst.- Membangun daftar tanpa
ListView.builder. - Gambar berukuran raksasa yang diperkecil saat ditampilkan.
- Perhitungan berat di thread utama — pindahkan ke
compute()atau isolate.
Memory
Menampilkan penggunaan memori dari waktu ke waktu. Kalau grafiknya terus naik dan tidak pernah turun meskipun kamu berpindah halaman, kemungkinan besar ada yang tidak di-dispose(). Periksa AnimationController, TextEditingController, StreamSubscription, dan Timer.
Network
Menampilkan setiap permintaan HTTP: URL, metode, kode status, waktu respons, dan isi body-nya. Ini menghilangkan kebutuhan menaburkan print di sekitar kode jaringan. Sangat berguna untuk Bab 14.
Widget yang membantu debugging
Beberapa alat bantu yang bisa kamu selipkan sementara:
dart
// Menampilkan seluruh pohon widget ke konsol
debugDumpApp();
// Menampilkan render tree beserta ukuran & posisinya
debugDumpRenderTree();
// Menggambar garis batas di sekeliling semua widget
import 'package:flutter/rendering.dart';
void main() {
debugPaintSizeEnabled = true; // hapus setelah selesai
runApp(const AplikasiSaya());
}
// Memeriksa asumsi — otomatis dihapus di build rilis
assert(_daftar.isNotEmpty, 'Daftar seharusnya sudah terisi di sini');assert sangat berguna dan sering diabaikan. Ia hanya berjalan dalam mode debug, jadi tidak membebani aplikasi rilis, tetapi ia menangkap asumsi yang salah lebih awal — jauh sebelum gejalanya muncul di tempat lain.
Mode debug, profile, dan release
Flutter punya tiga mode build, dan perilakunya berbeda cukup jauh.
| Mode | Perintah | Ciri |
|---|---|---|
| Debug | flutter run | Hot reload aktif, assert aktif, DevTools penuh. Lambat — jangan menilai performa dari mode ini. |
| Profile | flutter run --profile | Optimasi seperti rilis, tetapi perkakas performa tetap aktif. Pakai ini untuk mengukur kecepatan. |
| Release | flutter run --release | Optimasi penuh, tanpa perkakas debug. Ini yang dikirim ke pengguna. |
Jangan mengukur performa di mode debug
Ini kesalahan yang sering membuat orang panik tanpa perlu. Mode debug menjalankan banyak pemeriksaan tambahan dan tidak dikompilasi AOT, sehingga animasi bisa terasa tersendat padahal di mode rilis sangat mulus. Selalu ukur di mode profile atau release.
Penanganan error yang baik
Debugging bukan hanya soal memperbaiki setelah rusak; ia juga soal membuat kegagalan lebih mudah dipahami sejak awal.
dart
Future<List<Berita>> ambilBerita() async {
try {
final respons = await http
.get(Uri.parse('$baseUrl/berita.json'))
.timeout(const Duration(seconds: 10));
if (respons.statusCode >= 400) {
throw Exception('Server menolak permintaan (${respons.statusCode})');
}
final data = json.decode(respons.body) as Map<String, dynamic>;
return data.entries.map((e) => Berita.dariJson(e.key, e.value)).toList();
} on SocketException {
throw Exception('Tidak ada koneksi internet.');
} on TimeoutException {
throw Exception('Server terlalu lama merespons.');
} on FormatException {
throw Exception('Format data dari server tidak dikenali.');
}
}Perhatikan bahwa setiap jenis kegagalan menghasilkan pesan yang bisa dimengerti pengguna, bukan jejak tumpukan mentah. Ini penting untuk dua hal: pengguna tahu apa yang harus dilakukan, dan kamu tahu di mana harus mencari ketika mereka melaporkannya.
Menangkap error yang tidak tertangani di seluruh aplikasi:
dart
void main() {
FlutterError.onError = (rincian) {
FlutterError.presentError(rincian);
// Di produksi: kirim ke Crashlytics atau Sentry
};
runApp(const AplikasiSaya());
}Dan mengganti layar merah error dengan sesuatu yang lebih ramah:
dart
void main() {
ErrorWidget.builder = (rincian) {
return Material(
child: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 48, color: Colors.red),
const SizedBox(height: 16),
const Text('Terjadi kesalahan pada tampilan ini.'),
if (kDebugMode) ...[
const SizedBox(height: 12),
Text(
rincian.exceptionAsString(),
style: const TextStyle(fontSize: 12),
textAlign: TextAlign.center,
),
],
],
),
),
),
);
};
runApp(const AplikasiSaya());
}Perhatikan if (kDebugMode) — rincian teknis hanya ditampilkan saat pengembangan, sedangkan pengguna hanya melihat pesan yang sopan.
Daftar periksa saat buntu
Ketika kamu sudah mencoba segalanya dan masih tidak berhasil, jalankan daftar ini berurutan:
- Sudah hot restart, bukan hanya hot reload? Perubahan pada
main(),initState(), dan aset butuhR. - Sudah
flutter cleanlaluflutter pub get? Kadang cache build yang rusak menyebabkan gejala yang tidak masuk akal. - Sudah baca pesan errornya sampai bagian "relevant error-causing widget"?
- Apakah asumsimu benar? Cetak nilainya. Sering kali variabel yang kamu yakin berisi sesuatu ternyata kosong.
- Sudah memeriksa
mountedsetelahawait? - Apakah kamu lupa
setState? Nilai berubah tetapi layar tidak. - Apakah kamu mengubah list/objek yang sama alih-alih membuat yang baru? Ini penyebab utama "state tidak update" di Riverpod dan BLoC.
- Apakah
keydiperlukan di sini? Lihat Bab 12. - Apakah
dispose()sudah lengkap? - Coba jelaskan masalahnya keras-keras. Setengah dari bug terpecahkan di tengah kalimat penjelasan.
Latihan Mandiri
Kerjakan salah satu, beberapa, atau semuanya secara berurutan untuk melatih pemahamanmu sampai benar-benar lekat.
Variasi 1: Empat Bug di Layar Suhu — ⭐⭐ · 40–60 menit
Tantangan: Kode di bawah ini berisi empat bug, masing-masing dari kategori berbeda yang dibahas di bab ini. Satu menyebabkan error saat dibuka, satu menyebabkan error setelah beberapa detik, satu tidak menghasilkan error sama sekali tetapi hasilnya salah, dan satu adalah kebocoran yang hanya terlihat lewat DevTools. Salin ke proyek kosong, jalankan, dan perbaiki keempatnya.
dart
class LayarSuhu extends StatefulWidget {
const LayarSuhu({super.key});
@override
State<LayarSuhu> createState() => _LayarSuhuState();
}
class _LayarSuhuState extends State<LayarSuhu> {
final List<double> _riwayat = [];
double? _suhuTerakhir;
@override
void initState() {
super.initState();
Timer.periodic(const Duration(seconds: 1), (_) async {
final suhu = await _ambilSuhu();
setState(() {
_suhuTerakhir = suhu;
_riwayat.add(suhu);
});
});
}
double get rataRata =>
_riwayat.fold(0.0, (a, b) => a + b) / _riwayat.length;
Future<double> _ambilSuhu() async {
await Future.delayed(const Duration(milliseconds: 800));
return 20 + Random().nextDouble() * 10;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Suhu')),
body: Column(
children: [
Text('Sekarang: ${_suhuTerakhir!.toStringAsFixed(1)}'),
Text('Rata-rata: ${rataRata.toStringAsFixed(1)}'),
ListView.builder(
itemCount: _riwayat.length,
itemBuilder: (_, i) => Text('${_riwayat[i]}'),
),
],
),
);
}
}Kriteria selesai:
- Keempat bug ditemukan, dan untuk masing-masing kamu bisa menyebut kategorinya — Tipe 1 atau Tipe 2, dan kenapa.
- Setidaknya satu bug ditemukan lewat Flutter Inspector atau tab Memory di DevTools, bukan dengan menaburkan
print. - Setelah diperbaiki, aplikasi berjalan lima menit tanpa error dan tanpa pemakaian memori yang terus naik.
- Kamu menaruh satu breakpoint bersyarat di suatu tempat dalam kode ini dan menjalankannya setidaknya sekali.
Petunjuk: Mulai dari yang paling berisik. Baris pertama pesan error dan bagian "relevant error-causing widget" akan langsung menunjuk dua bug pertama — satu berkaitan dengan nilai yang belum ada saat layar pertama kali dibangun, satu lagi berkaitan dengan widget bergulir yang diletakkan di tempat yang tingginya tidak terbatas. Bug ketiga tidak menghasilkan apa pun kecuali angka aneh, dan hanya muncul pada frame paling awal — tanyakan apa hasil pembagian ketika daftarnya masih kosong. Bug keempat tidak akan pernah kamu lihat di layar: perhatikan apa yang dibuat di initState dan cari padanannya di dispose, lalu buka layar ini sepuluh kali berturut-turut sambil mengawasi tab Memory.
Variasi 2: Daftar yang Tersendat — ⭐⭐⭐ · 45–60 menit
Tantangan: Kode di bawah ini tidak menghasilkan satu pun error, tetapi daftarnya terasa berat saat digulir dan makin berat seiring waktu. Temukan tiga penyebabnya memakai tab Performance dan Flutter Inspector, lalu perbaiki tanpa mengubah tampilan yang dihasilkan.
dart
class LayarKatalog extends StatefulWidget {
const LayarKatalog({super.key, required this.semuaItem});
final List<Item> semuaItem;
@override
State<LayarKatalog> createState() => _LayarKatalogState();
}
class _LayarKatalogState extends State<LayarKatalog> {
String _cari = '';
@override
Widget build(BuildContext context) {
final hasil = widget.semuaItem
.where((e) => e.nama.toLowerCase().contains(_cari.toLowerCase()))
.toList()
..sort((a, b) => a.nama.compareTo(b.nama));
return Column(
children: [
TextField(onChanged: (v) => setState(() => _cari = v)),
Expanded(
child: SingleChildScrollView(
child: Column(
children: hasil.map((item) {
return Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(blurRadius: 12, color: Colors.black26),
],
),
child: ListTile(
title: Text(item.nama),
subtitle: Text(hitungRingkasan(item)),
),
);
}).toList(),
),
),
),
],
);
}
}Kriteria selesai:
- Ketiga penyebab ditemukan dan bisa kamu jelaskan: satu soal cara daftar dibangun, satu soal pekerjaan berat yang diulang setiap frame, dan satu soal biaya menggambar.
- Perbaikan diukur di mode profile, bukan mode debug, dan kamu bisa menyebutkan angka sebelum dan sesudahnya.
- Tampilan akhirnya identik dengan sebelumnya.
- Kamu memakai tab Performance setidaknya sekali untuk menemukan frame yang melewati batas waktu.
Petunjuk: Pertanyaan pertama untuk daftar apa pun yang terasa berat adalah berapa banyak anak yang benar-benar dibangun — dan Column membangun semuanya, termasuk yang berada jauh di luar layar. Pertanyaan kedua adalah apa yang dikerjakan ulang di setiap build; menyaring dan mengurutkan ribuan item di sana berarti mengerjakannya di setiap ketukan tombol. Pertanyaan ketiga lebih halus: bayangan dengan radius kabur besar mahal untuk digambar, dan mahalnya dikalikan jumlah baris yang terlihat.
Variasi 3: Investigasi Tanpa Petunjuk — ⭐⭐⭐⭐ · 60–90 menit
Tantangan: Ambil aplikasi apa pun yang sudah kamu tulis dan rusakkan dengan sengaja — minta orang lain mengubah satu baris tanpa memberitahumu yang mana, atau ubah sendiri satu baris lalu tinggalkan selama sehari. Lalu temukan kembali penyebabnya memakai metode membagi dua, bukan menebak.
Kriteria selesai:
- Kamu menuliskan hipotesis sebelum memeriksa, lalu mencatat apakah ia terbukti — minimal untuk tiga hipotesis berturut-turut.
- Pencarian dilakukan dengan membagi wilayah kemungkinan menjadi dua di setiap langkah, bukan memeriksa satu per satu dari atas.
- Kamu memakai
git bisectatau riwayat commit setidaknya sekali untuk mempersempit wilayah. - Setelah ditemukan, kamu menambahkan satu
assertyang akan menangkap kesalahan serupa lebih awal di masa depan.
Petunjuk: Metode membagi dua terasa lambat dibandingkan menebak, dan justru itu ilusinya: menebak berhasil dengan cepat ketika kamu beruntung, dan membuang berjam-jam ketika tidak. Membagi dua selalu selesai dalam jumlah langkah yang bisa diperkirakan. Kunci penerapannya adalah menonaktifkan setengah kemungkinan sekaligus — bukan setengah baris kode, melainkan setengah wilayah masalah: apakah datanya sudah salah sebelum sampai ke UI, atau baru salah setelahnya? Satu debugPrint di perbatasan menjawab itu dan langsung membuang separuh pekerjaan.
Ikhtisar
- Bedakan Tipe 1 (ada pesan error) dari Tipe 2 (tidak ada error, hasil salah). Keduanya butuh pendekatan berbeda.
- Pada pesan error Flutter, baca baris pertama (inti masalah) dan "relevant error-causing widget" (berkas dan nomor baris). Sisanya biasanya bisa diabaikan.
- Error paling umum: overflow (butuh
Expanded/SingleChildScrollView), unbounded height (butuhExpanded), null check pada nilai null,setStatesetelahdispose(butuhmounted), danExpandedyang bukan anak langsungRow/Column. - Promosi tipe null tidak bekerja pada properti kelas — salin ke variabel lokal dulu.
printcepat,debugPrintuntuk keluaran panjang,developer.loguntuk keluaran terstruktur. Bungkus dengankDebugModeagar tidak terbawa ke rilis.- Debugger lebih kuat daripada print karena kamu bisa memeriksa semua variabel sekaligus. Breakpoint bersyarat menghemat banyak waktu di daftar panjang.
- Ketika buntu, persempit dengan membagi dua — jangan menebak.
- Flutter Inspector untuk struktur dan layout, Performance untuk frame yang tersendat, Memory untuk kebocoran, Network untuk permintaan HTTP.
- Jangan pernah mengukur performa di mode debug. Pakai
--profile. - Tangkap kegagalan spesifik (
SocketException,TimeoutException,FormatException) dan ubah menjadi pesan yang bisa dimengerti pengguna. assertmenangkap asumsi yang salah lebih awal, dan gratis karena dihapus di build rilis.
Bagian I selesai. Kamu sekarang punya fondasi yang cukup untuk membangun antarmuka apa pun. Berikutnya: Bab 14 — Networking & REST API, tempat aplikasimu mulai bicara dengan dunia luar.
Transkrip asli
Disintesis dari 1_flutter_core/3_debugging.md (5 video: Understanding Error Messages, Debug Mode, Flutter DevTools) dan 1_flutter_zero-to-hero/4_fixing-bugs-2-step-process.md (Type 1, Type 2, Null Safety). Lihat PDF Flutter Core dan PDF Zero to Hero.