State management adalah topik yang tidak pernah berhenti diperdebatkan di komunitas Flutter. Provider, Bloc, Riverpod, GetX, MobX — setiap bulan sepertinya ada solusi baru yang diklaim lebih baik dari semua yang ada sebelumnya.
Saya sudah mencoba sebagian besar dari daftar itu. Dan setelah menggunakan Riverpod 2.0 di beberapa proyek production, ini adalah framework state management yang saya rekomendasikan untuk hampir semua skala proyek Flutter.
Bukan karena Riverpod sempurna — tidak ada yang sempurna — tapi karena ia memecahkan masalah nyata dengan cara yang elegan, type-safe, dan testable.
Mengapa Bukan Provider?
Provider sudah lama jadi rekomendasi resmi Flutter. Saya sendiri masih menggunakannya di proyek lama. Tapi ada beberapa friction yang terus mengganggu:
Provider mengharuskan widget tree yang tepat — provider harus berada di atas consumer dalam tree. Ini menyebabkan masalah ketika Anda perlu mengakses sebuah state dari tempat yang tidak memiliki hubungan langsung dalam widget tree.
Tidak ada cara bawaan untuk menangani state yang bergantung pada parameter. Kalau Anda butuh "data artikel dengan id tertentu", Anda harus membuat workaround yang tidak elegan.
Dan yang paling mengganggu: tidak ada compile-time safety yang sesungguhnya. Error karena mengakses provider yang tidak ada baru terdeteksi saat runtime.
Fondasi Riverpod: Provider sebagai Object Global
Perbedaan paling fundamental: di Riverpod, provider adalah object global yang dideklarasikan di luar widget tree.
// Deklarasi provider di level file, bukan di dalam widget
final hitungProvider = StateProvider<int>((ref) => 0);
final artikelRepositoryProvider = Provider<ArtikelRepository>((ref) {
return ArtikelRepository();
});
final daftarArtikelProvider = FutureProvider<List<Artikel>>((ref) async {
final repo = ref.watch(artikelRepositoryProvider);
return repo.getSemuaArtikel();
});
Tidak ada ChangeNotifierProvider yang perlu dibungkus di atas widget. Tidak ada kekhawatiran soal provider tidak ditemukan di tree. Provider bisa diakses dari mana saja selama ProviderScope ada di root aplikasi.
Jenis Provider yang Paling Sering Saya Gunakan
Provider (baca saja):
final formatterProvider = Provider<NumberFormat>((ref) {
return NumberFormat.currency(locale: 'id_ID', symbol: 'Rp ');
});
Untuk dependency injection — membuat instance service, repository, atau utility yang bisa diakses di mana saja.
StateProvider (state sederhana):
final indeksTabProvider = StateProvider<int>((ref) => 0);
final temaGelapProvider = StateProvider<bool>((ref) => false);
Untuk state sederhana yang tidak butuh logika tambahan. Persis seperti useState di React.
FutureProvider (data async):
final profilPenggunaProvider = FutureProvider<Pengguna>((ref) async {
final token = ref.watch(tokenProvider);
return ApiService.getProfile(token);
});
Untuk operasi async yang hasilnya ingin ditampilkan di UI. Secara otomatis menangani loading dan error state.
NotifierProvider (logika kompleks):
@riverpod
class KeranjangBelanja extends _$KeranjangBelanja {
@override
List<ItemBelanja> build() => [];
void tambahItem(Produk produk) {
final sudahAda = state.any((item) => item.produk.id == produk.id);
if (sudahAda) {
state = state.map((item) {
if (item.produk.id == produk.id) {
return item.copyWith(jumlah: item.jumlah + 1);
}
return item;
}).toList();
} else {
state = [...state, ItemBelanja(produk: produk, jumlah: 1)];
}
}
void hapusItem(String produkId) {
state = state.where((item) => item.produk.id != produkId).toList();
}
double get totalHarga =>
state.fold(0, (total, item) => total + item.produk.harga * item.jumlah);
}
Ini adalah pola yang paling sering saya gunakan untuk fitur utama. Logika bisnis terkapsul bersih di dalam Notifier, mudah ditest, dan mudah dibaca.
Code Generation: Investasi Awal yang Worth It
Riverpod 2.0 mendukung code generation via annotation @riverpod. Saya tahu — code generation terdengar berlebihan. Tapi setelah menggunakannya, saya tidak bisa kembali ke cara manual.
Setup di pubspec.yaml:
dependencies:
riverpod_annotation: ^2.3.0
dev_dependencies:
riverpod_generator: ^2.4.0
build_runner: ^2.4.0
Dengan annotation, kode yang perlu Anda tulis jauh lebih sedikit:
// Anda tulis ini:
@riverpod
Future<List<Artikel>> daftarArtikel(DaftarArtikelRef ref) async {
final repo = ref.watch(artikelRepositoryProvider);
return repo.getSemuaArtikel();
}
// Generator akan membuat: daftarArtikelProvider secara otomatis
Jalankan generator:
dart run build_runner watch --delete-conflicting-outputs
Keuntungannya: IDE auto-complete yang sempurna, compile-time safety yang ketat, dan boilerplate yang jauh berkurang. Harga yang dibayar: langkah build tambahan yang perlu dijalankan setiap ada perubahan.
Menangani AsyncValue: Pola yang Bersih
FutureProvider dan StreamProvider mengembalikan AsyncValue<T> — sebuah sealed class yang merepresentasikan tiga kemungkinan state: loading, data, dan error.
// Di widget:
final artikelAsync = ref.watch(daftarArtikelProvider);
return artikelAsync.when(
loading: () => const LoadingSpinner(),
error: (error, stack) => ErrorWidget(pesan: error.toString()),
data: (daftar) => ListView.builder(
itemCount: daftar.length,
itemBuilder: (context, index) => ArtikelCard(artikel: daftar[index]),
),
);
Ini memaksa developer untuk menangani semua kemungkinan state secara eksplisit. Tidak ada lagi loading spinner yang terlupakan atau error yang diam-diam diabaikan.
Testing dengan Riverpod: Sangat Mudah
Ini adalah keunggulan Riverpod yang paling sering diremehkan. Karena provider adalah object independen (bukan terikat widget tree), testing sangat straightforward:
test('keranjang belanja menambah item dengan benar', () {
final container = ProviderContainer();
addTearDown(container.dispose);
final keranjang = container.read(keranjangBelanjaProvider.notifier);
keranjang.tambahItem(Produk(id: '1', nama: 'Buku', harga: 50000));
keranjang.tambahItem(Produk(id: '1', nama: 'Buku', harga: 50000));
expect(container.read(keranjangBelanjaProvider).length, 1);
expect(container.read(keranjangBelanjaProvider)[0].jumlah, 2);
expect(keranjang.totalHarga, 100000);
});
Tidak perlu mock widget tree, tidak perlu testWidgets wrapper yang kompleks. Business logic ditest langsung, cepat, dan bersih.
Kesimpulan
Riverpod 2.0 adalah state management yang dirancang untuk proyek nyata — dengan kebutuhan nyata seperti dependency injection, async handling, testing, dan skalabilitas. Kurva belajarnya memang ada, terutama untuk memahami perbedaan antara jenis-jenis provider.
Tapi investasi waktu untuk belajar Riverpod dengan benar terbayar sangat cepat, terutama di proyek yang akan dikembangkan lebih dari beberapa bulan. Kode yang lebih mudah ditest berarti fitur baru yang lebih aman untuk ditambahkan. Dan fitur baru yang aman berarti produk yang lebih baik untuk pengguna.
Itu selalu tujuan akhirnya.
Belum ada komentar. Jadilah yang pertama menulis.
Tulis Komentar