TextField & TextEditingController
Input teks: membaca nilai, membersihkan, dan mengontrol cursor.
Kenapa Perlu Controller?
TextField punya dua cara membaca input: onChanged (callback setiap ketikan) dan TextEditingController. Callback cukup untuk kasus sederhana, tapi controller memberi kendali penuh dari kode: membaca nilai kapan saja, mengisi nilai secara program, memindahkan cursor, sampai mendengarkan perubahan tanpa rebuild.
final _namaController = TextEditingController();
TextField(
controller: _namaController,
onChanged: (nilai) => print(nilai), // opsional, untuk reaksi tiap ketikan
)
ElevatedButton(
onPressed: () {
print('Nama: ${_namaController.text}');
_namaController.clear(); // kosongkan dari kode
},
child: const Text('Cetak & Bersihkan'),
)Nilai Awal dan Manipulasi Teks
Beri nilai awal lewat konstruktor, dan ubah teks kapan saja dari kode:
final _kotaController = TextEditingController(text: 'Bandung');
// nanti, misalnya tombol "Isi Otomatis":
_kotaController.text = 'Jakarta'; // cursor otomatis pindah ke akhirEdge case yang sering mengejutkan: meng-assign .text selalu memindahkan cursor ke akhir teks. Kalau kamu perlu menjaga posisi cursor (misalnya saat memformat input nomor HP sambil mengetik), set value lengkap dengan selection:
_nomorController.value = TextEditingValue(
text: teksTerformat,
selection: TextSelection.collapsed(offset: teksTerformat.length),
);Mendengarkan Perubahan Tanpa Rebuild
addListener berguna saat perubahan teks harus memicu logika, bukan tampilan (misalnya mengaktifkan tombol hanya jika input valid), tanpa me-rebuild widget lewat setState:
@override
void initState() {
super.initState();
_namaController.addListener(() {
final valid = _namaController.text.trim().length >= 3;
_tombolAktif.value = valid; // ValueNotifier, tanpa setState
});
}Jangan lupa removeListener kalau listener-nya fungsi bernama, atau biarkan ikut terbuang saat controller di-dispose (listener anonim aman ikut ter-dispose bersama controller).
Contoh Nyata: Pencarian Live
final _cariController = TextEditingController();
final _hasil = ValueNotifier<List<String>>([]);
@override
void initState() {
super.initState();
_cariController.addListener(() {
final q = _cariController.text.toLowerCase();
_hasil.value = _semuaMenu
.where((m) => m.toLowerCase().contains(q))
.toList();
});
}
// di build:
Column(children: [
TextField(
controller: _cariController,
decoration: const InputDecoration(
labelText: 'Cari menu',
prefixIcon: Icon(Icons.search),
),
),
ValueListenableBuilder<List<String>>(
valueListenable: _hasil,
builder: (context, hasil, _) => Column(
children: hasil.map((m) => ListTile(title: Text(m))).toList(),
),
),
])Catatan teknis: Satu controller untuk satu field saja. Memakai controller yang sama di dua
TextFieldmembuat keduanya berbagi teks dan cursor, hasilnya perilaku aneh yang sulit di-debug.
Kapan TIDAK Perlu Controller
- Hanya butuh nilai saat tombol ditekan dan tidak perlu mengubah teks dari kode:
onChanged+ variabel lokal cukup, lebih sedikit boilerplate. - Field di dalam
Formyang nilainya diambil lewatonSaved: biarkanFormStateyang mengelola. - Input sekali pakai yang langsung di-submit (misalnya kolom komentar cepat): pakai
onSubmittedsaja.
Wajib Dispose
Controller menahan listener dan koneksi ke text input system. Selalu dispose:
@override
void dispose() {
_namaController.dispose();
super.dispose();
}Pengecualian: controller yang dibuat oleh TextFormField internal (tanpa kamu pass controller sendiri) diurus oleh Flutter, tidak perlu kamu dispose.
Tantangan
Input nama + sapa
Buat TextField nama + tombol 'Sapa'. Saat ditekan, tampilkan SnackBar 'Halo, [nama]!' dan kosongkan field.