Membuat Form Dinamis di Flutter dengan Reactive Forms
Mengembangkan aplikasi Flutter seringkali melibatkan pengelolaan formulir (form), dan tidak jarang kebutuhan akan form yang dinamis muncul. Form dinamis adalah form yang strukturnya dapat berubah saat runtime—misalnya, menambah atau menghapus bidang input berdasarkan interaksi pengguna, atau menampilkan bagian form yang berbeda tergantung pada pilihan tertentu. Mengelola state dan validasi form dinamis dengan pendekatan tradisional (misalnya, menggunakan TextEditingController secara manual) dapat menjadi rumit dan rentan kesalahan. Di sinilah Reactive Forms hadir sebagai solusi yang elegan dan powerful.
Apa itu Reactive Forms?
Reactive Forms adalah package populer di Flutter yang terinspirasi oleh implementasi form di Angular. Ia menyediakan pendekatan deklaratif dan reaktif untuk mengelola state form. Daripada memanipulasi nilai input secara langsung melalui controller, Reactive Forms memungkinkan Anda untuk mendefinisikan struktur form Anda sebagai model data (FormGroup, FormControl, FormArray) yang kemudian secara otomatis diikatkan ke widget UI Anda.
FormControl: Merepresentasikan bidang input tunggal (misalnya, nama, email).FormGroup: Mengelompokkan beberapaFormControlmenjadi satu unit logis (misalnya, form login yang berisi email dan password).FormArray: Mengelola array dariFormControlatauFormGroup, yang sangat cocok untuk item berulang atau daftar dinamis.
Mengapa Reactive Forms untuk Form Dinamis?
Reactive Forms sangat unggul dalam skenario form dinamis karena beberapa alasan utama:
- Manajemen State yang Terpusat: Seluruh state form diwakili oleh objek
FormGroupatauFormArray, membuatnya mudah untuk menambah, menghapus, atau memodifikasi bidang secara terprogram. - Validasi yang Kuat: Validator dapat diterapkan secara dinamis ke
FormControlatauFormGroup, dan status validasi (misalnya,isValid,hasErrors) diperbarui secara reaktif. - Abstraksi UI: Form model terpisah dari representasi UI, memungkinkan Anda untuk fokus pada logika bisnis tanpa terjebak dalam detail widget.
- Kemudahan Pengujian: Karena form model adalah objek Dart murni, ia dapat diuji secara independen tanpa perlu rendering UI.
Memulai dengan Reactive Forms
Langkah pertama adalah menambahkan dependensi ke file pubspec.yaml Anda:
dependencies:
flutter:
sdk: flutter
reactive_forms: ^<versi_terbaru> # Ganti dengan versi terbaru
Kemudian, jalankan flutter pub get.
Struktur dasar sebuah form melibatkan pembuatan FormGroup dan mengikatnya ke widget ReactiveForm:
final form = FormGroup({
'namaDepan': FormControl(value: '', validators: [Validators.required]),
'email': FormControl(value: '', validators: [Validators.required, Validators.email]),
});
// Dalam widget build:
ReactiveForm(
formGroup: form,
child: Column(
children: [
ReactiveTextField(
formControlName: 'namaDepan',
decoration: const InputDecoration(labelText: 'Nama Depan'),
),
ReactiveTextField(
formControlName: 'email',
decoration: const InputDecoration(labelText: 'Email'),
),
ElevatedButton(
onPressed: () {
if (form.valid) {
print(form.value);
} else {
form.markAllAsTouched();
}
},
child: const Text('Kirim'),
),
],
),
)
Mengimplementasikan Form Dinamis dengan FormArray
FormArray adalah kunci untuk membangun form dinamis, terutama ketika Anda memiliki daftar item yang berulang, seperti daftar anggota keluarga, hobi, atau detail produk.
Skenario: Daftar Item Belanja
Misalkan kita ingin membuat form di mana pengguna dapat menambahkan beberapa item belanja, dan setiap item memiliki nama dan kuantitasnya sendiri.
import 'package:flutter/material.dart';
import 'package:reactive_forms/reactive_forms.dart';
class ShoppingForm extends StatefulWidget {
const ShoppingForm({super.key});
@override
State createState() => _ShoppingFormState();
}
class _ShoppingFormState extends State {
final form = FormGroup({
'namaPelanggan': FormControl(value: '', validators: [Validators.required]),
'items': FormArray([
_createShoppingItem(), // Item awal
]),
});
static FormGroup _createShoppingItem() {
return FormGroup({
'namaProduk': FormControl(value: '', validators: [Validators.required]),
'kuantitas': FormControl(value: 1, validators: [Validators.required, Validators.min(1)]),
});
}
FormArray get items => form.control('items') as FormArray;
void _addShoppingItem() {
items.add(_createShoppingItem());
}
void _removeShoppingItem(int index) {
items.removeAt(index);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Form Belanja Dinamis')),
body: ReactiveForm(
formGroup: form,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ReactiveTextField(
formControlName: 'namaPelanggan',
decoration: const InputDecoration(labelText: 'Nama Pelanggan'),
),
const SizedBox(height: 20),
Text('Daftar Belanja', style: Theme.of(context).textTheme.headline6),
ReactiveFormArray(
formArrayName: 'items',
builder: (context, formArray, child) {
return Column(
children: formArray.controls.asMap().entries.map((entry) {
final index = entry.key;
final itemFormGroup = entry.value;
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: ReactiveTextField(
formControlName: 'namaProduk',
decoration: InputDecoration(labelText: 'Produk #${index + 1}'),
validationMessages: {
ValidationMessage.required: (_) => 'Nama produk wajib diisi',
},
formControl: itemFormGroup.control('namaProduk') as FormControl,
),
),
const SizedBox(width: 10),
SizedBox(
width: 80,
child: ReactiveTextField(
formControlName: 'kuantitas',
keyboardType: TextInputType.number,
decoration: const InputDecoration(labelText: 'Qty'),
validationMessages: {
ValidationMessage.required: (_) => 'Qty wajib',
ValidationMessage.min: (_) => 'Min. 1',
},
formControl: itemFormGroup.control('kuantitas') as FormControl,
),
),
IconButton(
icon: const Icon(Icons.remove_circle, color: Colors.red),
onPressed: () => _removeShoppingItem(index),
),
],
),
);
}).toList(),
);
},
),
ElevatedButton.icon(
onPressed: _addShoppingItem,
icon: const Icon(Icons.add),
label: const Text('Tambah Item'),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
if (form.valid) {
print('Form data: ${form.value}');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Form valid! Data: ${form.value}')),
);
} else {
form.markAllAsTouched();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Form tidak valid! Periksa input Anda.')),
);
}
},
child: const Text('Kirim Belanja'),
),
],
),
),
),
);
}
}
Penjelasan Kode
- Kita membuat
FormArraybernama'items'. Setiap elemen dalam array ini adalahFormGroupyang mewakili satu item belanja. - Fungsi
_createShoppingItem()adalah helper untuk membuatFormGroupbaru untuk setiap item, memastikan setiap item memiliki struktur yang sama (nama produk dan kuantitas). - Metode
_addShoppingItem()hanya memanggilitems.add(_createShoppingItem())untuk menambahkanFormGroupbaru keFormArray. Ini secara otomatis akan memicu build ulang UI yang terkait. - Metode
_removeShoppingItem(int index)menggunakanitems.removeAt(index)untuk menghapusFormGrouppada indeks tertentu, juga memicu pembaruan UI. - Widget
ReactiveFormArraydigunakan untuk membangun UI berdasarkanFormArray. Di dalam builder-nya, kita mengulang melalui setiapFormGroupdalamformArray.controlsdan merender widget input yang sesuai (ReactiveTextField). - Penting untuk dicatat penggunaan
formControl: itemFormGroup.control('namaProduk') as FormControlsaat berada di dalamReactiveFormArraykarenaReactiveTextFieldperlu tahuFormControlmana yang harus diikat dalam konteksFormGroupspesifik item tersebut.
Validasi dalam Form Dinamis
Reactive Forms secara otomatis menangani validasi untuk form dinamis Anda. Ketika Anda menambahkan atau menghapus item dari FormArray, status validasi dari FormArray (dan juga FormGroup induknya) akan diperbarui secara reaktif.
- Anda dapat menambahkan validator ke setiap
FormControlsaat membuatnya (sepertiValidators.requiredatauValidators.min(1)). - Validator juga dapat ditambahkan ke
FormGroupatauFormArrayitu sendiri (misalnya, validator yang membandingkan dua bidang dalam satuFormGroup). - Metode
form.markAllAsTouched()sangat berguna untuk memicu pesan validasi pada semua bidang ketika pengguna mencoba mengirim form yang tidak valid.
Keunggulan dan Praktik Terbaik
- Separasi Tanggung Jawab: Form model terpisah dari UI, membuat kode lebih bersih dan mudah dikelola.
- Reaktivitas Penuh: Perubahan pada model form secara otomatis tercermin di UI, dan sebaliknya.
- Pengujian Unit yang Mudah: Logika validasi dan manajemen state form dapat diuji secara terpisah dari UI.
- Performa: Gunakan
ReactiveFormArraydengan bijak. Untuk daftar yang sangat panjang, pertimbangkan untuk mengoptimalkan rendering denganListView.builderjika memungkinkan, meskipunReactiveFormArraysendiri cukup efisien. - Ekstensibilitas: Reactive Forms mendukung validator kustom dan widget UI kustom, memungkinkan Anda untuk memperluas fungsionalitasnya sesuai kebutuhan.
Kesimpulan
Reactive Forms adalah alat yang sangat ampuh untuk membangun form di Flutter, terutama ketika berhadapan dengan kompleksitas form dinamis. Dengan menyediakan pendekatan deklaratif dan reaktif, ia menyederhanakan manajemen state, validasi, dan interaksi UI, menghasilkan kode yang lebih mudah dibaca, diuji, dan dipelihara. Jika aplikasi Anda membutuhkan form yang fleksibel dan interaktif, Reactive Forms patut menjadi pertimbangan utama Anda.