Topik ini membahas dua mekanisme berbagi state di Flutter, yaitu pola lifting state up (menaikkan state) dan widget InheritedWidget. Keduanya menyelesaikan masalah yang muncul ketika batas setState terlampaui, dengan cara yang berbeda. Pemahaman atas keduanya menjadi fondasi menuju package Provider, yang dibangun di atas mekanisme InheritedWidget.
Pola menaikkan state#
Pola menaikkan state memindahkan penyimpanan state ke leluhur terdekat yang mencakup seluruh widget pemakainya. Data mengalir turun melalui konstruktor, sedangkan perubahan dilaporkan naik melalui callback. Arahnya selalu satu. Anak tidak pernah mengubah state leluhur secara langsung, melainkan meminta leluhur melakukannya melalui callback.
Contoh daftar belanja pribadi berikut menempatkan List<String> pada widget LayarBelanja, lalu meneruskannya ke dua anak, yaitu KotakInput dan TampilanBelanja.
// tidak-dijalankan
class LayarBelanja extends StatefulWidget {
const LayarBelanja({super.key});
@override
State<LayarBelanja> createState() => _LayarBelanjaState();
}
class _LayarBelanjaState extends State<LayarBelanja> {
final _belanjaan = <String>[];
void _tambahBarang(String barang) {
setState(() {
_belanjaan.add(barang);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Daftar belanja (${_belanjaan.length})')),
body: Column(
children: [
KotakInput(onTambah: _tambahBarang),
Expanded(child: TampilanBelanja(belanjaan: _belanjaan)),
],
),
);
}
}Anak yang menghasilkan perubahan menerima callback tanpa mengetahui bagaimana daftar disimpan, sedangkan anak yang hanya menampilkan menerima data sebagai parameter konstruktor biasa.
// tidak-dijalankan
class KotakInput extends StatefulWidget {
const KotakInput({super.key, required this.onTambah});
final ValueChanged<String> onTambah;
@override
State<KotakInput> createState() => _KotakInputState();
}
class _KotakInputState extends State<KotakInput> {
final _kontrol = TextEditingController();
void _kirim() {
if (_kontrol.text.isNotEmpty) {
widget.onTambah(_kontrol.text);
_kontrol.clear();
}
}
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: TextField(
controller: _kontrol,
onSubmitted: (_) => _kirim(),
),
),
IconButton(onPressed: _kirim, icon: const Icon(Icons.add)),
],
);
}
@override
void dispose() {
_kontrol.dispose();
super.dispose();
}
}Di dalam kelas State, callback milik widget diakses melalui widget.onTambah. Keuntungan pola ini adalah konsistensi yang diberikan secara otomatis. Hanya ada satu tempat penyimpanan, sehingga tidak ada dua tampilan yang menunjukkan nilai berbeda untuk data yang sama. Setiap perubahan dari anak memicu setState pada leluhur, dan seluruh anak yang meneruskan data ikut dibangun ulang dengan nilai terbaru.
Diagram berikut merangkum arah aliran pada pola ini. State disimpan pada widget leluhur, data mengalir turun ke anak melalui konstruktor, dan callback membawa laporan perubahan ke arah sebaliknya.
InheritedWidget#
InheritedWidget adalah widget khusus bawaan Flutter yang menyebarkan data ke seluruh subpohon di bawahnya tanpa penerusan konstruktor. Widget mana pun yang berada di bawahnya dapat membaca data tersebut langsung dari BuildContext. Widget bawaan seperti Theme dan MediaQuery sebenarnya bekerja dengan mekanisme ini, sehingga Theme.of(context) yang sudah sering digunakan merupakan contoh nyatanya.
Cara menggunakannya adalah membuat subclass yang menyimpan data pada field final, menimpa updateShouldNotify, dan menyediakan metode statis untuk membacanya. Instans subclass tersebut selanjutnya disebut penyedia karena tugasnya menyediakan data bagi subpohon di bawahnya. Penyedia dapat dipasang pada titik mana pun di atas seluruh pemakainya. Data yang digunakan satu layar cukup diletakkan di atas body layar tersebut, sedangkan data lintas layar dapat ditempatkan di atas MaterialApp agar seluruh subpohon berada di bawahnya. Cakupan data ditentukan oleh penempatan ini, sehingga perlu dirancang bersama dengan rancangan pohon widget.
// tidak-dijalankan
class PenghitungData extends InheritedWidget {
const PenghitungData({
super.key,
required this.jumlahLangkah,
required super.child,
});
final int jumlahLangkah;
static PenghitungData? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<PenghitungData>();
}
@override
bool updateShouldNotify(PenghitungData lama) {
return lama.jumlahLangkah != jumlahLangkah;
}
}Metode of memanggil dependOnInheritedWidgetOfExactType, yang sekaligus mendaftarkan widget pemanggil sebagai dependen. Dengan pendaftaran itu, setiap kali penyedia baru dipasang dengan data yang berubah, metode build seluruh dependen dipanggil ulang. Keputusan apakah dependen perlu diperbarui ditentukan oleh updateShouldNotify, yaitu metode yang membandingkan data lama dengan data baru.
Mekanisme pendaftaran ini berlangsung pada setiap build. Flutter mencatat dependensi saat of dipanggil, lalu menggunakan catatan itu untuk memutuskan widget mana yang ikut dibangun ulang. Karena pencatatan berlangsung per widget, dua widget di bawah penyedia yang sama dapat memiliki perilaku rebuild yang berbeda sesuai pemanggilan of masing-masing.
Pembacaan data dari widget mana pun di bawah penyedia cukup dilakukan dengan satu baris. Karena of mengembalikan tipe nullable, yaitu tipe yang boleh bernilai null, operator ! digunakan ketika keberadaan penyedia sudah dipastikan.
// tidak-dijalankan
final data = PenghitungData.of(context)!;Pembacaan melalui of bersifat satu arah dan tidak mengubah apa pun. Nilai yang diterima hanyalah data terbaru pada saat build berjalan, sehingga widget tidak perlu menyimpan salinan datanya sendiri.
Batas InheritedWidget dan jalan menuju Provider#
InheritedWidget menyelesaikan masalah penerusan konstruktor, tetapi penggunaannya masih memerlukan langkah yang rinci. Setiap jenis data menuntut satu subclass, satu metode of, dan satu penimpaan updateShouldNotify. Penyedia juga harus dipasang ulang dengan data baru agar dependen diberi tahu, sehingga state yang bisa berubah tetap perlu tempat penyimpanan tersendiri di atas penyedia.
Package Provider dibangun di atas mekanisme ini. Provider membungkus InheritedWidget dengan API yang lebih ringkas, bekerja sama dengan ChangeNotifier untuk bagian state yang berubah, dan menghilangkan penulisan subclass secara manual. Pemahaman terhadap InheritedWidget membuat cara kerja Provider terlihat jelas, karena keduanya mengandalkan pendaftaran dependen yang sama. State yang digunakan di seluruh aplikasi juga mendapat tempat yang tepat, yaitu kelas tersendiri di luar pohon widget.
Kesalahan umum#
updateShouldNotifyyang selalu mengembalikanfalse. Dependen tidak pernah dibangun ulang meskipun data berubah. Kembalikan hasil perbandingan data lama dan data baru.- Memanggil
of(context)dengancontextdi atas penyedia. Nilai kembaliannya bernilai null sehingga operator!melempar error. Pastikan widget pemanggil berada di bawah penyedia dalam pohon. - Mengubah isi penyedia tanpa memasang ulang penyedia. Tidak ada pemberitahuan yang dikirim karena Flutter membandingkan instans penyedia, bukan isi fieldnya. Pasang ulang penyedia dengan data baru agar
updateShouldNotifybekerja.
Ringkasan#
- Pola menaikkan state menempatkan state pada leluhur terdekat, data turun melalui konstruktor, dan perubahan naik melalui callback.
InheritedWidgetmenyebarkan data ke seluruh subpohon dan memberi tahu dependen yang terdaftar melaluidependOnInheritedWidgetOfExactType.updateShouldNotifymembandingkan data lama dengan data baru untuk memutuskan rebuild.- Package Provider dibangun di atas mekanisme
InheritedWidgetdengan API yang lebih ringkas.
Referensi#
- InheritedWidget — Flutter
— Dokumentasi resmi
InheritedWidgetbeserta kontrakupdateShouldNotify-nya. - Simple app state management — Flutter — Panduan resmi berbagi state antarwidget hingga menuju package Provider.
- Ephemeral vs app state — Flutter — Kerangka resmi untuk menentukan tempat penyimpanan state yang tepat.