Card & ListTile
Kartu Material dan baris daftar siap pakai dengan leading, title, trailing.
Analogi: Kartu Nama dan Daftar Belanja
Bayangkan kartu nama: selembar kertas agak tebal, sedikit terangkat dari meja karena bayangannya, berisi nama, jabatan, dan kontak dalam susunan yang selalu sama. Kamu langsung tahu cara membacanya tanpa diajari. Card di Flutter adalah "kartu nama" digital: permukaan yang sedikit terangkat (elevasi) dengan sudut membulat. Sedangkan ListTile seperti baris di daftar belanja: setiap baris punya pola tetap, ikon di kiri, nama barang di tengah, harga di kanan. Dua widget ini adalah fondasi hampir semua daftar di aplikasi: menu makanan, riwayat transaksi, daftar kontak.
Kenapa Card dan ListTile Itu Ada?
Material Design (bahasa desain Android) punya aturan: konten yang dikelompokkan tampil sebagai kartu yang terangkat dari latar, dan baris daftar punya struktur baku (leading, title, subtitle, trailing). Tanpa widget bawaan, setiap programmer akan merancang baris daftarnya sendiri dengan Row, Column, dan GestureDetector, hasilnya: tampilan tidak konsisten antar aplikasi dan efek sentuh (ripple) sering lupa dipasang. Card dan ListTile membakukan pola ini: kamu cukup mengisi slot yang tersedia, konsistensi dan efek Material didapat gratis.
Contoh 1: Card Sederhana
Card(
elevation: 4,
margin: const EdgeInsets.all(8),
child: const Padding(
padding: EdgeInsets.all(16),
child: Text('Isi kartu'),
),
)elevation mengatur seberapa "terangkat" kartu terlihat (semakin besar, bayangan semakin lebar). Card punya margin bawaan, jadi kamu tidak perlu membungkusnya dengan Padding hanya untuk jarak antar kartu.
Contoh 2: Daftar Menu dengan ListView
Di aplikasi nyata, Card dan ListTile hampir selalu dipakai bersama di dalam daftar yang bisa di-scroll:
class MenuList extends StatelessWidget {
const MenuList({super.key});
final items = const [
('Kopi Hitam', 'Rp10.000', Icons.coffee),
('Kopi Susu', 'Rp15.000', Icons.local_cafe),
('Matcha Latte', 'Rp18.000', Icons.emoji_food_beverage),
];
@override
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: items.length,
itemBuilder: (context, index) {
final (nama, harga, ikon) = items[index];
return Card(
child: ListTile(
leading: Icon(ikon, color: Colors.brown),
title: Text(nama),
subtitle: const Text('Diseduh fresh setiap pesanan'),
trailing: Text(harga,
style: const TextStyle(fontWeight: FontWeight.bold)),
onTap: () {
// buka detail menu
},
),
);
},
);
}
}Setiap baris punya peran jelas: leading untuk ikon kategori, title untuk nama, subtitle untuk deskripsi kecil, trailing untuk harga atau panah navigasi. Pola ini dipakai aplikasi kasir, e-commerce, sampai aplikasi chat.
Catatan teknis:
ListTilesudah punya ripple effect bawaan saatonTapdiisi. Jangan bungkus denganInkWelllagi, efeknya akan dobel dan terlihat aneh.
Kustomisasi yang Sering Dibutuhkan
shape: ubah bentuk kartu, misalnyaRoundedRectangleBorder(borderRadius: BorderRadius.circular(16))untuk sudut lebih membulat.color: warna latar kartu (default-nya mengikuti tema aplikasi).dense: truediListTile: membuat baris lebih ramping untuk daftar yang padat.trailingtidak harus ikon: bisaTextharga,Switch, atauCheckboxuntuk pengaturan.
Tips: Garis Pemisah Antar Baris
Kalau daftar tidak dibungkus Card per baris (misalnya daftar pengaturan), baris-baris ListTile bisa terlihat menyatu. Pakai ListTile.divideTiles untuk menyisipkan garis pemisah otomatis:
ListView(
children: ListTile.divideTiles(
context: context,
tiles: const [
ListTile(leading: Icon(Icons.person), title: Text('Profil')),
ListTile(leading: Icon(Icons.lock), title: Text('Keamanan')),
ListTile(leading: Icon(Icons.info), title: Text('Tentang')),
],
).toList(),
)Garisnya mengikuti warna tema (dividerColor) sehingga selalu selaras dengan mode terang dan gelap aplikasi.
Kesalahan Umum Pemula
1. Membungkus ListTile dengan InkWell/GestureDetector
// SALAH: ripple dobel, kode mubazir
InkWell(
onTap: () {},
child: const ListTile(title: Text('Profil')),
)
// BENAR: ListTile sudah menangani sentuhan sendiri
ListTile(
title: const Text('Profil'),
onTap: () {},
)2. onTap dikosongkan tapi mengharapkan ripple. Kalau onTap bernilai null, ListTile tampil nonaktif: tidak ada ripple dan teks terlihat pudar. Itu memang disengaja untuk status "disabled". Kalau baris harus bisa ditekan, selalu isi onTap dengan fungsi, walau masih kosong.
3. Card menempel tanpa margin di dalam Column. Card punya margin default 4px, tapi di dalam Column sempit itu sering kurang. Tambahkan margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6) agar kartu punya ruang napas, atau lebih baik: pakai ListView dengan padding seperti contoh di atas.
Tantangan
Daftar menu
Buat 3 Card berisi ListTile menu: Kopi Hitam (Rp10.000), Kopi Susu (Rp15.000), Matcha (Rp18.000). Masing-masing dengan leading icon coffee dan trailing teks harga.