Riverpod #
Pustaka Riverpod adalah evolusi arsitektural modern dari sistem Provider yang dirancang khusus untuk memecahkan berbagai keterbatasan mendasar dari pendahulunya. Dengan menghilangkan ketergantungan mutlak pada BuildContext, Riverpod mampu menawarkan tingkat keamanan waktu kompilasi (compile-time safety) yang sangat tinggi, memungkinkan instansiasi ganda dari tipe provider yang sama secara independen, serta menyediakan penanganan status asinkron yang sangat deklaratif melalui konsep AsyncValue. Fleksibilitas ini menjadikan Riverpod sebagai salah satu standar utama manajemen status yang paling direkomendasikan untuk pengembangan aplikasi Flutter komersial skala menengah hingga besar saat ini.
Instalasi #
Untuk menggunakan Riverpod di aplikasi Flutter kita, tambahkan paket dependensi berikut ke dalam berkas pubspec.yaml proyek kita:
dependencies:
flutter:
sdk: flutter
# Paket integrasi Riverpod untuk Flutter
flutter_riverpod: ^2.6.1
dev_dependencies:
# Digunakan untuk menjalankan generator kode (opsional, namun sangat disarankan)
build_runner: ^2.4.13
Setup ProviderScope #
Agar seluruh status dari provider yang kita buat dapat disimpan dan diakses dengan aman di memori, kita wajib membungkus root widget aplikasi menggunakan ProviderScope. Di balik layar, ProviderScope bertindak sebagai penyimpan objek ProviderContainer global.
void main() {
runApp(
// ProviderScope wajib diletakkan di bagian teratas pohon widget
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: LayarDashboard(),
);
}
}
Arsitektur Penyimpanan Status Riverpod #
Salah satu keunggulan terbesar Riverpod adalah arsitekturnya yang memisahkan penyimpanan data status sepenuhnya dari struktur antarmuka UI (widget tree).
flowchart TD
ProviderScope["ProviderScope (Root Widget)"] --> Container["ProviderContainer (Global Memory Store)"]
Container --> ProviderA["ProviderA: ApiService\n(State: Instantiated)"]
Container --> ProviderB["ProviderB: AuthNotifier\n(State: UserLoggedIn)"]
Container --> ProviderC["ProviderC: CartNotifier\n(State: 3 items)"]
WidgetTree["Widget Tree (Render Layer)"] -.-|Bebas Context / Ambil Langsung via Ref| ContainerDengan memisahkan memori penyimpanan status data dari rendering pohon widget, kita dapat dengan mudah membaca, memanipulasi, dan menguji status aplikasi kita dari file Dart murni tanpa perlu mensimulasikan lingkungan render UI.
Jenis-jenis Provider di Riverpod #
Riverpod menyediakan berbagai jenis provider khusus yang disesuaikan dengan skenario data yang kita hadapi:
1. Provider (Nilai Imut / Read-Only) #
Cocok digunakan untuk menampung instansiasi layanan atau repositori statis yang nilainya tidak akan berubah sepanjang daur hidup aplikasi berjalan.
// Mendefinisikan ApiService statis
final apiServiceProvider = Provider<ApiService>((ref) {
return ApiService(baseUrl: 'https://api.example.com');
});
// Menghubungkan dependensi antar-provider secara instan tanpa ProxyProvider
final produkRepositoryProvider = Provider<ProdukRepository>((ref) {
// ref.watch melacak perubahan apiServiceProvider secara dinamis
final apiService = ref.watch(apiServiceProvider);
return ProdukRepository(api: apiService);
});
2. Notifier (State Sinkron yang Dapat Diubah) #
Notifier adalah kelas reaktif modern di Riverpod untuk mengelola status data sinkron yang membutuhkan logika mutasi status terstruktur melalui metode khusus.
@immutable
class StatusKoleksi {
final List<String> daftarNama;
const StatusKoleksi({this.daftarNama = const []});
StatusKoleksi copyWith({List<String>? daftarNama}) {
return StatusKoleksi(daftarNama: daftarNama ?? this.daftarNama);
}
}
// Membuat kelas Notifier kustom
class KoleksiNotifier extends Notifier<StatusKoleksi> {
// build() wajib di-override untuk menetapkan nilai status awal (initial state)
@override
StatusKoleksi build() {
return const StatusKoleksi();
}
void tambahItem(String nama) {
// state merepresentasikan nilai status saat ini
state = state.copyWith(daftarNama: [...state.daftarNama, nama]);
}
void hapusItem(String nama) {
state = state.copyWith(
daftarNama: state.daftarNama.where((item) => item != nama).toList(),
);
}
}
// Mendeklarasikan NotifierProvider secara global
final koleksiProvider = NotifierProvider<KoleksiNotifier, StatusKoleksi>(
KoleksiNotifier.new,
);
3. AsyncNotifier (State Asinkron dengan Kontrol Mutasi) #
AsyncNotifier adalah solusi terbaik untuk mengelola data status yang diperoleh melalui proses asinkron (seperti HTTP Request ke API server) namun tetap membutuhkan penulisan aksi bisnis logik pembaruan data.
class ProdukNotifier extends AsyncNotifier<List<Produk>> {
@override
Future<List<Produk>> build() async {
// Membaca repositori secara asinkron
final repo = ref.watch(produkRepositoryProvider);
return repo.ambilSemuaProduk();
}
Future<void> tambahProduk(Produk produkBaru) async {
// 1. Dapatkan referensi repositori
final repo = ref.read(produkRepositoryProvider);
// 2. Set status sementara ke loading atau lakukan optimistic update
state = const AsyncLoading();
// 3. Jalankan aksi dan perbarui status berdasarkan hasil
state = await AsyncValue.guard(() async {
await repo.simpanProduk(produkBaru);
// Ambil data terbaru untuk diselaraskan
return repo.ambilSemuaProduk();
});
}
}
final produkProvider = AsyncNotifierProvider<ProdukNotifier, List<Produk>>(
ProdukNotifier.new,
);
4. FutureProvider (Pemuatan Asinkron Sederhana) #
Digunakan ketika kita hanya perlu memuat data dari Future sekali saja secara pasif tanpa perlu melakukan mutasi manual setelahnya.
final detailBeritaProvider = FutureProvider.autoDispose.family<Berita, String>(
(ref, idBerita) async {
final repo = ref.watch(produkRepositoryProvider);
return repo.ambilBeritaDetail(idBerita);
},
);
Interaksi Menggunakan Objek Ref #
Untuk membaca status di Riverpod, kita menggunakan objek WidgetRef di dalam widget, atau Ref di dalam kelas provider lainnya. Ada tiga metode utama interaksi:
1. ref.watch() (Mendengarkan Perubahan & Rebuild) #
Metode ref.watch() bertugas mengamati nilai status dari provider tertentu dan memicu rekonstruksi (rebuild) pada widget pemanggil setiap kali status data tersebut mengalami pembaruan.
- Aturan: Selalu gunakan
ref.watch()di dalam fungsibuildwidget kita.
class WidgetDaftarKoleksi extends ConsumerWidget {
const WidgetDaftarKoleksi({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Berlangganan perubahan data koleksi
final koleksi = ref.watch(koleksiProvider);
return ListView(
children: koleksi.daftarNama.map((nama) => Text(nama)).toList(),
);
}
}
2. ref.read() (Membaca Sekali Tanpa Rebuild) #
Metode ref.read() bertugas mengambil nilai status saat ini secara instan tanpa mendaftarkan widget sebagai pendengar aktif.
- Aturan: Gunakan hanya di dalam fungsi callback kejadian seperti tombol klik atau inisialisasi awal, jangan pernah memanggilnya langsung di dalam build method.
ElevatedButton(
onPressed: () {
// Memanggil method Notifier tanpa memicu rebuild pada tombol ini
ref.read(koleksiProvider.notifier).tambahItem('Item Baru');
},
child: const Text('Tambah Data'),
)
3. ref.listen() (Memicu Efek Samping / Side Effects) #
Metode ref.listen() digunakan untuk mendengarkan perubahan status dan mengeksekusi aksi prosedural tertentu (seperti menampilkan SnackBar, membuka halaman baru via Navigator, atau memicu dialog peringatan) tanpa memicu rebuild UI.
class WidgetDashboard extends ConsumerWidget {
const WidgetDashboard({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Mendengarkan status error untuk memicu SnackBar secara reaktif
ref.listen<StatusKoleksi>(koleksiProvider, (StatusKoleksi? prev, StatusKoleksi next) {
if (next.daftarNama.length > 5) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Koleksi Anda sudah penuh!')),
);
}
});
return const Center(child: Text('Konten Aktif'));
}
}
AsyncValue — Menangani Tiga Status Asinkron Secara Deklaratif #
Setiap kali kita membaca asinkron provider (seperti FutureProvider atau AsyncNotifierProvider), Riverpod mengembalikan objek AsyncValue<T>. Konsep ini adalah kelas tertutup (sealed class) yang membungkus tiga kemungkinan status data secara terstruktur:
- AsyncLoading: Proses asinkron sedang berjalan.
- AsyncData: Data berhasil diambil dengan tipe data
T. - AsyncError: Terjadi kesalahan asinkron saat proses pemuatan.
Konsep ini memaksa kita menangani ketiga skenario tersebut di tingkat UI secara aman menggunakan fungsi .when():
class LayarDaftarProduk extends ConsumerWidget {
const LayarDaftarProduk({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Membaca status asinkron produk
final produkAsync = ref.watch(produkProvider);
return Scaffold(
appBar: AppBar(title: const Text('Katalog Produk')),
body: produkAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (Object error, StackTrace stackTrace) => Center(
child: Text('Terjadi kesalahan: $error'),
),
data: (List<Produk> daftarProduk) {
return ListView.builder(
itemCount: daftarProduk.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(daftarProduk[index].nama),
);
},
);
},
),
);
}
}
family — Mengirim Parameter Dinamis ke Provider #
Modifikasi .family digunakan ketika kita membutuhkan provider yang nilainya dinamis berdasarkan parameter input tertentu (seperti ID entitas).
// Mendefinisikan provider dengan parameter String ID
final detailKatalogProvider = FutureProvider.family<Produk, String>(
(ref, idKatalog) async {
final repo = ref.watch(produkRepositoryProvider);
return repo.ambilDetail(idKatalog);
},
);
// Cara Mengaksesnya di UI:
class DetailWidget extends ConsumerWidget {
final String idProduk;
const DetailWidget({super.key, required this.idProduk});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Riverpod otomatis mengidentifikasi dan meng-cache instance terpisah untuk idProduk ini
final produkAsync = ref.watch(detailKatalogProvider(idProduk));
return produkAsync.when(
loading: () => const CircularProgressIndicator(),
error: (e, s) => Text('Error: $e'),
data: (produk) => Text(produk.nama),
);
}
}
autoDispose — Manajemen Memori Pintar #
Di aplikasi berskala produksi, penting bagi kita untuk menjaga beban penggunaan RAM agar tidak membengkak. Modifikasi .autoDispose menginstruksikan Riverpod untuk secara otomatis menghancurkan status (state) dari provider tersebut dan membebaskan memorinya segera setelah tidak ada lagi widget aktif di layar yang mendengarkannya.
flowchart TD
Start["Widget Mulai Mendengarkan Provider"] --> Create["Memicu build() Provider"]
Create --> InUse["Provider Berada di Memori"]
InUse --> WidgetDestroyed["Widget Dihancurkan / Tidak Ada Listener"]
WidgetDestroyed --> CheckAutoDispose{"Apakah menggunakan\n.autoDispose?"}
CheckAutoDispose -->|Ya| KeepAliveActive{"Apakah ref.keepAlive()\ndipanggil?"}
CheckAutoDispose -->|Tidak| KeepInMemory["Tetap Disimpan di Memori"]
KeepAliveActive -->|Ya| KeepInMemory
KeepAliveActive -->|Tidak| Destroy["Hancurkan State & Bebaskan Memori"]Jika dalam skenario tertentu kita membutuhkan data asinkron dari provider yang menggunakan .autoDispose tetap tersimpan sementara di memori (misalnya agar pengguna tidak perlu memuat ulang data saat kembali ke halaman sebelumnya), kita dapat memanfaatkan ref.keepAlive() untuk mempertahankan data tersebut secara terkontrol.
final dataBeritaProvider = FutureProvider.autoDispose<List<Berita>>((ref) async {
final repo = ref.watch(produkRepositoryProvider);
final data = await repo.ambilBeritaTerkini();
// Mempertahankan status cache secara dinamis
final link = ref.keepAlive();
// Kita dapat menutup keepAlive berdasarkan timer atau aksi kustom jika diperlukan
ref.onDispose(() {
link.close();
});
return data;
});
ConsumerWidget vs ConsumerStatefulWidget #
Untuk mengintegrasikan reaktivitas Riverpod ke antarmuka, kita mengganti basis kelas widget standar Flutter:
- ConsumerWidget: Pengganti dari
StatelessWidget. ParameterWidgetRef refdisuntikkan secara langsung di dalam tanda tangan metodebuild(). - ConsumerStatefulWidget: Pengganti dari
StatefulWidget. Objekreftersedia secara global sebagai properti kelas di dalam kelasConsumerStatependampingnya (sehingga kita dapat mengaksesrefdari metode daur hidup sepertiinitStateataudidChangeDependencies).
class LayarPencarian extends ConsumerStatefulWidget {
const LayarPencarian({super.key});
@override
ConsumerState<LayarPencarian> createState() => _LayarPencarianState();
}
class _SearchScreenState extends ConsumerState<LayarPencarian> {
final TextEditingController _queryController = TextEditingController();
@override
void initState() {
super.initState();
// Kita dapat menggunakan ref secara langsung di metode daur hidup!
ref.read(koleksiProvider.notifier).tambahItem('Pencarian Dimulai');
}
@override
void dispose() {
_queryController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// ref diakses sebagai field anggota kelas, tidak perlu dilewatkan sebagai parameter build
final status = ref.watch(koleksiProvider);
return Scaffold(
appBar: AppBar(title: const Text('Pencarian')),
body: Column(
children: [
TextField(controller: _queryController),
Expanded(
child: ListView(
children: status.daftarNama.map((name) => Text(name)).toList(),
),
),
],
),
);
}
}
Struktur Folder Berbasis Fitur yang Direkomendasikan #
Dalam aplikasi berskala besar, disarankan bagi kita untuk mengatur file kode menggunakan pendekatan berorientasi fitur (Feature-First Architecture):
lib/
├── main.dart
├── core/ # Logika global & utilitas bersama
│ └── network/
│ └── api_provider.dart
└── features/ # Modul fitur mandiri
├── katalog/
│ ├── data/
│ │ └── katalog_repository.dart
│ ├── domain/
│ │ └── produk_model.dart
│ └── presentation/
│ ├── layar_katalog.dart
│ └── katalog_provider.dart # Mendefinisikan Notifiers & Providers katalog
└── transaksi/
├── data/
├── domain/
└── presentation/
Ringkasan #
- Bebas Context: Arsitektur Riverpod menyimpan status di memori global (
ProviderContainer), membebaskan logika bisnis dari ketergantungan padaBuildContext.- AsyncValue: Konsep yang menjamin penanganan yang aman terhadap status loading, data, dan error pada seluruh transaksi asinkron di tingkat UI secara deklaratif.
- Pilihan Metode: Selalu panggil
ref.watch()di build method untuk reaktivitas,ref.read()di fungsi callback kejadian untuk aksi sekali jalan, danref.listen()untuk mengeksekusi efek samping.- Optimasi RAM: Manfaatkan properti
.autoDisposedikombinasikan denganref.keepAlive()untuk mengotomatisasi pembersihan status memori yang sudah tidak digunakan.- Widget Pendukung: Gunakan
ConsumerWidgetuntuk widget statis cepat, danConsumerStatefulWidgetjika membutuhkan penanganan metode daur hidup (lifecycle methods).