BAB 4 — Fundamental Pengelolaan State pada Aplikasi Flutter. Memahami bagaimana data hidup, mengalir, dan diperbarui di dalam aplikasi.
Gunakan tombol ◀ ▶ atau panah keyboard untuk navigasi
Di bab ini kamu akan belajar bagaimana aplikasi Flutter mengingat dan memperbarui data. Ikuti urutannya dari yang paling dasar sampai perbandingan teknik profesional.
State adalah data yang bisa berubah selama aplikasi berjalan, dan ketika data itu berubah, tampilan (UI) ikut berubah. Contoh: jumlah like, status login, isi keranjang belanja, atau mode gelap/terang.
Lampu punya state: nyala atau mati. Saat kamu menekan saklar, state berubah dan kondisi lampu (UI) ikut berubah. Di Flutter, mengubah state = memerintahkan UI menggambar ulang dirinya.
bool isLampOn = false; // ini adalah STATE
void toggleLamp() {
setState(() {
isLampOn = !isLampOn; // ubah state -> UI digambar ulang
});
}
class Lampu extends StatefulWidget {
@override
State<Lampu> createState() => _LampuState();
}
class _LampuState extends State<Lampu> {
bool isOn = false; // STATE disimpan di sini
@override
Widget build(BuildContext context) {
return Switch(
value: isOn,
onChanged: (v) => setState(() => isOn = v),
);
}
}
Tiap klik memanggil setState(), Flutter menjalankan ulang build() sehingga UI sinkron dengan state terbaru.
Di Flutter, UI tersusun sebagai pohon widget (widget tree). Widget induk membungkus widget anak, membentuk hierarki dari atas (root) ke bawah (daun).
Seperti silsilah keluarga: setiap widget punya induk dan bisa punya banyak anak. Posisi di pohon menentukan dari mana sebuah widget bisa mengambil data.
Setiap level menjorok ke dalam menandakan anak dari widget di atasnya. BuildContext adalah "alamat" posisi widget di dalam pohon ini.
MaterialApp( // root
home: Scaffold( // level 1
appBar: AppBar( // level 2
title: Text('Halo'), // level 3
),
body: Column( // level 2
children: [
Text('Counter:'), // level 3
SizedBox(height: 8), // level 3
ElevatedButton( // level 3
onPressed: () {},
child: Text('Tambah'),
),
],
),
),
)
💡 Aturan penting: data hanya bisa mengalir dari induk ke anak. Anak tidak bisa langsung "naik" mengambil data dari induknya kecuali lewat mekanisme khusus seperti InheritedWidget / Provider.
Di Flutter, data dikirim dari induk ke anak melalui parameter constructor. Ini disebut unidirectional data flow (aliran satu arah) — membuat aplikasi mudah dilacak dan diprediksi.
Seperti air terjun: air (data) mengalir dari kolam paling atas ke bawah, tidak pernah naik ke atas dengan sendirinya. Klik tombol untuk mengalirkan data.
Rebuild adalah proses Flutter menjalankan ulang method build() untuk menggambar UI sesuai state terbaru. Memanggil setState() memicu rebuild pada widget tersebut dan anak-anaknya.
setState() → build() → Render ulang widget! Setiap blok di-render ulang. Karena itu, taruh setState sedekat mungkin dengan widget yang benar-benar berubah agar tidak boros.
setState() berkata kepada Flutter: "Datanya berubah, tolong gambar ulang!". Kamu mengubah variabel di dalam callback setState, lalu Flutter menjadwalkan rebuild.
Mengubah variabel saja seperti menulis pesanan tapi tidak membunyikan bel — dapur (Flutter) tidak tahu. setState() adalah bel yang memberi tahu Flutter untuk memproses ulang.
class _CounterState extends State<Counter> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('$_count', style: TextStyle(fontSize: 48)),
ElevatedButton(
onPressed: _increment,
child: Text('Tambah'),
),
],
),
),
);
}
}
InheritedWidget menyimpan data di atas pohon, lalu semua keturunannya bisa mengaksesnya langsung tanpa mengoper data lewat setiap constructor (menghindari prop drilling).
Kakek memegang "warna tema". Tanpa harus dititip lewat ayah lalu anak, cucu bisa langsung mengakses warisan kakek. Klik untuk melihat tiap generasi mengakses data.
class ThemeData extends InheritedWidget {
final Color primaryColor;
const ThemeData({
Key? key,
required this.primaryColor,
required Widget child,
}) : super(key: key, child: child);
// Cara cucu mengakses data kakek:
static ThemeData of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<ThemeData>()!;
}
@override
bool updateShouldNotify(ThemeData oldWidget) {
return primaryColor != oldWidget.primaryColor;
}
}
🔑 context.dependOnInheritedWidgetOfExactType<ThemeData>() = cara widget mana pun di bawah pohon "mendaftar" untuk mendapat data, dan otomatis di-rebuild saat data berubah (jika updateShouldNotify bernilai true).
InheritedWidget adalah fondasi dari hampir semua state management modern. Provider, misalnya, dibangun di atas InheritedWidget agar lebih mudah digunakan.
Provider membungkus InheritedWidget agar lebih ringkas. Sebuah ChangeNotifier menyimpan data; saat data berubah, ia memanggil notifyListeners() dan semua Consumer otomatis rebuild.
Toko (Provider) mengumumkan stok baru lewat notifyListeners(). Semua pelanggan (Consumer) yang berlangganan langsung tahu dan memperbarui diri. Klik untuk update.
// ChangeNotifier
class ThemeProvider extends ChangeNotifier {
bool _isDark = false;
bool get isDark => _isDark;
void toggleTheme() {
_isDark = !_isDark;
notifyListeners(); // beri tahu semua Consumer
}
}
// Setup di main
ChangeNotifierProvider(
create: (_) => ThemeProvider(),
child: MaterialApp(home: MyHomePage()),
)
// Konsumsi di widget
Consumer<ThemeProvider>(
builder: (context, theme, child) {
return Container(
color: theme.isDark ? Colors.black : Colors.white,
);
},
)
3 widget berlangganan ke ThemeProvider. Satu klik mengubah semuanya serentak — tanpa setState di tiap widget.
BLoC memisahkan logika dari UI menggunakan aliran: EVENT masuk → diproses BLoC → menghasilkan STATE baru. Cocok untuk aplikasi besar yang butuh struktur rapi dan mudah dites.
Seperti pabrik: bahan mentah (Event) masuk ke mesin (BLoC), diolah, lalu keluar produk jadi (State). UI hanya menampilkan produk akhir. Klik untuk menjalankan mesin.
// Event
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
// State
class CounterState {
final int count;
CounterState(this.count);
}
// BLoC
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterState(0)) {
on<IncrementEvent>((event, emit) {
emit(CounterState(state.count + 1));
});
}
}
Riverpod menyempurnakan Provider: tidak butuh BuildContext untuk mengakses data, lebih aman secara tipe (type-safe), dan mudah dites. Widget memakai ref.watch() untuk "menonton" sebuah provider.
Satu satelit (provider) menyiarkan data; semua penerima (consumer) yang menonton (ref.watch) langsung update saat siaran berubah. Klik untuk menyiarkan.
// Definisi provider
final counterProvider =
StateNotifierProvider<CounterNotifier, int>(
(ref) => CounterNotifier(),
);
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
void decrement() => state--;
}
// Konsumsi
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}
🔑 ref.watch(counterProvider) = menonton provider, widget otomatis rebuild saat nilainya berubah. ref.read(...) dipakai saat hanya ingin membaca/memanggil method tanpa menonton.
Tidak ada yang "paling benar" — pilih sesuai kebutuhan proyek.
| Kriteria | setState | Provider | BLoC | Riverpod |
|---|---|---|---|---|
| Kemudahan Pakai | ★★★★★ | ★★★★☆ | ★★☆☆☆ | ★★★★☆ |
| Skalabilitas | ★☆☆☆☆ | ★★★☆☆ | ★★★★★ | ★★★★★ |
| Boilerplate | Sedikit | Sedang | Banyak | Sedang |
| Testing | ★★☆☆☆ | ★★★☆☆ | ★★★★★ | ★★★★★ |
| Popularitas | ★★★☆☆ | ★★★★★ | ★★★★☆ | ★★★★☆ |
Gunakan diagram ini sebagai panduan cepat memilih teknik state management sesuai skala dan kebutuhan aplikasimu.
Buat aplikasi counter sederhana dengan Flutter menggunakan setState. Aplikasi menampilkan angka dan bisa ditambah/dikurangi serta direset.
| Kriteria | Poin |
|---|---|
| Counter tambah/kurang berfungsi & tidak negatif | 30 |
| Tombol reset berfungsi dengan benar | 40 |
| Pesan dinamis sesuai nilai counter | 30 |
Total: 100 poin
Gunakan satu variabel int _count = 0; di dalam class State. Bungkus setiap perubahan di dalam setState(() {...}). Untuk mencegah negatif, cek if (_count > 0) _count--;.
Buat aplikasi Flutter dengan tema dark/light yang menggunakan Provider. Saat tema diganti, beberapa widget berubah bersamaan.
ChangeNotifier.notifyListeners()).| Kriteria | Poin |
|---|---|
| ThemeProvider (ChangeNotifier) benar | 30 |
| Toggle tema berfungsi via Provider | 40 |
| Minimal 3 widget berubah serentak | 30 |
Total: 100 poin
Bungkus aplikasi dengan ChangeNotifierProvider di atas MaterialApp. Gunakan Consumer<ThemeProvider> atau context.watch<ThemeProvider>() pada widget yang perlu ikut berubah.
Implementasikan state management untuk sebuah aplikasi mini pilihanmu sendiri (misal: to-do list, catatan, kuis, atau keranjang belanja sederhana).
| Kriteria | Poin |
|---|---|
| Ketepatan implementasi state management | 40 |
| Kreativitas fitur aplikasi | 30 |
| Kualitas kode (bersih & terdokumentasi) | 20 |
| Presentasi & penjelasan | 10 |
Total: 100 poin
Mulai dari yang sederhana: rancang dulu data apa yang menjadi state, lalu tentukan screen mana yang membaca/mengubahnya. Gunakan navigasi Navigator.push antar screen, dan letakkan state di atas agar bisa dibagikan.