Tampilan
Bab 17 — Pengantar State Management
Kamu sudah tahu cara memakai setState. Untuk satu layar, itu cukup. Bab ini menjelaskan kapan ia berhenti cukup, kenapa, dan apa pilihanmu setelah itu.
Masalah yang sebenarnya
Bayangkan aplikasi resep makanan. Ada layar daftar kategori, layar daftar menu, layar detail menu, dan layar favorit. Pengguna menandai sebuah menu sebagai favorit dari layar detail — dan layar favorit harus segera mengetahuinya.
Dengan setState saja, satu-satunya cara adalah mengangkat state ke leluhur bersama dan mengoper fungsi ke bawah:
LayarTab
(menyimpan _favorit)
│
┌───────────────┴───────────────┐
│ │
LayarKategori LayarFavorit
+ fungsi toggle + daftar favorit
│
LayarMeal
+ fungsi toggle ← hanya meneruskan, tidak memakainya
│
LayarDetailMeal
+ fungsi toggle ← akhirnya dipakai di siniKodenya terlihat seperti ini:
dart
// LayarTab — pemilik state
class _LayarTabState extends State<LayarTab> {
final List<Meal> _mealFavorit = [];
void _toggleFavorit(Meal meal) {
final sudahAda = _mealFavorit.contains(meal);
setState(() {
if (sudahAda) {
_mealFavorit.remove(meal);
} else {
_mealFavorit.add(meal);
}
});
}
@override
Widget build(BuildContext context) {
return LayarKategori(saatToggleFavorit: _toggleFavorit);
}
}
// LayarKategori — tidak memakainya, hanya meneruskan
class LayarKategori extends StatelessWidget {
const LayarKategori({super.key, required this.saatToggleFavorit});
final void Function(Meal) saatToggleFavorit;
// ... meneruskan ke LayarMeal
}
// LayarMeal — juga hanya meneruskan
class LayarMeal extends StatelessWidget {
const LayarMeal({super.key, required this.saatToggleFavorit});
final void Function(Meal) saatToggleFavorit;
// ... meneruskan ke LayarDetailMeal
}
// LayarDetailMeal — akhirnya memakainya
class LayarDetailMeal extends StatelessWidget {
const LayarDetailMeal({super.key, required this.saatToggleFavorit});
final void Function(Meal) saatToggleFavorit;
// ... akhirnya dipanggil di sini
}Ini disebut prop drilling, dan masalahnya bukan sekadar jumlah baris:
- Widget di tengah dipaksa peduli pada sesuatu yang tidak ada hubungannya dengan mereka.
LayarKategoritidak punya urusan dengan favorit. - Setiap perubahan merambat. Menambah satu data baru berarti mengubah konstruktor di empat tempat.
- Rebuild yang terlalu luas.
setStatediLayarTabmembangun ulang seluruh cabang di bawahnya, termasuk yang tidak peduli. - Sulit diuji. Untuk menguji
LayarDetailMeal, kamu harus menyediakan seluruh rantai fungsi palsunya.
Dua jenis state
Sebelum memilih solusi, penting membedakan dua jenis state — karena tidak semuanya butuh perkakas berat.
State lokal (ephemeral). Hanya satu widget yang peduli. Kalau hilang, tidak ada kerugian berarti.
- Apakah kata sandi sedang disembunyikan
- Tab mana yang aktif
- Isi kolom teks sebelum dikirim
- Apakah sebuah panel sedang terbuka
- Posisi animasi
State aplikasi (app state). Banyak bagian aplikasi peduli, dan sering perlu bertahan.
- Pengguna yang sedang masuk
- Isi keranjang belanja
- Daftar favorit
- Preferensi tema
- Data dari server
┌────────────────────────────────────────────────────────────┐
│ STATE LOKAL → setState() sudah cukup │
│ Satu widget peduli │
│ Contoh: kolom teks, tab aktif, panel terbuka │
└────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────┐
│ STATE APLIKASI → butuh state management │
│ Banyak widget peduli │
│ Contoh: login, keranjang, favorit, tema │
└────────────────────────────────────────────────────────────┘Jangan berlebihan
Kesalahan umum pemula setelah belajar Riverpod atau BLoC adalah memindahkan semua state ke sana, termasuk yang lokal. Ini membuat kode lebih rumit tanpa manfaat. Kalau hanya satu widget yang peduli, setState tetap jawaban yang benar — dan tetap begitu bahkan di aplikasi besar.
Apa yang sebenarnya dilakukan state management
Semua solusi state management pada dasarnya menyelesaikan tiga hal yang sama:
1. Menyimpan state di luar widget tree. Sehingga ia tidak terikat pada posisi widget mana pun dan tidak ikut hilang saat widget dilepas.
2. Menyediakan akses langsung dari mana saja. Widget yang butuh data mengambilnya sendiri, tanpa harus dioper turun melalui perantara.
3. Memberi tahu hanya yang berlangganan. Ketika state berubah, hanya widget yang benar-benar memakainya yang dibangun ulang.
TANPA state management DENGAN state management
────────────────────── ───────────────────────
LayarTab ┌──────────┐
(punya state) │ STATE │
│ │ (di │
┌──────┴──────┐ │ luar │
│ │ │ pohon) │
Kategori Favorit └────┬─────┘
│ ┌────┴────┬─────────┐
Meal ← meneruskan │ │ │
│ Kategori Favorit Detail
Detail ← akhirnya dipakai (langganan)(langganan)(langganan)Peta pilihan
Ekosistem Flutter punya banyak pilihan. Berikut yang paling relevan.
| Solusi | Kompleksitas | Cocok untuk |
|---|---|---|
setState | Sangat rendah | State lokal, satu widget |
InheritedWidget | Sedang | Fondasi; jarang dipakai langsung |
Provider | Rendah | Aplikasi kecil-menengah, tim yang sudah familier |
Riverpod | Rendah–sedang | Sebagian besar aplikasi baru |
BLoC | Sedang–tinggi | Aplikasi besar, tim besar, logika kompleks |
GetX | Rendah | Prototipe cepat; kontroversial di komunitas |
MobX, Redux | Sedang | Tim yang berpengalaman dengannya |
Tiga yang dibahas tuntas di situs ini adalah Riverpod, Provider, dan BLoC, karena ketiganya yang paling banyak dipakai di dunia nyata dan mencakup ketiganya memberimu kemampuan membaca hampir semua basis kode Flutter.
InheritedWidget: fondasi semuanya
Sebelum melihat paket, ada baiknya tahu mekanisme bawaan Flutter yang mendasari semuanya — karena Provider, Riverpod, dan flutter_bloc semuanya dibangun di atasnya.
InheritedWidget memungkinkan widget di bawah mengakses data dari widget di atas tanpa dioper lewat konstruktor.
dart
class PenyediaTema extends InheritedWidget {
const PenyediaTema({
super.key,
required this.modeGelap,
required super.child,
});
final bool modeGelap;
/// Cara widget di bawah mengaksesnya.
static PenyediaTema? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<PenyediaTema>();
}
/// Menentukan apakah widget yang berlangganan perlu dibangun ulang.
@override
bool updateShouldNotify(PenyediaTema oldWidget) {
return modeGelap != oldWidget.modeGelap;
}
}Memakainya:
dart
// Di atas pohon
PenyediaTema(
modeGelap: true,
child: MaterialApp(home: const Beranda()),
)
// Di mana pun di bawahnya — tanpa dioper
final gelap = PenyediaTema.of(context)!.modeGelap;Inilah mekanisme yang sama dipakai oleh Theme.of(context), MediaQuery.of(context), dan Navigator.of(context) — pola .of(context) yang sudah kamu pakai sejak Bab 3.
Kamu jarang menulis InheritedWidget sendiri karena ia hanya menyediakan akses, bukan perubahan. Paket-paket state management menambahkan lapisan itu di atasnya.
Riverpod versus BLoC: bagaimana memilih
Ini pertanyaan yang paling sering muncul, jadi mari dijawab langsung.
Pilih Riverpod kalau:
- Kamu mengerjakan proyek sendiri atau tim kecil.
- Kamu ingin sedikit kode boilerplate.
- Aplikasimu kecil sampai menengah.
- Kamu banyak berurusan dengan pengambilan data asinkron — Riverpod menangani loading dan error dengan sangat rapi.
Pilih BLoC kalau:
- Tim kamu besar dan butuh pola yang seragam serta ketat.
- Logika bisnismu kompleks dengan banyak transisi keadaan.
- Kamu butuh riwayat kejadian yang bisa dilacak untuk debugging.
- Pengujian adalah prioritas tinggi — BLoC sangat mudah diuji.
Pilih Provider kalau:
- Kamu bergabung ke proyek yang sudah memakainya.
- Kamu butuh sesuatu yang sangat sederhana dan sudah dikenal luas.
Tidak ada jawaban tunggal yang benar
Aplikasi produksi yang baik sering memakai kombinasi: setState untuk state lokal, dan satu solusi state management untuk state aplikasi. Yang berbahaya bukan memilih yang "salah", melainkan mencampur tiga solusi berbeda dalam satu proyek tanpa aturan yang jelas.
Prinsip yang berlaku di semua solusi
Apa pun yang kamu pilih, tiga prinsip berikut berlaku dan akan menyelamatkanmu dari bug yang membingungkan.
1. State harus tidak berubah (immutable)
Ini yang paling sering dilanggar dan paling membingungkan ketika salah.
dart
// ❌ Mengubah list yang sama — perubahan tidak terdeteksi
void tambahFavorit(Meal meal) {
state.add(meal); // objek yang sama, alamat yang sama
}
// ✅ Membuat list baru — perubahan terdeteksi
void tambahFavorit(Meal meal) {
state = [...state, meal];
}Kenapa? Karena solusi state management membandingkan state lama dan baru untuk memutuskan apakah perlu memberi tahu pendengarnya. Kalau alamat objeknya sama, mereka menyimpulkan tidak ada yang berubah — meskipun isinya berbeda.
Ini penerapan langsung dari apa yang dibahas di Bab 12 tentang final yang mengunci alamat, bukan isi.
Pola-pola yang benar:
dart
// Menambah ke list
state = [...state, itemBaru];
// Menghapus dari list
state = state.where((i) => i.id != id).toList();
// Mengganti satu elemen
state = [
for (final i in state) i.id == id ? i.copyWith(selesai: true) : i,
];
// Mengubah sebagian objek
state = state.copyWith(hitungan: state.hitungan + 1);2. Pisahkan logika dari tampilan
Widget seharusnya hanya menggambar dan meneruskan interaksi. Perhitungan, validasi bisnis, dan panggilan jaringan tidak seharusnya berada di dalam build().
┌─────────────────────────────────────────────┐
│ PRESENTATION (widget) │
│ Menampilkan state, mengirim intent │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ BUSINESS LOGIC (Notifier / Bloc / Cubit) │
│ Aturan bisnis, transisi keadaan │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ DATA (Repository / Service) │
│ HTTP, basis data, cache │
└─────────────────────────────────────────────┘Arsitektur berlapis ini dibahas tuntas di Bab 22 dan Bab 33.
3. Berlangganan sesempit mungkin
Semakin sedikit yang kamu dengarkan, semakin sedikit yang dibangun ulang.
dart
// ❌ Dibangun ulang setiap kali APA PUN di keranjang berubah
final keranjang = ref.watch(keranjangProvider);
return Text('${keranjang.items.length}');
// ✅ Dibangun ulang hanya kalau jumlahnya yang berubah
final jumlah = ref.watch(keranjangProvider.select((k) => k.items.length));
return Text('$jumlah');Setiap solusi punya mekanismenya sendiri: select di Riverpod, Selector di Provider, buildWhen di BLoC.
Peta perjalanan Bagian III
Kalau waktumu terbatas dan kamu hanya ingin satu solusi, baca Bab 18 (Riverpod) lalu langsung lompat ke Bagian IV. Rangkaian BLoC bisa kamu kembali lagi nanti ketika kamu bergabung ke proyek yang memakainya.
Ikhtisar
- Masalah utamanya adalah prop drilling: mengoper data lewat widget yang tidak memakainya, yang membuat kode rapuh dan rebuild terlalu luas.
- Bedakan state lokal (satu widget peduli —
setStatecukup) dari state aplikasi (banyak bagian peduli — butuh state management). - Jangan memindahkan semua state ke Riverpod atau BLoC.
setStatetetap jawaban yang benar untuk state lokal, bahkan di aplikasi besar. - Semua solusi melakukan tiga hal yang sama: menyimpan state di luar pohon, memberi akses langsung, dan memberi tahu hanya yang berlangganan.
InheritedWidgetadalah fondasi bawaan Flutter yang mendasariTheme.of(),MediaQuery.of(), dan semua paket state management.- Riverpod untuk sebagian besar proyek baru; BLoC untuk tim besar dan logika kompleks; Provider kalau proyekmu sudah memakainya.
- State harus immutable. Buat objek baru (
[...state, item],state.copyWith(...)), jangan mengubah yang lama — kalau tidak, perubahan tidak terdeteksi. - Pisahkan presentation, business logic, dan data ke lapisan yang berbeda.
- Berlangganan sesempit mungkin:
select(Riverpod),Selector(Provider),buildWhen(BLoC).
Berikutnya: Bab 18 — Riverpod.
Transkrip asli
Disintesis dari 1_flutter_core/8_meals-app_managing-app-wide-state.md (video 1–4), 1_flutter_zero-to-hero/12_next-steps.md ("Let's talk about State Management"), 2_flutter_bloc-zero-to-hero/1_why-bloc-as-a-state-management.md, dan 4_flutter_ai-chatbot-n-firebase/5_state-management-with-provider.md (video 1).