Widget Keys
Kapan butuh Key: list dinamis, reorder, dan menjaga state.
Kenapa Key Dibutuhkan?
Saat rebuild, Flutter harus mencocokkan widget baru dengan element lama di tree. Aturan defaultnya sederhana: cocokkan berdasarkan tipe dan posisi. Untuk UI statis ini cukup. Tapi begitu daftar bisa dihapus, disisipi, atau di-reorder, pencocokan posisi runtuh. Bayangkan tiga TextField berisi teks, lalu item pertama dihapus: Flutter mengira "field di posisi 0 tetap field di posisi 0", sehingga teks yang diketik user bergeser ke field yang salah. Key memberi tiap widget identitas yang bertahan melintasi perubahan posisi, jadi Flutter tahu persis element mana milik siapa.
// TANPA key: hapus item pertama, teks field bergeser ke field yang salah
ListView.builder(
itemCount: item.length,
itemBuilder: (context, i) => TextField(
controller: item[i].controller,
),
)
// DENGAN key: tiap field terikat pada datanya, bukan posisinya
ListView.builder(
itemCount: item.length,
itemBuilder: (context, i) => TextField(
key: ValueKey(item[i].id),
controller: item[i].controller,
),
)ValueKey(id) membuat Flutter mencocokkan berdasarkan id data, bukan indeks. Item yang dihapus benar-benar hilang, dan field yang tersisa mempertahankan state-nya. Tanpa key, yang "hilang" selalu item terakhir secara visual, meski data yang dihapus adalah item pertama, bug klasik yang membingungkan.
Contoh Nyata: Dismissible + Reorder
Kasus klasik lain adalah daftar yang bisa digeser untuk menghapus sekaligus diurutkan ulang:
ReorderableListView.builder(
itemCount: tugas.length,
onReorder: (lama, baru) {
setState(() {
var tujuan = baru;
if (tujuan > lama) tujuan -= 1;
final item = tugas.removeAt(lama);
tugas.insert(tujuan, item);
});
},
itemBuilder: (context, i) {
final t = tugas[i];
return Dismissible(
key: ValueKey(t.id), // wajib: identitas stabil per item
background: Container(color: Colors.red),
onDismissed: (_) =>
setState(() => tugas.removeWhere((e) => e.id == t.id)),
child: ListTile(title: Text(t.nama)),
);
},
)Tanpa key yang stabil, ReorderableListView bahkan menolak berjalan (ia mewajibkan key pada tiap anak langsung), dan Dismissible akan menghapus item yang salah setelah reorder. Key di sini bukan opsional, melainkan syarat.
Jenis-Jenis Key
- ValueKey: identitas dari sebuah nilai (
id, string, angka). Paling umum, pakai ini dulu. - ObjectKey: identitas dari identitas object itu sendiri. Berguna kalau datanya tidak punya id unik.
- UniqueKey: identitas acak yang dibuat sekali. Cocok untuk memaksa widget "lahir baru" (misalnya mereset seluruh form), tapi jangan dipakai di dalam
buildkarena tiap rebuild membuat identitas baru dan state selalu hilang. - GlobalKey: key global yang bisa mengakses
Stateataucontextdari mana saja. Contoh paling umum: validasi form dari tombol yang berada di luar form.
final _formKey = GlobalKey<FormState>();
Form(
key: _formKey,
child: Column(children: [...]),
);
// dari mana saja, misalnya AppBar action:
void _simpan() {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
}
}Kapan TIDAK Pakai Key
Key bukan hiasan gratis. Key yang tidak perlu menghambat framework memakai ulang element, sehingga performa justru turun. Aturannya sederhana: pakai key hanya saat ada daftar dinamis (tambah, hapus, reorder) berisi widget stateful, atau saat butuh GlobalKey untuk akses dari luar. Jangan tempel ValueKey di semua widget "biar aman", dan jangan pernah pakai UniqueKey di dalam build. Kalau daftarmu statis dan tidak pernah berubah urutan, kamu tidak butuh key sama sekali.
Catatan teknis:
GlobalKeyitu mahal: ia mendaftarkan diri ke registry global dan mencegah subtree-nya dibuang dari memori. Satu atau duaGlobalKeyuntuk form tidak masalah, tapi puluhanGlobalKeydi list panjang adalah bau kode yang harus diwaspadai.
Tantangan
Dismissible ber-key
Buat daftar item dengan Dismissible + ValueKey(item.id). Geser untuk menghapus, pastikan item yang terhapus benar.