ListView Horizontal
Daftar geser ke samping untuk kategori, banner promo, dan galeri.
Analogi: Rak Berjalan di Toko
Bayangkan rak pajangan di toko yang bisa digeser ke samping: permen, cokelat, dan mainan berjejer horizontal, dan kamu menggeser raknya untuk melihat semuanya. Kalau semua barang itu disusun vertikal ke bawah, tokonya butuh lorong super panjang.
Begitu juga di aplikasi: kategori, banner promo, dan galeri foto adalah barang-barang yang "setara". Menyusunnya vertikal membuat halaman jadi sangat panjang dan melelahkan di-scroll. ListView horizontal menaruhnya dalam satu baris geser yang hemat ruang vertikal, persis seperti story di Instagram atau deretan kategori di aplikasi belanja.
Kenapa Butuh Scroll Horizontal?
Layar HP itu sempit ke samping tapi panjang ke bawah. Untuk konten yang jumlahnya banyak tapi tiap itemnya kecil (chip kategori, thumbnail foto), scroll vertikal membuang ruang: satu layar hanya muat sedikit item. Scroll horizontal membalik logikanya: satu baris menampung banyak item, dan sisa layar vertikal bisa dipakai konten lain seperti daftar utama atau detail produk.
Contoh 1: Deretan Kategori Bulat
Pola paling umum: avatar bulat plus label, seperti kategori di aplikasi makanan atau e-commerce.
SizedBox(
height: 96,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: kategori.length,
itemBuilder: (context, index) {
final k = kategori[index];
return Padding(
padding: const EdgeInsets.only(right: 12),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius: 30,
backgroundColor: Colors.teal[100],
child: Icon(k.ikon, color: Colors.teal[800]),
),
const SizedBox(height: 6),
Text(k.nama, style: const TextStyle(fontSize: 12)),
],
),
);
},
),
)Dua hal wajib di contoh ini: scrollDirection: Axis.horizontal, dan SizedBox(height: 96) yang membungkus ListView. ListView horizontal harus tahu seberapa tinggi ia boleh tumbuh, kalau tidak Flutter akan protes dengan error layout.
Contoh 2: Banner Promo Geser
Naik level: kartu banner berwarna dengan teks promosi, seperti carousel di halaman utama aplikasi belanja.
SizedBox(
height: 150,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
physics: const BouncingScrollPhysics(),
itemCount: promo.length,
separatorBuilder: (context, index) => const SizedBox(width: 12),
itemBuilder: (context, index) {
final p = promo[index];
return Container(
width: 280,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: LinearGradient(colors: p.warna),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
p.judul,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(p.deskripsi, style: const TextStyle(color: Colors.white70)),
],
),
);
},
),
)Di sini ListView.separated dipakai versi horizontalnya untuk memberi jarak antar banner, dan tiap banner diberi width: 280 yang eksplisit. Lebar eksplisit itu wajib karena di arah scroll, ListView memberi ruang tak terbatas ke itemnya.
Kesalahan Umum Pemula
1. Lupa membatasi tinggi
Salah:
// JANGAN: error "Vertical viewport was given unbounded height"
Column(
children: [
ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) => KartuPromo(index),
),
const Text('Konten lain'),
],
)Benar: bungkus dengan SizedBox (atau Container) yang punya height pasti.
Column(
children: [
SizedBox(
height: 150,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) => KartuPromo(index),
),
),
const Text('Konten lain'),
],
)2. Memakai Expanded di dalam item horizontal
Salah:
// JANGAN: error karena lebar tak terbatas bertemu Expanded
itemBuilder: (context, index) => Expanded(child: KartuPromo(index)),Expanded butuh batas yang jelas untuk membagi ruang, sedangkan di arah scroll batas itu tidak ada. Benar: beri width eksplisit pada tiap item (contoh: width: 280).
3. Bingung soal nested scroll
Aturan praktisnya sederhana: ListView horizontal di dalam scroll vertikal (misalnya SingleChildScrollView atau Column) itu aman, asal tingginya dibatasi. Yang bermasalah adalah ListView vertikal di dalam scroll vertikal lain: di situ kamu butuh shrinkWrap: true plus NeverScrollableScrollPhysics, atau lebih baik rancang ulang layoutnya.
Catatan teknis: Untuk daftar horizontal yang sangat panjang (ratusan foto),
ListView.buildertetap lazy dan hanya membangun item yang terlihat, jadi performanya aman. TambahkancacheExtenthanya jika kamu melihat item berkedip saat di-scroll cepat.
Tantangan
Banner promo geser
Buat ListView horizontal berisi 5 banner (Container 250x120, warna berbeda, teks 'Promo 1'..'Promo 5'). Tinggi total 120.