Topik ini membahas batas penggunaan setState. State lokal hanya berlaku pada lingkup satu widget. Pembahasan menunjukkan gejala ketika batas itu terlewati, masalah prop drilling yang menyertainya, serta tanda kapan state perlu dinaikkan atau dipindahkan ke lapisan pengelolaan state.
Lingkup state lokal#
Data pada kelas State hanya diketahui oleh widget yang memiliki kelas tersebut. Widget saudara tidak dapat membacanya, widget leluhur tidak dapat melihat isinya, dan satu-satunya jalur keluar nilai adalah penerusan data ke anak melalui konstruktor. Pemisahan ini disengaja karena setiap widget membangun tampilannya sendiri dari data yang diterima.
// tidak-dijalankan
class PenghitungKecil extends StatefulWidget {
const PenghitungKecil({super.key});
@override
State<PenghitungKecil> createState() => _PenghitungKecilState();
}
class _PenghitungKecilState extends State<PenghitungKecil> {
int _jumlah = 0;
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: () {
setState(() {
_jumlah += 1;
});
},
child: Text('Tekan: $_jumlah'),
);
}
}Menekan salah satu instans PenghitungKecil tidak memengaruhi instans lainnya karena masing-masing memiliki objek State sendiri. Perilaku ini tepat untuk data yang memang lokal, misalnya status animasi atau panel yang diperluas dan diciutkan. Masalah baru muncul ketika satu nilai harus diketahui oleh beberapa widget sekaligus.
Isolasi semacam ini adalah fitur, bukan kelemahan. Widget dengan state yang benar-benar lokal mudah dipindahkan dan mudah diuji karena tidak ada ketergantungan tersembunyi antarwidget. Oleh karena itu, state sebaiknya diletakkan serendah mungkin, yaitu pada widget terkecil yang membutuhkannya, dan dinaikkan hanya ketika kebutuhan berbagi muncul.
Ketika dua cabang membutuhkan state yang sama#
Aplikasi catatan menyediakan contoh yang khas. Kotak masukan menjadi tempat penulisan catatan baru, sedangkan daftar di bawahnya menampilkan seluruh catatan, dan jumlah catatan ingin ditampilkan pada judul layar. Ketiganya membutuhkan nilai List<String> yang sama. Tidak ada satu pun widget anak yang layak menjadi tempat penyimpan nilai tersebut karena setiap anak hanya melihat sebagian kebutuhan.
Satu-satunya tempat yang memenuhi ketiganya adalah leluhur terdekat yang mencakup kotak masukan, daftar, dan judul. State harus naik ke widget tersebut, data mengalir turun melalui konstruktor, dan perubahan dilaporkan naik melalui callback. Dua arah ini, yaitu data yang turun dan laporan perubahan yang naik, membentuk pola aliran data satu arah yang menjadi kunci keteraturan aplikasi Flutter.
Apabila nilai tersebut tetap disimpan di salah satu anak, dua widget lain tidak memiliki cara mengambilnya, sehingga nilai cenderung diduplikasi dan cepat berbeda. Pemindahan state ini bukan sekadar gaya penulisan, melainkan konsekuensi dari bentuk kebutuhan data itu sendiri.
Prop drilling#
Prop drilling adalah penerusan data dan callback melalui beberapa lapisan widget yang sebenarnya tidak membutuhkannya. Lapisan tengah hanya menjadi perantara, padahal setiap parameter tambahan memperluas konstruktor, memperbanyak properti kelas, dan menambah titik yang rawan keliru. Semakin dalam data harus menurun, semakin banyak lapisan yang tersentuh setiap kali kebutuhan berubah.
Pada contoh jadwal piket berikut, callback penandaan selesai harus mencapai widget terdalam, sehingga dua lapisan di atasnya ikut meneruskannya. Tipe ValueChanged<String> hanyalah nama lain untuk fungsi yang menerima satu String dan tidak mengembalikan apa pun.
// tidak-dijalankan
class LayarJadwal extends StatelessWidget {
const LayarJadwal({super.key, required this.onTandaiSelesai});
final ValueChanged<String> onTandaiSelesai;
@override
Widget build(BuildContext context) {
return PanelHari(onTandaiSelesai: onTandaiSelesai);
}
}
class PanelHari extends StatelessWidget {
const PanelHari({super.key, required this.onTandaiSelesai});
final ValueChanged<String> onTandaiSelesai;
@override
Widget build(BuildContext context) {
return KartuPiket(onTandaiSelesai: onTandaiSelesai);
}
}
class KartuPiket extends StatelessWidget {
const KartuPiket({super.key, required this.onTandaiSelesai});
final ValueChanged<String> onTandaiSelesai;
@override
Widget build(BuildContext context) {
return Column(
children: [
const Text('Senin'),
ButirPiket(
nama: 'Ali',
onTandaiSelesai: onTandaiSelesai,
),
],
);
}
}
class ButirPiket extends StatelessWidget {
const ButirPiket({super.key, required this.nama, required this.onTandaiSelesai});
final String nama;
final ValueChanged<String> onTandaiSelesai;
@override
Widget build(BuildContext context) {
return CheckboxListTile(
title: Text(nama),
onChanged: (selesai) {
if (selesai ?? false) {
onTandaiSelesai(nama);
}
},
);
}
}PanelHari dan KartuPiket tidak pernah menggunakan callback tersebut, tetapi keduanya tetap wajib mendeklarasikan properti, menerimanya pada konstruktor, dan meneruskannya ke anak. Penambahan satu parameter baru berarti menyunting semua lapisan. Apabila lintasan ini melewati lima atau sepuluh lapisan, perubahan kecil pada bentuk callback menyentuh hampir seluruh berkas proyek.
Biaya semacam ini tidak langsung terasa pada pohon kecil, tetapi tumbuh seiring bertambahnya kedalaman pohon. Satu properti baru menambah tiga titik suntingan per lapisan, yaitu deklarasi field, konstruktor, dan pemanggilan pada anak. Mengganti satu nama callback berarti mengubah seluruh lintasan.
Tanda state perlu berpindah#
Batas setState tercapai apabila salah satu tanda berikut muncul.
- Dua widget atau lebih membutuhkan nilai state yang sama pada waktu yang sama.
- Data dan callback diteruskan melewati dua lapisan widget atau lebih tanpa digunakan oleh lapisan tengah.
- Logika yang sama terduplikasi di beberapa widget agar hasilnya tampak sinkron.
- Nilai harus tetap tersimpan ketika widget pemiliknya dibuang, misalnya saat berpindah layar.
Dua jalan keluar tersedia, sesuai dengan tingkat kebutuhannya. State yang dibagikan di dalam satu layar cukup dinaikkan ke widget leluhur terdekat. State yang perlu diakses lintas layar atau lintas cabang pohon yang dalam lebih tepat dipindahkan ke lapisan pengelolaan state, yang dibangun di atas mekanisme InheritedWidget.
Ambang praktisnya dapat dirumuskan sebagai berikut. Penerusan melewati satu lapisan masih wajar dan lazim terjadi. Melewati dua lapisan sudah patut dipertanyakan, dan melewati tiga lapisan atau lebih hampir selalu menandakan bahwa state seharusnya dipindahkan. Angka ini bukan kaidah baku, melainkan pedoman untuk menghentikan pertumbuhan lintasan penerusan lebih awal.
Kesalahan umum#
- Menyimpan state bersama pada widget anak. Widget lain tidak dapat mengaksesnya, sehingga solusi yang menggoda adalah menduplikasi nilainya. Kenali tanda-tanda di atas lebih awal, lalu naikkan state ke leluhur terdekat.
- Meneruskan callback hanya karena jarak penerusannya dekat. Lapisan tengah ikut berubah setiap kali bentuk callback berubah. Periksa apakah lapisan itu benar-benar membutuhkan nilai sebelum menambahkan propertinya.
Ringkasan#
- State pada kelas State hanya berlaku pada widget pemiliknya dan tidak terlihat oleh saudara maupun leluhur.
- Dua cabang yang membutuhkan nilai sama menuntut state naik ke leluhur terdekat yang mencakup keduanya.
- Prop drilling adalah penerusan data dan callback melalui lapisan yang tidak membutuhkannya, dan biayanya tumbuh seiring kedalaman pohon.
- State yang lintas layar lebih tepat berpindah ke lapisan pengelolaan state.
Referensi#
- Ephemeral vs app state — Flutter
— Dokumentasi resmi yang membedakan state lokal
setStatedari state tingkat aplikasi. - Simple app state management — Flutter — Panduan resmi mengenali kapan state perlu dikelola di luar widget.