Tampilan
Bab 19 — Provider
Provider adalah paket state management yang mendahului Riverpod — dibuat oleh pengembang yang sama. Ia masih sangat banyak dipakai di proyek yang sudah berjalan, dan memahaminya penting kalau kamu bergabung ke tim yang memakainya.
Bab ini lebih ringkas daripada Riverpod, karena banyak konsepnya serupa. Fokus kita adalah perbedaannya.
Memasang
yaml
# pubspec.yaml
dependencies:
provider: ^6.1.2ChangeNotifier: fondasinya
Berbeda dari Riverpod yang memakai StateNotifier, Provider paling sering dipasangkan dengan ChangeNotifier — kelas bawaan Flutter yang bisa memberi tahu pendengarnya ketika sesuatu berubah.
dart
// lib/providers/penyedia_polling.dart
import 'package:flutter/material.dart';
import '../models/polling.dart';
import '../services/layanan_polling.dart';
class PenyediaPolling extends ChangeNotifier {
PenyediaPolling(this._layanan);
final LayananPolling _layanan;
List<Polling> _daftarPolling = [];
bool _sedangMemuat = false;
String? _pesanKesalahan;
// Getter publik — widget hanya bisa membaca, tidak mengubah langsung
List<Polling> get daftarPolling => List.unmodifiable(_daftarPolling);
bool get sedangMemuat => _sedangMemuat;
String? get pesanKesalahan => _pesanKesalahan;
int get jumlahPolling => _daftarPolling.length;
Future<void> muatPolling() async {
_sedangMemuat = true;
_pesanKesalahan = null;
notifyListeners(); // ← beri tahu UI: mulai memuat
try {
_daftarPolling = await _layanan.ambilSemua();
} catch (kesalahan) {
_pesanKesalahan = 'Gagal memuat polling. Coba lagi.';
} finally {
_sedangMemuat = false;
notifyListeners(); // ← beri tahu UI: selesai
}
}
Future<void> tambahPolling(Polling polling) async {
final tersimpan = await _layanan.simpan(polling);
_daftarPolling = [tersimpan, ..._daftarPolling];
notifyListeners();
}
Future<void> hapusPolling(String id) async {
// Optimistic update — hapus dulu, kembalikan kalau gagal
final cadangan = _daftarPolling;
_daftarPolling = _daftarPolling.where((p) => p.id != id).toList();
notifyListeners();
try {
await _layanan.hapus(id);
} catch (_) {
_daftarPolling = cadangan;
_pesanKesalahan = 'Gagal menghapus polling.';
notifyListeners();
}
}
Future<void> beriSuara(String idPolling, int indeksOpsi) async {
final indeks = _daftarPolling.indexWhere((p) => p.id == idPolling);
if (indeks == -1) return;
final polling = _daftarPolling[indeks];
final diperbarui = polling.dengansuara(indeksOpsi);
// Buat list baru dengan satu elemen diganti
_daftarPolling = [
for (final p in _daftarPolling) p.id == idPolling ? diperbarui : p,
];
notifyListeners();
await _layanan.perbarui(diperbarui);
}
}Perbedaan mendasar dari StateNotifier:
ChangeNotifier (Provider) | StateNotifier (Riverpod) | |
|---|---|---|
| Cara memberi tahu | notifyListeners() manual | Otomatis saat state = ... |
| Struktur state | Banyak properti terpisah | Satu objek state |
| Mutabilitas | Properti boleh diubah langsung | Harus ganti seluruh objek |
ChangeNotifier lebih longgar — kamu bisa mengubah properti langsung, asal ingat memanggil notifyListeners(). Kelonggaran itu sekaligus jebakannya: lupa memanggil notifyListeners() adalah bug nomor satu di Provider, dan tidak ada error apa pun yang muncul — UI hanya diam saja.
Kenapa List.unmodifiable
Getter daftarPolling mengembalikan salinan yang tidak bisa diubah. Tanpa itu, widget bisa memanggil penyedia.daftarPolling.add(...) dan mengubah state tanpa memicu notifyListeners() — menghasilkan bug yang sangat sulit dilacak.
Mendaftarkan provider
dart
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/penyedia_polling.dart';
import 'providers/penyedia_auth.dart';
import 'providers/penyedia_tema.dart';
import 'services/layanan_polling.dart';
void main() {
runApp(const AplikasiSaya());
}
class AplikasiSaya extends StatelessWidget {
const AplikasiSaya({super.key});
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
// Provider biasa — untuk objek yang tidak berubah
Provider<LayananPolling>(
create: (_) => LayananPolling(),
),
// ChangeNotifierProvider — untuk state yang berubah
ChangeNotifierProvider<PenyediaAuth>(
create: (_) => PenyediaAuth(),
),
ChangeNotifierProvider<PenyediaTema>(
create: (_) => PenyediaTema(),
),
// ProxyProvider — provider yang bergantung pada provider lain
ChangeNotifierProxyProvider<LayananPolling, PenyediaPolling>(
create: (ctx) => PenyediaPolling(
ctx.read<LayananPolling>(),
),
update: (ctx, layanan, sebelumnya) =>
sebelumnya ?? PenyediaPolling(layanan),
),
],
child: Consumer<PenyediaTema>(
builder: (context, tema, _) {
return MaterialApp(
title: 'Aplikasi Polling',
theme: TemaAplikasi.terang(),
darkTheme: TemaAplikasi.gelap(),
themeMode: tema.mode,
home: const LayarBeranda(),
);
},
),
);
}
}Beberapa hal yang perlu diperhatikan:
MultiProvider menghindari widget yang bersarang berlapis-lapis. Tanpa itu, kamu akan menulis ChangeNotifierProvider di dalam ChangeNotifierProvider di dalam Provider.
Urutan penting. Provider yang bergantung pada provider lain harus dideklarasikan setelah yang menjadi ketergantungannya.
ChangeNotifierProxyProvider untuk provider yang butuh provider lain. Parameter update dipanggil setiap kali ketergantungannya berubah; mengembalikan sebelumnya ?? ... mencegah pembuatan ulang yang tidak perlu.
Membaca provider
Ada beberapa cara, dan memilih yang tepat sama pentingnya seperti di Riverpod.
context.watch — berlangganan
dart
@override
Widget build(BuildContext context) {
final penyedia = context.watch<PenyediaPolling>();
if (penyedia.sedangMemuat) {
return const Center(child: CircularProgressIndicator());
}
return ListView.builder(
itemCount: penyedia.daftarPolling.length,
itemBuilder: (ctx, i) => KartuPolling(polling: penyedia.daftarPolling[i]),
);
}Widget ini akan dibangun ulang setiap kali notifyListeners() dipanggil.
context.read — baca sekali
dart
ElevatedButton(
onPressed: () {
context.read<PenyediaPolling>().muatPolling();
},
child: const Text('Muat Ulang'),
)Sama seperti Riverpod: watch di build, read di callback.
Jangan read di build, jangan watch di callback
dart
// ❌ UI tidak akan pernah diperbarui
final penyedia = context.read<PenyediaPolling>();
// ❌ Error: watch dipanggil di luar build
onPressed: () => context.watch<PenyediaPolling>().muat(),context.select — berlangganan sebagian
dart
// Hanya dibangun ulang kalau jumlahnya berubah,
// bukan setiap kali isi daftar berubah
final jumlah = context.select<PenyediaPolling, int>(
(p) => p.daftarPolling.length,
);
return Text('$jumlah polling');Consumer — membatasi cakupan rebuild
dart
class LayarBeranda extends StatelessWidget {
const LayarBeranda({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
// AppBar ini tidak pernah dibangun ulang
appBar: AppBar(
title: const Text('Polling'),
actions: [
// Hanya lencana ini yang berlangganan
Consumer<PenyediaPolling>(
builder: (context, penyedia, child) {
return Badge(
label: Text('${penyedia.jumlahPolling}'),
child: child,
);
},
child: const Icon(Icons.poll),
),
],
),
// Hanya body ini yang berlangganan
body: Consumer<PenyediaPolling>(
builder: (context, penyedia, _) {
if (penyedia.sedangMemuat) {
return const Center(child: CircularProgressIndicator());
}
if (penyedia.pesanKesalahan != null) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(penyedia.pesanKesalahan!),
const SizedBox(height: 12),
ElevatedButton(
onPressed: penyedia.muatPolling,
child: const Text('Coba Lagi'),
),
],
),
);
}
if (penyedia.daftarPolling.isEmpty) {
return const Center(child: Text('Belum ada polling.'));
}
return RefreshIndicator(
onRefresh: penyedia.muatPolling,
child: ListView.builder(
itemCount: penyedia.daftarPolling.length,
itemBuilder: (ctx, i) => KartuPolling(
polling: penyedia.daftarPolling[i],
saatBeriSuara: (indeksOpsi) => context
.read<PenyediaPolling>()
.beriSuara(penyedia.daftarPolling[i].id, indeksOpsi),
),
),
);
},
),
// FAB ini juga tidak pernah dibangun ulang
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const LayarBuatPolling()),
),
child: const Icon(Icons.add),
),
);
}
}Parameter child pada Consumer bekerja sama seperti di AnimatedBuilder: ia dibuat sekali dan diteruskan tanpa dibangun ulang.
Selector — kendali paling halus
dart
Selector<PenyediaPolling, int>(
selector: (context, penyedia) => penyedia.jumlahPolling,
builder: (context, jumlah, child) {
return Text('$jumlah polling');
},
)Selector hanya membangun ulang ketika nilai yang dipilih benar-benar berubah, dibandingkan dengan ==.
Memuat data saat layar dibuka
Ini titik yang sering menimbulkan kebingungan. Kamu tidak boleh memanggil notifyListeners() selama fase pembangunan widget.
dart
class LayarPolling extends StatefulWidget {
const LayarPolling({super.key});
@override
State<LayarPolling> createState() => _LayarPollingState();
}
class _LayarPollingState extends State<LayarPolling> {
@override
void initState() {
super.initState();
// ❌ Error: notifyListeners dipanggil saat widget sedang dibangun
// context.read<PenyediaPolling>().muatPolling();
// ✅ Tunda sampai frame pertama selesai
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<PenyediaPolling>().muatPolling();
});
}
@override
Widget build(BuildContext context) {
return Consumer<PenyediaPolling>(
builder: (context, penyedia, _) {
// ...
},
);
}
}Alternatif yang lebih bersih
Beberapa tim lebih memilih memuat data di dalam konstruktor provider:
dart
class PenyediaPolling extends ChangeNotifier {
PenyediaPolling(this._layanan) {
muatPolling(); // langsung muat saat dibuat
}
// ...
}Ini menghilangkan kebutuhan addPostFrameCallback sama sekali, dan membuat widget tidak perlu tahu apa-apa tentang pemuatan data.
FutureProvider dan StreamProvider
Provider juga punya padanan untuk data asinkron:
dart
MultiProvider(
providers: [
FutureProvider<List<Polling>>(
create: (ctx) => ctx.read<LayananPolling>().ambilSemua(),
initialData: const [],
),
StreamProvider<User?>(
create: (_) => FirebaseAuth.instance.authStateChanges(),
initialData: null,
),
],
child: const AplikasiSaya(),
)dart
final daftar = context.watch<List<Polling>>();
final pengguna = context.watch<User?>();Keduanya butuh initialData, dan tidak menyediakan penanganan loading/error sebaik AsyncValue milik Riverpod. Untuk data asinkron yang kompleks, banyak tim tetap memakai FutureBuilder bersama ChangeNotifier, atau beralih ke Riverpod.
Contoh: penyedia autentikasi
Pola yang sangat umum di aplikasi nyata.
dart
// lib/providers/penyedia_auth.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
enum StatusAuth { belumDitentukan, masuk, keluar }
class PenyediaAuth extends ChangeNotifier {
PenyediaAuth() {
// Dengarkan perubahan status login dari Firebase
_langganan = FirebaseAuth.instance.authStateChanges().listen((pengguna) {
_pengguna = pengguna;
_status = pengguna == null ? StatusAuth.keluar : StatusAuth.masuk;
notifyListeners();
});
}
StreamSubscription<User?>? _langganan;
User? _pengguna;
StatusAuth _status = StatusAuth.belumDitentukan;
bool _sedangProses = false;
String? _pesanKesalahan;
User? get pengguna => _pengguna;
StatusAuth get status => _status;
bool get sedangProses => _sedangProses;
String? get pesanKesalahan => _pesanKesalahan;
bool get sudahMasuk => _pengguna != null;
Future<bool> masuk(String email, String sandi) async {
_sedangProses = true;
_pesanKesalahan = null;
notifyListeners();
try {
await FirebaseAuth.instance.signInWithEmailAndPassword(
email: email.trim(),
password: sandi,
);
return true;
} on FirebaseAuthException catch (e) {
_pesanKesalahan = switch (e.code) {
'user-not-found' => 'Akun tidak ditemukan.',
'wrong-password' => 'Kata sandi salah.',
'invalid-email' => 'Format email tidak valid.',
'invalid-credential' => 'Email atau kata sandi salah.',
'too-many-requests' => 'Terlalu banyak percobaan. Coba lagi nanti.',
_ => 'Gagal masuk. Coba lagi.',
};
return false;
} finally {
_sedangProses = false;
notifyListeners();
}
}
Future<void> keluar() async {
await FirebaseAuth.instance.signOut();
// _pengguna diperbarui otomatis lewat authStateChanges
}
@override
void dispose() {
_langganan?.cancel(); // ⚠️ wajib, kalau tidak memori bocor
super.dispose();
}
}Dipakai untuk mengatur layar mana yang tampil:
dart
class GerbangAuth extends StatelessWidget {
const GerbangAuth({super.key});
@override
Widget build(BuildContext context) {
final auth = context.watch<PenyediaAuth>();
return switch (auth.status) {
StatusAuth.belumDitentukan => const LayarSplash(),
StatusAuth.masuk => const LayarBeranda(),
StatusAuth.keluar => const LayarMasuk(),
};
}
}Perhatikan dispose() yang membatalkan langganan stream. ChangeNotifier punya dispose() sendiri yang dipanggil ketika provider dibuang, dan melewatkannya adalah penyebab kebocoran memori yang umum.
Provider versus Riverpod
Sekarang setelah melihat keduanya, berikut perbandingannya secara langsung.
| Aspek | Provider | Riverpod |
|---|---|---|
Butuh BuildContext | Ya | Tidak |
| Kesalahan tipe terdeteksi | Saat runtime | Saat kompilasi |
| Provider ganda dengan tipe sama | Sulit | Mudah |
| Testing | Butuh widget tree | Bisa tanpa widget |
| Provider bergantung provider | ProxyProvider (verbose) | ref.watch (alami) |
| Penanganan async | Manual | AsyncValue bawaan |
| Boilerplate | Sedikit lebih sedikit | Sedikit lebih banyak |
| Kematangan | Sangat matang | Matang, aktif dikembangkan |
Kelemahan terbesar Provider yang diselesaikan Riverpod:
dart
// Provider: kalau tipe tidak terdaftar, error baru muncul saat runtime
final penyedia = context.watch<PenyediaYangLupaDidaftarkan>();
// 💥 ProviderNotFoundException saat aplikasi berjalan
// Riverpod: provider adalah variabel, compiler menangkap kesalahannya
final nilai = ref.watch(providerYangSalahKetik);
// ❌ Error saat kompilasi — nama tidak dikenalJuga, Provider tidak bisa punya dua provider dengan tipe yang sama:
dart
// ❌ Provider mana yang diambil? Tidak jelas.
Provider<String>(create: (_) => 'nama'),
Provider<String>(create: (_) => 'alamat'),Di Riverpod ini bukan masalah, karena setiap provider adalah variabel dengan nama sendiri.
Kapan tetap memakai Provider
- Proyekmu sudah memakainya dan bekerja dengan baik. Migrasi besar-besaran jarang sepadan.
- Tim kamu sudah terbiasa dan tidak ingin belajar API baru.
- Aplikasimu sederhana dan
ChangeNotifiersudah cukup.
Untuk proyek baru, sebagian besar rekomendasi saat ini mengarah ke Riverpod atau BLoC.
Latihan Mandiri
Kerjakan salah satu, beberapa, atau semuanya secara berurutan untuk melatih pemahamanmu sampai benar-benar lekat.
Variasi 1: Daftar Belanja dengan ChangeNotifier — ⭐⭐ · 40–60 menit
Tantangan: Ambil latihan daftar belanja dari bab sebelumnya — atau buat ulang dari nol — lalu implementasikan dengan Provider dan ChangeNotifier. Setelah selesai, tulis satu paragraf untuk dirimu sendiri: apa yang terasa lebih mudah, dan apa yang terasa lebih rawan salah dibandingkan Riverpod.
Bayangan tampilan:
text
╭───────────────────────────────────╮
│ 9:41 ▂▄▆ 🔋 │
│ ┌─────────────────────────────┐ │
│ │ ☐ Susu │ │
│ │ ☑ Roti │ │
│ │ ☐ Telur │ │
│ └─────────────────────────────┘ │
│ 2 dari 3 sudah dibeli │
│ ▬▬▬▬ │
╰───────────────────────────────────╯Sengaja hapus satu notifyListeners() → centang di atas berubah di data, tapi tampilannya diam saja (tidak error, tidak peringatan).
Kriteria selesai:
ChangeNotifiermemanggilnotifyListeners()di setiap tempat state berubah — dan kamu sudah membuktikannya dengan sengaja menghapus satu panggilan lalu mengamati apa yang terjadi.- Getter daftar mengembalikan
List.unmodifiable, sehingga widget tidak bisa mengubah state diam-diam. - Provider didaftarkan lewat
MultiProvider, dancontext.watchhanya dipakai dibuildsementaracontext.readhanya di callback. - Ringkasan "sekian dari sekian" memakai
Selectorataucontext.select, bukanwatchpada seluruh objek.
Petunjuk: Kriteria pertama adalah inti latihan ini. Menghapus satu notifyListeners() tidak menghasilkan peringatan, tidak menghasilkan error, dan kodenya tetap terlihat benar — UI hanya diam. Rasakan sekali, dan kamu akan selamanya ingat kenapa Riverpod menghilangkan langkah ini. Kalau kamu mendapat error tentang notifyListeners yang dipanggil saat widget sedang dibangun, itu biasanya karena memuat data di dalam build; pindahkan ke konstruktor provider-nya. Dan kalau memakai ChangeNotifierProxyProvider, kembalikan sebelumnya ?? ... supaya objeknya tidak dibuat ulang setiap kali ketergantungannya berubah.
Variasi 2: Tiket Wahana Bertingkat — ⭐⭐⭐ · 45–60 menit
Tantangan: Bangun pemesanan tiket taman hiburan dengan dua provider yang saling bergantung: satu memegang keanggotaan pengguna (biasa, perak, emas), satu lagi memegang keranjang tiket. Harga di keranjang harus otomatis menyesuaikan diskon keanggotaan setiap kali tingkatnya berubah.
Bayangan tampilan:
text
╭───────────────────────────────────╮
│ 9:41 ▂▄▆ 🔋 │
│ Keanggotaan: Perak (diskon 10%) │
│ ┌─────────────────────────────┐ │
│ │ Tiket Dewasa x2 Rp 180.000 │ │
│ │ Tiket Anak x1 Rp 45.000 │ │
│ │ Diskon Perak (10%) -Rp22.500 │ │
│ │ TOTAL Rp 202.500 │ │
│ └─────────────────────────────┘ │
│ ▬▬▬▬ │
╰───────────────────────────────────╯Ganti keanggotaan ke "Emas" → TOTAL berubah seketika, tapi isi keranjang (Dewasa x2, Anak x1) tidak ikut hilang atau ter-reset.
Kriteria selesai:
- Keranjang menerima data keanggotaan lewat
ChangeNotifierProxyProvider, bukan dengan membaca provider lain lewatcontextdi dalam metodenya. - Mengubah tingkat keanggotaan langsung memperbarui total keranjang tanpa pengguna menyentuh apa pun.
- Provider dideklarasikan dalam urutan yang benar di
MultiProvider. - Objek keranjang tidak dibuat ulang setiap kali keanggotaan berubah — isinya tetap.
Petunjuk: Kriteria terakhir adalah jebakan ChangeNotifierProxyProvider: fungsi update dipanggil setiap kali ketergantungannya berubah, dan kalau kamu selalu mengembalikan objek baru di sana, isi keranjang pengguna akan terhapus setiap kali keanggotaannya berubah. Mengembalikan objek sebelumnya kalau sudah ada — lalu memperbaruinya — adalah pola yang benar. Untuk urutan di MultiProvider, ingat bahwa yang di bawah bisa melihat yang di atas, tidak sebaliknya.
Variasi 3: Sesi Perpustakaan — ⭐⭐⭐ · 45–60 menit
Tantangan: Bangun provider sesi anggota perpustakaan yang mendengarkan sebuah Stream status sesi (masuk, keluar, sesi kedaluwarsa), lalu memakainya untuk menentukan layar mana yang tampil. Sesi yang kedaluwarsa harus langsung mengeluarkan pengguna dari layar mana pun ia berada.
Bayangan alur layar:
text
Stream status sesi:
masuk ──▶ [Layar Beranda]
keluar ──▶ [Layar Masuk]
kedaluwarsa ──▶ [Layar Masuk] (dari layar mana pun, tanpa
layar lama tertinggal di tumpukan)
Saat aplikasi dibuka:
[Splash] ──(sesi dipulihkan)──▶ [Beranda] atau [Masuk]
(mencegah Layar Masuk "berkedip" sesaat sebelum Beranda muncul)Kriteria selesai:
- Provider berlangganan stream di konstruktornya, dan membatalkan langganan di
dispose(). - Perpindahan layar terjadi secara reaktif dari state, bukan lewat
pushmanual di dalam callback. - Sesi kedaluwarsa saat pengguna berada di layar detail mengembalikannya ke layar masuk tanpa meninggalkan layar lama di tumpukan.
- Layar splash mencegah layar masuk berkedip saat sesi sedang dipulihkan.
Petunjuk: ChangeNotifier punya dispose() seperti State, dan melewatkan pembatalan langganan di sana adalah kebocoran yang sama persis bentuknya — stream tetap hidup, callback-nya tetap dipanggil, dan notifyListeners() dijalankan pada objek yang sudah dibuang. Untuk navigasi reaktif, bangun satu widget gerbang yang mengawasi state sesi dan mengembalikan layar yang sesuai; pola ini akan muncul lagi hampir identik di Bab 28.
Variasi 4: Papan Skor Langsung — ⭐⭐⭐ · 45–60 menit
Tantangan: Bangun papan skor pertandingan yang diperbarui setiap detik: skor, waktu berjalan, jumlah pelanggaran, dan daftar kejadian. Yang diuji di sini adalah membatasi rebuild — memperbarui waktu tidak boleh membangun ulang daftar kejadian.
Bayangan tampilan:
text
╭───────────────────────────────────╮
│ 9:41 ▂▄▆ 🔋 │
│ Tim A 2 - 1 Tim B │
│ ⏱ 45:12 │
│ Pelanggaran: 3 │
│ ┌─────────────────────────────┐ │
│ │ 12' Gol - Tim A │ │
│ │ 34' Kartu Kuning - Tim B │ │
│ └─────────────────────────────┘ │
│ ▬▬▬▬ │
╰───────────────────────────────────╯Hanya baris ⏱ 45:12 yang rebuild setiap detik — skor, pelanggaran, dan daftar kejadian di atas diam kecuali nilainya benar-benar berubah.
Kriteria selesai:
- Kamu sudah mengukur berapa banyak widget yang dibangun ulang per detik sebelum optimasi, dengan
debugPrintdibuild(). - Setelah optimasi, hanya widget waktu yang dibangun ulang per detik.
Selectorataucontext.selectdipakai untuk memisahkan langganan per properti.- Parameter
childpadaConsumerdipakai untuk bagian yang tidak pernah berubah.
Petunjuk: context.watch membangun ulang seluruh widget yang memanggilnya, sehingga menaruhnya di build milik Scaffold berarti seluruh layar dibangun ulang setiap detik. Selector mempersempit dua hal sekaligus: bagian state mana yang didengarkan, dan bagian pohon mana yang dibangun ulang. Parameter child adalah tambahan yang gratis — apa pun yang kamu taruh di sana dibangun sekali lalu dioper apa adanya.
Variasi 5: Pindah ke Riverpod — ⭐⭐⭐⭐ · 60–90 menit
Tantangan: Ambil salah satu latihan Provider di atas dan tulis ulang dengan Riverpod, tanpa mengubah perilaku yang terlihat pengguna. Lalu tulis perbandingan jujurmu sendiri dalam satu halaman.
Kriteria selesai:
- Seluruh
ChangeNotifierdiganti denganStateNotifieratauAsyncNotifier, dan tidak ada satu punnotifyListeners()yang tersisa. - Tidak ada provider yang membutuhkan
BuildContextuntuk diakses dari lapisan logika. - Kamu membuat dua provider bertipe sama dan membuktikan keduanya bisa hidup berdampingan — sesuatu yang tidak bisa dilakukan Provider.
- Perbandinganmu menyebut minimal satu hal yang justru lebih baik di Provider.
Petunjuk: Kriteria ketiga adalah perbedaan yang paling sulit dirasakan sampai kamu membutuhkannya: Provider mencari berdasarkan tipe, sehingga dua ChangeNotifier bertipe sama dalam satu pohon membuatnya bingung. Riverpod mencari berdasarkan identitas objek providernya, sehingga masalah itu tidak ada. Untuk kriteria terakhir, jawaban yang jujur biasanya berkisar pada seberapa cepat Provider bisa dipahami orang yang baru bergabung — kesederhanaan punya nilainya sendiri.
Ikhtisar
- Provider paling sering dipasangkan dengan
ChangeNotifier, dan kamu harus memanggilnotifyListeners()secara manual setiap kali state berubah. - Lupa
notifyListeners()adalah bug nomor satu — tidak ada error, UI hanya diam. - Kembalikan
List.unmodifiabledari getter agar widget tidak bisa mengubah state diam-diam. - Daftarkan dengan
MultiProvider; urutan penting karena provider yang bergantung harus dideklarasikan setelah ketergantungannya. ChangeNotifierProxyProvideruntuk provider yang butuh provider lain — kembalikansebelumnya ?? ...agar tidak dibuat ulang percuma.context.watchdibuild,context.readdi callback,context.selectuntuk berlangganan satu properti.ConsumerdanSelectormembatasi cakupan rebuild; parameterchilduntuk bagian yang tidak berubah.- Jangan panggil
notifyListeners()saat widget sedang dibangun — pakaiaddPostFrameCallback, atau lebih baik muat data di konstruktor provider. - Selalu batalkan
StreamSubscriptiondidispose()milikChangeNotifier. - Riverpod menyelesaikan tiga kelemahan Provider: kesalahan terdeteksi saat kompilasi, tidak butuh
BuildContext, dan bisa punya banyak provider bertipe sama.
Berikutnya: Bab 20 — BLoC: Konsep Inti, pendekatan yang berbeda secara mendasar.
Transkrip asli
Disintesis dari 4_flutter_ai-chatbot-n-firebase/5_state-management-with-provider.md (3 video: Introduction to State Management, State Management Using Provider Part 1 & 2). Lihat PDF Firebase & AI.