Radio & SegmentedButton
Pilihan tunggal: RadioListTile dan SegmentedButton modern.
Kenapa Butuh Widget Pilihan Tunggal?
Banyak form meminta user memilih tepat satu dari beberapa opsi yang saling meniadakan: metode bayar, ukuran kaos, bahasa aplikasi. Kalau kamu memakai banyak Checkbox, tidak ada yang mencegah user mencentang dua-duanya, dan kamu harus menulis logika matikan-yang-lain secara manual. Radio menyelesaikan masalah ini di level widget: satu grup berbagi satu groupValue, dan Flutter yang menjamin hanya satu yang aktif.
SegmentedButton adalah jawaban modern untuk kasus yang sama ketika opsinya sedikit, pendek, dan setara, mis. S/M/L atau Harian/Mingguan/Bulanan. Tampilannya seperti tombol-tombol yang menyatu, lebih hemat ruang vertikal daripada daftar radio.
Radio Klasik
String _bayar = 'tunai';
RadioListTile<String>(
title: const Text('Tunai'),
subtitle: const Text('Bayar di tempat'),
value: 'tunai',
groupValue: _bayar,
onChanged: (v) => setState(() => _bayar = v!),
),
RadioListTile<String>(
title: const Text('QRIS'),
subtitle: const Text('Scan kode saat checkout'),
value: 'qris',
groupValue: _bayar,
onChanged: (v) => setState(() => _bayar = v!),
),
RadioListTile<String>(
title: const Text('Transfer Bank'),
subtitle: const Text('BCA, BRI, Mandiri'),
value: 'transfer',
groupValue: _bayar,
onChanged: (v) => setState(() => _bayar = v!),
),
Text('Metode: $_bayar'),Semua radio dalam satu grup wajib memakai groupValue yang sama persis. Radio yang value-nya sama dengan groupValue tampil terpilih. Tanda ! pada v! aman karena onChanged tidak pernah mengirim null saat diklik, walau tipenya nullable.
Contoh Nyata: Ringkasan Ikut Berubah
Di aplikasi kasir, pilihan radio biasanya diikuti ringkasan yang ikut berubah, mis. biaya layanan per metode:
String _metode = 'Tunai';
final _biaya = {'Tunai': 0, 'QRIS': 2000, 'Transfer': 4000};
// di dalam Column, bangun tile dari map:
for (final m in _biaya.keys)
RadioListTile<String>(
title: Text(m),
subtitle: Text('Biaya Rp${_biaya[m]}'),
value: m,
groupValue: _metode,
onChanged: (v) => setState(() => _metode = v!),
),
const Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Biaya layanan'),
Text('Rp${_biaya[_metode]}'),
],
),Memakai String atau enum sebagai value lebih enak daripada index angka: kode tetap terbaca walau urutan berubah, dan opsi gampang ditambah tanpa copy-paste.
SegmentedButton (Modern)
String _ukuran = 'M';
SegmentedButton<String>(
segments: const [
ButtonSegment(value: 'S', label: Text('S')),
ButtonSegment(value: 'M', label: Text('M')),
ButtonSegment(value: 'L', label: Text('L')),
ButtonSegment(value: 'XL', label: Text('XL')),
],
selected: {_ukuran},
onSelectionChanged: (s) => setState(() => _ukuran = s.first),
),Perhatikan selected memakai Set ({_ukuran}), bukan nilai tunggal. Untuk multi-select, tambahkan multiSelectionEnabled: true dan simpan Set<String>, maka user bisa memilih beberapa segmen sekaligus. Satu widget, dua pola, tanpa ganti struktur.
Edge Case & Best Practice
Pertama, groupValue boleh null, artinya belum ada yang dipilih. Ini berguna untuk pertanyaan opsional, tapi untuk form wajib, isi nilai default agar user tidak bisa submit tanpa memilih. Kedua, tipe generic harus konsisten: RadioListTile<String> tidak bisa dicampur dengan value int dalam satu grup. Ketiga, kalau opsinya dibuat dari list dinamis (mis. dari API), bangun dengan .map() atau for-loop seperti contoh di atas, dan pastikan setiap value unik.
Keempat, SegmentedButton butuh ruang horizontal. Di layar sempit dengan label panjang, segmennya bisa terpotong, jadi untuk opsi berdeskripsi panjang tetap pilih RadioListTile vertikal. Kelima, untuk value yang bermakna di domain (metode bayar, status), pertimbangkan enum Dart daripada String mentah agar typo tertangkap saat compile.
Kapan TIDAK Memakai Ini
Untuk jawaban ya/tidak, pakai Checkbox atau Switch, bukan radio dengan opsi "Ya/Tidak". Untuk memilih banyak dari banyak, pakai Checkbox. Kalau opsinya lebih dari tujuh atau butuh penjelasan bergambar besar, Dropdown atau halaman pilihan khusus lebih tepat. Dan jangan memakai SegmentedButton untuk aksi seperti tab navigasi, karena user membacanya sebagai pilihan status, bukan perintah pindah halaman.
Catatan teknis:
Radiopolos (tanpa ListTile) tidak punya label dan area sentuhnya kecil. Di form nyata hampir selalu pakaiRadioListTileagar seluruh baris bisa diketuk dan memenuhi target sentuh 48px.
Tantangan
Pilih metode bayar
Buat 3 RadioListTile: Tunai, QRIS, Transfer. Tampilkan teks 'Dipilih: [metode]' di bawahnya yang update otomatis.