List panjang termasuk tampilan data yang paling sering ditemui pada aplikasi, mulai dari jadwal piket hingga ratusan catatan pribadi. Topik ini membahas cara menampilkan list yang dapat digulir dengan ListView, perbedaannya dengan ListView.builder yang membangun item sesuai permintaan layar, serta disiplin performa yang menyertainya. Setelah membacanya, pemilihan antara ListView biasa, ListView.builder, dan ListView.separated dapat dilakukan dengan alasan yang tepat.
ListView biasa dan beban pembangunannya#
Widget ListView menyusun daftar yang dapat digulir dari properti children. Seluruh anak di dalam children dibangun pada saat yang sama, termasuk anak yang posisinya jauh di bawah dan belum terlihat pada layar. Cara kerja ini disebut eager, yakni semua item dikerjakan seketika tanpa menunggu sampai benar-benar dibutuhkan. Untuk daftar pendek, perilaku tersebut tidak terasa, tetapi bebannya tumbuh seiring jumlah item.
// tidak-dijalankan
ListView(
children: [
ListTile(title: Text('Senin, 3 November: Ali')),
ListTile(title: Text('Selasa, 4 November: Budi')),
ListTile(title: Text('Rabu, 5 November: Citra')),
],
)Tiga ListTile pada contoh dibangun sekaligus dan bebannya masih wajar. Persoalan muncul ketika jumlah anak mencapai ribuan, misalnya arsip catatan bertahun-tahun. Pada pembukaan layar, semua ListTile itu tetap dibangun meskipun hanya belasan yang tampak, sehingga waktu build pertama memanjang dan memori terpakai untuk widget yang tidak pernah dilihat. Kondisi inilah yang diperbaiki oleh ListView.builder.
ListView di dalam body milik Scaffold sudah dapat digulir sejak awal, sehingga tidak diperlukan pembungkus penggulir di sekelilingnya. Arah gulirannya dapat diubah melalui scrollDirection, sedangkan properti reverse membalik urutannya untuk kebutuhan tampilan tertentu. Perilaku membangun sekaligus ini berlaku untuk semua varian yang menerima children secara langsung, bukan hanya ListView dengan arah vertikal.
Konsep lazy dalam program Dart murni#
Lazy berarti menunda pekerjaan sampai hasilnya benar-benar dibutuhkan. Konsep ini dapat dibuktikan tanpa perangkat dan tanpa Flutter, cukup dengan Dart murni yang dijalankan di terminal. Fungsi bangunItem pada program berikut berperan seperti itemBuilder, yaitu setiap pemanggilannya mewakili pembangunan satu item, dan pemanggilan itu dicetak agar terlihat kapan item benar-benar dibangun. Simpan program sebagai konsep-lazy.dart, lalu jalankan dengan perintah dart konsep-lazy.dart.
// Menyimulasi pembangunan sekaligus (eager) versus lazy dengan Dart murni.
// Fungsi pembangun item, meniru peran itemBuilder pada ListView.builder.
String bangunItem(int indeks) {
print('membangun item $indeks');
return 'Item $indeks';
}
void main() {
// List.generate membangun SEMUA item pada saat list dibuat.
final semua = List.generate(5, bangunItem);
print('eager: $semua');
// Iterable.generate hanya menyimpan resep, belum membangun apa pun.
final resep = Iterable.generate(5, bangunItem);
print('lazy dibuat, belum ada satu pun item dibangun');
// Item baru dibangun hanya ketika benar-benar dibutuhkan.
final duaPertama = resep.take(2).toList();
print('dibutuhkan: $duaPertama');
}membangun item 0
membangun item 1
membangun item 2
membangun item 3
membangun item 4
eager: [Item 0, Item 1, Item 2, Item 3, Item 4]
lazy dibuat, belum ada satu pun item dibangun
membangun item 0
membangun item 1
dibutuhkan: [Item 0, Item 1]Urutan keluarannya menyatakan perbedaan kedua cara. List.generate memanggil bangunItem lima kali saat list dibuat, sehingga lima baris membangun item tercetak lebih dahulu. Sebaliknya, Iterable.generate hanya menyimpan resep pembangunannya, dan tidak ada satu pun item dibangun ketika baris lazy dibuat tercetak. Item 0 dan 1 baru dibangun ketika take(2).toList() memintanya. ListView.builder menerapkan disiplin yang sama, yaitu itemBuilder menjadi resepnya dan Flutter hanya memanggilnya untuk item yang tampil di layar beserta sedikit cadangan di luarnya.
Anatomi ListView.builder#
ListView.builder menggantikan children dengan dua properti. Properti itemCount menyatakan jumlah item yang tersedia, sedangkan properti itemBuilder adalah fungsi yang menghasilkan widget untuk satu indeks. Fungsi itemBuilder menerima BuildContext dan angka index, lalu mengembalikan widget untuk baris pada indeks itu. Flutter memanggil itemBuilder hanya apabila baris pada indeks tersebut mendekati layar, sehingga daftar sepuluh ribu item tetap dibangun beberapa baris pada satu waktu.
// tidak-dijalankan
final jadwalPiket = ['Ali', 'Budi', 'Citra', 'Dewi', 'Eka'];
ListView.builder(
itemCount: jadwalPiket.length,
itemBuilder: (context, index) {
return ListTile(
leading: const Icon(Icons.event_available),
title: Text('Piket ${index + 1}: ${jadwalPiket[index]}'),
);
},
)Nilai itemCount wajib sesuai jumlah data yang benar-benar tersedia. itemBuilder dapat dipanggil untuk indeks hingga itemCount - 1, sehingga nilai itemCount yang lebih besar dari panjang data memicu RangeError saat pengguna menggulir ke bagian bawah. Flutter juga membangun beberapa item di luar area pandang sebelum benar-benar terlihat, yang dikenal sebagai cache extent, agar pengguliran tidak menunggu pembangunan item.
Parameter itemBuilder berbentuk sama dengan builder yang digunakan MaterialPageRoute, yaitu fungsi yang menerima BuildContext dan mengembalikan widget. Pola seperti ini muncul berulang di Flutter, misalnya GridView.builder untuk kisi dua dimensi, sehingga kebiasaan membaca itemCount dan itemBuilder langsung terbawa ke widget lain yang serupa.
ListView.separated#
ListView.separated adalah varian ListView.builder yang menyisipkan satu widget pemisah di antara setiap pasangan item yang berurutan. Konstruktor ini memerlukan tiga properti utama, yaitu itemCount, itemBuilder, dan separatorBuilder. separatorBuilder dipanggil untuk menghasilkan pemisah pada celah antaritem, sehingga garis tipis atau jarak antarkartu tidak perlu diselipkan manual di dalam item.
// tidak-dijalankan
ListView.separated(
itemCount: jadwalPiket.length,
itemBuilder: (context, index) => ListTile(
title: Text(jadwalPiket[index]),
),
separatorBuilder: (context, index) => const Divider(height: 1),
)Sifat lazy ListView.separated sama dengan ListView.builder, karena keduanya membangun isi list sesuai permintaan layar. Perbedaannya hanya pada jumlah widget yang dihasilkan, yaitu item dan pemisah dibangun bergantian. Apabila desain memerlukan jarak antarkartu, pemisah dapat berupa SizedBox dengan tinggi tertentu, sehingga pemisahan tetap menjadi urusan daftar dan bukan urusan tiap item.
Jumlah pemisah yang dihasilkan adalah itemCount - 1, karena tidak ada pemisah setelah item terakhir. separatorBuilder juga dipanggil secara lazy, sehingga pemisah hanya dibuat ketika pasangan item yang diapitnya mendekati layar. Apabila daftarnya kosong, tidak ada satu pun pemisah yang dibangun.
Aplikasi contoh utuh#
Contoh berikut menyusun aplikasi jadwal piket lengkap dalam satu berkas main.dart. Layar tunggalnya menampilkan sepuluh nama melalui ListView.builder, lengkap dengan nomor urut pada setiap barisnya. Salin berkas ini ke proyek Flutter, lalu jalankan pada emulator untuk melihat hasilnya.
// tidak-dijalankan
import 'package:flutter/material.dart';
void main() {
runApp(const AplikasiPiket());
}
class AplikasiPiket extends StatelessWidget {
const AplikasiPiket({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Jadwal Piket',
theme: ThemeData(colorSchemeSeed: Colors.teal),
home: Scaffold(
appBar: AppBar(title: const Text('Jadwal Piket')),
body: const DaftarPiket(),
),
);
}
}
class DaftarPiket extends StatelessWidget {
const DaftarPiket({super.key});
static const namaPiket = [
'Ali', 'Budi', 'Citra', 'Dewi', 'Eka',
'Fajar', 'Gita', 'Hana', 'Indra', 'Joko',
];
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: namaPiket.length,
itemBuilder: (context, index) {
final nama = namaPiket[index];
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(nama),
subtitle: Text('Piket hari ke-${index + 1}'),
);
},
);
}
}Struktur widgetnya berlapis tiga, yaitu MaterialApp sebagai akar, Scaffold yang menyediakan kerangka layar beserta AppBar-nya, dan DaftarPiket yang menjadi isi body. Setiap baris memperlihatkan CircleAvatar berisi nomor urut sebagai leading, nama sebagai title, dan keterangan hari sebagai subtitle. Daftar namaPiket dinyatakan static const pada kelasnya, sehingga datanya berada di satu tempat dan tidak digandakan pada setiap build.
Kode di atas memperlihatkan dua kebiasaan yang patut dibiasakan. Daftar data dinyatakan const, sehingga tidak dibuat ulang setiap kali build berjalan. itemBuilder hanya menyusun satu ListTile dari satu nama tanpa pekerjaan lain, sehingga pembangunan tiap item tetap ringan.
Disiplin performa list panjang#
Sifat lazy ListView.builder bukan jaminan performa, melainkan fondasinya. Performa guliran ditentukan oleh biaya setiap pemanggilan itemBuilder, karena pemanggilan itu terjadi persis saat pengguna menggulir. Prinsip yang menyertainya ada tiga.
Pertama, jaga item tetap ringan. Satu ListTile sederhana jauh lebih murah daripada pohon widget bersarang yang panjang, dan pemformatan yang kompleks sebaiknya dikerjakan sekali di luar itemBuilder, misalnya saat data dimuat. Kedua, jaga itemBuilder tetap murni. Fungsi yang hanya membaca data dan menyusun widget tidak memicu efek samping, sebagaimana kontrak metode build. Ketiga, gunakan const sedapat mungkin. Widget yang dinyatakan const dibangun sekali dan tidak ikut dibangun ulang ketika widget induknya membangun ulang dirinya, sehingga baris yang tidak berubah tidak dikerjakan ulang.
Masalah performa pada list umumnya tampil sebagai guliran yang tersendat, yaitu satu frame tidak selesai digambar dalam waktunya. Penyebabnya paling sering bukan jumlah item, melainkan biaya tiap item yang dibangun saat menggulir. Karena itemBuilder mengerjakan satu item per panggilan, memperingan satu item saja sudah memperingan seluruh daftar.
Memilih konstruktor yang tepat#
| Konstruktor | Cara membangun anak | Cocok untuk |
|---|---|---|
ListView | seluruh children sekaligus | list pendek yang seluruh itemnya sudah diketahui |
ListView.builder | lazy melalui itemCount dan itemBuilder | list panjang atau data yang jumlahnya tidak pasti |
ListView.separated | lazy, ditambah separatorBuilder | list builder yang perlu pemisah antaritem |
Aturan pemilihannya mengikuti ukuran dan sumber data. Daftar menu pendek atau kelompok baris yang jumlahnya tetap sudah cukup ditangani oleh ListView biasa, karena keterbacaan kodenya paling lugas. Daftar yang datanya berasal dari API, basis data, atau masukan pengguna yang terus bertumbuh sebaiknya langsung menggunakan ListView.builder. Ambang antara list pendek dan list panjang bukan angka yang pasti, tetapi dapat dibaca dari gejalanya, yaitu pembukaan layar yang terasa berat meskipun isinya belum tampak. Varian separated dipilih sejak awal ketika desain memang membutuhkan pemisah.
Kesalahan umum#
- Menaruh
ListViewdi dalamColumntanpa ruang yang pasti.Columnmemberi tinggi tak terbatas pada anaknya, sehingga list di dalamnya gagal menyusun tata letak dan memunculkan error. Perbaikannya, bungkus list denganExpandedatauFlexible, atau beri tinggi tertentu pada induknya. - Menuliskan
itemCountyang tidak cocok dengan data. Angka yang lebih besar dari panjang data membuatitemBuilderdipanggil dengan indeks yang tidak tersedia, danRangeErrormuncul saat pengguna menggulir. Perbaikannya, tulisdaftar.lengthsebagaiitemCount. - Melakukan pekerjaan berat di dalam
itemBuilder. Penguraian data atau pembuatan objek besar di dalamnya terjadi berulang setiap kali item dibangun, sehingga guliran tersendat. Perbaikannya, pindahkan pekerjaan itu ke luaritemBuilderdan simpan hasilnya. - Membungkus
ListViewdenganSingleChildScrollView. Pembungkus itu memaksa seluruh isi dibangun sekaligus, sehingga sifat lazy-nya hilang tanpa pesan error apa pun. Perbaikannya, biarkanListViewmengatur penggulirannya sendiri.
Ringkasan#
ListViewmembangun seluruhchildrensekaligus, sehingga hanya pantas untuk list pendek.ListView.buildermembangun item secara lazy melaluiitemCountdanitemBuilderyang dipanggil saat item mendekati layar.ListView.separatedbekerja seperti builder denganseparatorBuilderuntuk pemisah antaritem.- Disiplin performanya terdiri atas item yang ringan,
itemBuilderyang murni, danconstsedapat mungkin.
Referensi#
- ListView class — Flutter API
— Dokumentasi resmi
ListViewbeserta ketiga konstruktornya. - ListView.builder constructor — Flutter API
— Referensi resmi parameter
itemCountdanitemBuilder. - ListView.separated constructor — Flutter API — Referensi resmi varian list dengan pemisah antaritem.