Form & Validasi
Validasi input terpusat dengan Form, GlobalKey, dan validator.
Kenapa Butuh Widget Form?
Form login atau pendaftaran punya banyak field, dan tiap field punya aturannya sendiri: email wajib dan harus mengandung @, password minimal 6 karakter, konfirmasi password harus sama. Tanpa Form, kamu harus mengecek semuanya manual satu per satu, menampilkan error sendiri, dan gampang ada yang terlewat.
Solusi terpusat: satu panggilan validate() menjalankan semua aturan sekaligus dan menampilkan pesan error otomatis di field yang salah.
final _formKey = GlobalKey<FormState>();
Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
validator: (nilai) {
if (nilai == null || nilai.isEmpty) return 'Email wajib diisi';
if (!nilai.contains('@')) return 'Format email salah';
return null; // null artinya valid
},
),
TextFormField(
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
validator: (nilai) {
if (nilai == null || nilai.length < 6) return 'Minimal 6 karakter';
return null;
},
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// semua valid, baru proses data
_kirimPendaftaran();
}
},
child: const Text('Daftar'),
),
],
),
)Kontrak validator sederhana: kembalikan String error jika tidak valid, null jika valid.
Kapan Validasi Dijalankan: autovalidateMode
Secara default validasi hanya jalan saat validate() dipanggil (saat tombol submit ditekan). User baru tahu ada yang salah setelah menekan tombol, jadi autovalidateMode memberi perilaku yang lebih halus:
Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
...
)disabled(default): validasi hanya saatvalidate()dipanggil.onUserInteraction: validasi jalan setelah user menyentuh field. Pilihan terbaik untuk kebanyakan form: tidak mengganggu di awal, tapi memberi feedback cepat.always: validasi setiap ada perubahan, termasuk sebelum user menyentuh apa pun. Bikin form terlihat "marah" sejak awal, hindari kecuali ada alasan khusus.
Mengambil Nilai: onSaved dan save()
Selain validasi, Form bisa mengumpulkan nilai semua field sekaligus lewat onSaved:
String? _email;
TextFormField(
decoration: const InputDecoration(labelText: 'Email'),
validator: (v) => (v == null || !v.contains('@')) ? 'Email tidak valid' : null,
onSaved: (v) => _email = v,
)
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save(); // panggil semua onSaved
_kirimPendaftaran(_email!);
}
},
child: const Text('Daftar'),
)Pola ini rapi karena kamu tidak perlu TextEditingController untuk setiap field. Butuh reset form? _formKey.currentState!.reset() mengosongkan semua field sekaligus.
Contoh Nyata: Konfirmasi Password
Validator bisa membandingkan antar field, cukup simpan nilai field pertama di variabel:
final _passController = TextEditingController();
TextFormField(
controller: _passController,
obscureText: true,
decoration: const InputDecoration(labelText: 'Password'),
validator: (v) => (v == null || v.length < 6) ? 'Minimal 6 karakter' : null,
),
TextFormField(
obscureText: true,
decoration: const InputDecoration(labelText: 'Konfirmasi Password'),
validator: (v) {
if (v != _passController.text) return 'Password tidak sama';
return null;
},
),Catatan teknis: Pakai
TextFormField, bukanTextField, di dalamForm.TextFieldbiasa tidak terhubung ke sistem validator danonSavedmilik Form.
Edge Case: Validasi Async (Cek ke Server)
validator bersifat sinkron: tidak bisa await di dalamnya. Untuk cek yang butuh server (misalnya "username sudah dipakai"), lakukan manual setelah validasi lokal lolos:
onPressed: () async {
if (!_formKey.currentState!.validate()) return; // validasi lokal dulu
setState(() => _loading = true);
final dipakai = await api.cekUsername(_usernameController.text);
if (!mounted) return;
setState(() => _loading = false);
if (dipakai) {
// tampilkan error manual, karena validator tidak bisa async
setState(() => _usernameError = 'Username sudah dipakai');
return;
}
_kirimPendaftaran();
}Untuk error manual seperti ini, pakai errorText di decoration, atau simpan pesan di state lalu tampilkan lewat validator yang membaca variabel tersebut.
Kapan JANGAN Pakai Form
- Satu-dua field sederhana (kolom pencarian, input nama cepat):
Form+GlobalKeyadalah boilerplate yang tidak perlu. CukuponChanged/onSubmittedatau satu controller. - Validasi sangat dinamis dengan aturan yang berubah-ubah: state eksplisit (misalnya
ValueNotifierper field) kadang lebih mudah dibaca daripada validator yang saling bergantung.
Tantangan
Form login tervalidasi
Buat Form login: email (wajib + mengandung @) dan password (minimal 6). Tombol 'Masuk' hanya memproses jika validate() true, lalu tampilkan SnackBar 'Login berhasil'.