ListView.builder
Daftar scroll yang efisien untuk data banyak dengan itemBuilder.
Analogi: Warteg vs Koki Pribadi
Bayangkan warteg yang memasak 1000 porsi di pagi hari "untuk jaga-jaga", padahal yang datang cuma 50 orang. Sisanya mubazir, kompor kepanasan, dapur penuh sesak. Itulah ListView(children: [...]): semua item dibangun sekaligus, berapa pun jumlahnya. ListView.builder seperti koki pribadi: ia hanya memasak porsi yang dipesan, yaitu item yang sedang terlihat di layar. Sisanya dibuat saat kamu scroll ke sana. Teknik ini namanya lazy building.
Kenapa builder Ada?
HP punya memori terbatas. Membangun 1000 baris daftar sekaligus berarti 1000 widget duduk di memori, dan aplikasi mulai melambat bahkan sebelum pengguna sempat scroll. Untuk 5 atau 10 item, ListView(children: [...]) baik-baik saja. Tapi data nyata, daftar produk, isi chat, riwayat transaksi, jumlahnya tidak tentu dan bisa mencapai ribuan. ListView.builder menyelesaikan masalah ini dengan hanya membangun item yang terlihat plus sedikit cadangan, lalu mendaur ulang slotnya saat kamu scroll. Hasilnya: daftar 10.000 item terasa selancar daftar 10 item.
Singkatnya: children untuk daftar pendek yang jumlahnya pasti, builder untuk daftar yang datanya dinamis atau banyak.
ListView.builder Dasar
Dua properti wajib: itemCount (berapa total item) dan itemBuilder (fungsi yang membangun tampilan untuk tiap indeks).
final menu = ['Kopi Hitam', 'Kopi Susu', 'Matcha', 'Teh Manis'];
ListView.builder(
itemCount: menu.length,
itemBuilder: (context, index) {
return ListTile(
leading: const Icon(Icons.coffee),
title: Text(menu[index]),
);
},
)itemBuilder dipanggil dengan indeks 0, 1, 2, dan seterusnya, hanya untuk baris yang perlu tampil di layar. Kamu tidak pernah menulis 4 ListTile manual, apalagi 400. Datanya yang menentukan jumlahnya, bukan kodemu.
Contoh Realistis: Daftar Produk
Di aplikasi nyata, datanya berupa objek, bukan sekadar string. Lihat daftar produk toko ini: tiap baris menampilkan ikon, nama, harga, dan rating, semuanya dibangun dari satu list data.
class Produk {
final String nama;
final int harga;
final double rating;
const Produk(this.nama, this.harga, this.rating);
}
final produk = const [
Produk('Kopi Susu Gula Aren', 18000, 4.8),
Produk('Matcha Latte', 22000, 4.9),
Produk('Teh Manis Dingin', 8000, 4.5),
];
ListView.separated(
itemCount: produk.length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
final p = produk[index];
return ListTile(
leading: const CircleAvatar(child: Icon(Icons.coffee)),
title: Text(p.nama, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text('Rp${p.harga}'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.star, color: Colors.amber, size: 16),
Text(' ${p.rating}'),
],
),
onTap: () {
// buka halaman detail produk
},
);
},
)Pola ini, ambil produk[index] lalu bangun satu baris, adalah pola yang akan kamu tulis ratusan kali dalam karier Flutter-mu. ListView.separated adalah bonus praktis: sama seperti builder, tapi dengan garis pemisah otomatis di antara baris lewat separatorBuilder.
Catatan teknis:
itemBuilderbisa dipanggil berulang kali untuk indeks yang sama, misalnya saat layar di-refresh atau kamu scroll bolak-balik. Jangan taruh operasi berat di dalamnya seperti panggilan jaringan atau komputasi besar. Siapkan datanya terlebih dahulu, builder hanya bertugas menampilkan.
itemCount Itu Wajib
Tanpa itemCount, ListView menganggap daftarmu tak berujung dan membiarkan pengguna scroll selamanya, memanggil builder untuk indeks 1000, 100000, dan seterusnya. Selalu isi itemCount, kecuali kamu memang sengaja membuat daftar tak terbatas seperti feed yang dimuat bertahap, dan itu pun butuh logika pemuatan khusus.
ListView di Dalam Column
Ini jebakan paling terkenal di Flutter. ListView ingin setinggi mungkin karena ia bisa scroll vertikal tanpa batas, tapi Column tidak memberinya batas tinggi. Hasilnya: error "unbounded height" yang membingungkan.
// Pola yang benar: beri batas tinggi lewat Expanded
Column(
children: [
const Padding(
padding: EdgeInsets.all(16),
child: Text('Daftar Menu', style: TextStyle(fontSize: 20)),
),
Expanded(
child: ListView.builder(
itemCount: menu.length,
itemBuilder: (context, index) {
return ListTile(title: Text(menu[index]));
},
),
),
],
)Alternatifnya adalah shrinkWrap: true, yang membuat ListView hanya setinggi total itemnya. Ini praktis untuk list pendek di dalam halaman yang sudah bisa scroll, tapi lebih lambat karena menonaktifkan sebagian optimasi lazy. Aturan praktisnya: list panjang selalu pakai Expanded, list pendek di dalam scroll lain boleh pakai shrinkWrap.
Kesalahan Umum Pemula
1. Lupa mengisi itemCount
Salah: builder tanpa itemCount membuat daftar tak berujung yang scroll selamanya.
// SALAH: scroll tidak pernah berhenti
ListView.builder(
itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
)Benar: selalu pasang itemCount sesuai panjang datamu.
// BENAR
ListView.builder(
itemCount: menu.length,
itemBuilder: (context, index) => ListTile(title: Text(menu[index])),
)2. ListView langsung di dalam Column tanpa Expanded
Salah: error "unbounded height" yang bikin bingung.
// SALAH: Column tidak memberi batas tinggi
Column(
children: [
ListView.builder(
itemCount: 10,
itemBuilder: (context, i) => Text('$i'),
),
],
)Benar: bungkus ListView dengan Expanded supaya dapat jatah tinggi yang jelas.
// BENAR
Column(
children: [
Expanded(
child: ListView.builder(
itemCount: 10,
itemBuilder: (context, i) => Text('$i'),
),
),
],
)3. Menaruh logika berat di dalam itemBuilder
Salah: memanggil API atau mengolah data berat setiap kali baris dibangun. Karena builder dipanggil ulang berkali-kali, operasinya ikut berulang dan scroll jadi patah-patah.
// SALAH: operasi berat dipanggil berulang-ulang
itemBuilder: (context, index) {
final data = ambilDariInternet(index); // jangan lakukan ini!
return Text(data);
}Benar: ambil dan siapkan semua data sebelum ListView dibangun, builder hanya membaca data yang sudah siap.
// BENAR: data sudah siap, builder tinggal menampilkan
final semuaData = siapkanDataSekali();
ListView.builder(
itemCount: semuaData.length,
itemBuilder: (context, index) => Text(semuaData[index]),
)Tantangan
Daftar 50 angka
Buat ListView.builder dengan itemCount 50 yang menampilkan ListTile 'Item ke-1' sampai 'Item ke-50' (gunakan index + 1).