Riverpod #
Riverpod adalah evolusi dari Provider yang mengatasi keterbatasan fundamental-nya: tidak bergantung pada BuildContext, compile-time safe, mendukung multiple instance provider, dan memiliki penanganan state async yang jauh lebih elegan. Riverpod kini menjadi salah satu pilihan state management paling direkomendasikan untuk aplikasi Flutter produksi.
Instalasi #
# pubspec.yaml
dependencies:
flutter_riverpod: ^2.6.1
riverpod_annotation: ^2.6.1 # opsional, untuk code generation
dev_dependencies:
build_runner: ^2.4.13
riverpod_generator: ^2.6.3 # opsional, untuk code generation
Setup ProviderScope #
void main() {
runApp(
// ProviderScope WAJIB -- membungkus seluruh aplikasi
// menyimpan state semua provider
const ProviderScope(
child: MyApp(),
),
);
}
Jenis-jenis Provider #
Provider — Nilai yang Tidak Berubah #
Untuk dependency atau service yang tidak berubah sepanjang lifetime aplikasi:
// Provider sederhana
final apiServiceProvider = Provider<ApiService>((ref) {
return ApiService(baseUrl: 'https://api.contoh.com');
});
// Provider yang bergantung pada provider lain
final produkRepositoryProvider = Provider<ProdukRepository>((ref) {
final api = ref.watch(apiServiceProvider);
return ProdukRepository(api: api);
});
// Akses di widget
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final repo = ref.watch(produkRepositoryProvider);
return Text(repo.toString());
}
}
Notifier — State yang Bisa Diubah #
Notifier menggantikan StateNotifier lama. Ini adalah cara modern untuk membuat provider dengan state yang bisa diubah melalui method:
// State class
@immutable
class KeranjangState {
final List<CartItem> items;
const KeranjangState({this.items = const []});
double get total => items.fold(0, (sum, item) => sum + item.subtotal);
int get jumlah => items.fold(0, (sum, item) => sum + item.jumlah);
KeranjangState copyWith({List<CartItem>? items}) =>
KeranjangState(items: items ?? this.items);
}
// Notifier
class KeranjangNotifier extends Notifier<KeranjangState> {
@override
KeranjangState build() => const KeranjangState(); // initial state
void tambah(Produk produk) {
final items = [...state.items];
final idx = items.indexWhere((i) => i.produk.id == produk.id);
if (idx >= 0) {
items[idx] = items[idx].copyWith(jumlah: items[idx].jumlah + 1);
} else {
items.add(CartItem(produk: produk, jumlah: 1));
}
state = state.copyWith(items: items);
}
void hapus(String produkId) {
state = state.copyWith(
items: state.items.where((i) => i.produk.id != produkId).toList(),
);
}
void kosongkan() => state = const KeranjangState();
}
// Provider-nya
final keranjangProvider = NotifierProvider<KeranjangNotifier, KeranjangState>(
KeranjangNotifier.new,
);
AsyncNotifier — State Async #
AsyncNotifier adalah Notifier untuk state yang membutuhkan operasi async. Ia mengembalikan AsyncValue<T> yang merepresentasikan loading, data, atau error:
class ProdukNotifier extends AsyncNotifier<List<Produk>> {
@override
Future<List<Produk>> build() async {
// Dipanggil sekali saat provider pertama kali dibaca
// ref.watch di sini membuat provider re-build saat dependency berubah
final repo = ref.watch(produkRepositoryProvider);
return repo.getAll();
}
Future<void> refresh() async {
// Invalidate dan reload
ref.invalidateSelf();
await future; // tunggu selesai
}
Future<void> tambahProduk(Produk produk) async {
final repo = ref.read(produkRepositoryProvider);
// Optimistic update: langsung update UI, kemudian sync ke server
state = AsyncData([...state.valueOrNull ?? [], produk]);
try {
await repo.tambah(produk);
} catch (e) {
// Rollback jika gagal
state = AsyncError(e, StackTrace.current);
ref.invalidateSelf();
}
}
}
final produkProvider = AsyncNotifierProvider<ProdukNotifier, List<Produk>>(
ProdukNotifier.new,
);
FutureProvider — Data Async Sederhana #
Untuk data async yang tidak perlu method tambahan:
// FutureProvider sederhana -- load data sekali
final profilProvider = FutureProvider<Profil>((ref) async {
final api = ref.watch(apiServiceProvider);
return api.getProfil();
});
// Dengan autoDispose -- provider dihapus saat tidak ada listener
final detailProdukProvider = FutureProvider.autoDispose.family<Produk, String>(
(ref, id) async {
final repo = ref.watch(produkRepositoryProvider);
return repo.getById(id);
},
);
StreamProvider — Data Real-time #
// StreamProvider untuk data real-time (Firebase, WebSocket)
final chatProvider = StreamProvider.autoDispose.family<List<Pesan>, String>(
(ref, roomId) {
final service = ref.watch(chatServiceProvider);
return service.streamPesan(roomId);
},
);
ref.watch, ref.read, ref.listen #
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// ref.watch -- subscribe, rebuild saat berubah
// GUNAKAN DI BUILD METHOD
final keranjang = ref.watch(keranjangProvider);
final produk = ref.watch(produkProvider);
return Column(
children: [
Text('${keranjang.jumlah} item'),
ElevatedButton(
onPressed: () {
// ref.read -- baca tanpa subscribe
// GUNAKAN DI CALLBACK / EVENT HANDLER
ref.read(keranjangProvider.notifier).tambah(produk);
},
child: const Text('Tambah'),
),
],
);
}
}
// ref.listen -- untuk side effect (navigasi, snackbar)
// GUNAKAN DI BUILD METHOD TAPI UNTUK SIDE EFFECT, BUKAN REBUILD
class LoginWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<AuthState>(authProvider, (previous, next) {
if (next.isLoggedIn && !(previous?.isLoggedIn ?? false)) {
// Navigasi setelah login berhasil
context.go('/home');
}
if (next.error != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(next.error!)),
);
}
});
return const LoginForm();
}
}
AsyncValue — Menangani State Async dengan Elegan #
AsyncValue<T> adalah sealed class yang merepresentasikan tiga kemungkinan state dari operasi async:
// Di widget -- gunakan .when() untuk handle semua state
class ProdukList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final produkAsync = ref.watch(produkProvider);
return produkAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Column(
children: [
Text('Error: $error'),
ElevatedButton(
onPressed: () => ref.invalidate(produkProvider),
child: const Text('Coba Lagi'),
),
],
),
),
data: (produk) => ListView.builder(
itemCount: produk.length,
itemBuilder: (context, index) => ProdukTile(produk: produk[index]),
),
);
}
}
// Varian .when() yang berguna:
produkAsync.whenData((data) => data.length); // hanya handle data
produkAsync.valueOrNull; // nilai atau null
produkAsync.hasValue; // true jika ada data
produkAsync.isLoading; // true jika loading
produkAsync.hasError; // true jika error
// Tampilkan data lama saat refresh (tidak blank saat reload)
produkAsync.when(
skipLoadingOnReload: true, // jangan tampilkan loading saat refresh
loading: () => const LinearProgressIndicator(),
error: (e, _) => Text('Error: $e'),
data: (data) => ProdukGrid(produk: data),
)
family — Provider dengan Parameter #
family memungkinkan membuat provider yang berbeda untuk setiap nilai parameter:
// Provider berbeda untuk setiap ID produk
final detailProdukProvider = FutureProvider.autoDispose
.family<Produk, String>((ref, String id) async {
final repo = ref.watch(produkRepositoryProvider);
return repo.getById(id);
});
// Penggunaan -- Flutter membuat provider terpisah untuk setiap ID
final produkA = ref.watch(detailProdukProvider('produk-123'));
final produkB = ref.watch(detailProdukProvider('produk-456'));
// Dengan Notifier + family
class PesananNotifier extends FamilyAsyncNotifier<Pesanan, String> {
@override
Future<Pesanan> build(String pesananId) async {
final repo = ref.watch(pesananRepositoryProvider);
return repo.getById(pesananId);
}
Future<void> batalkan() async {
final repo = ref.read(pesananRepositoryProvider);
await repo.batalkan(arg); // arg = pesananId yang diteruskan ke build
ref.invalidateSelf();
}
}
final pesananProvider = AsyncNotifierProvider.autoDispose
.family<PesananNotifier, Pesanan, String>(PesananNotifier.new);
autoDispose — Provider yang Otomatis Dihapus #
// autoDispose: provider dihapus dari memory saat tidak ada lagi listener
// Sangat berguna untuk data yang hanya relevan di satu screen
final searchProvider = FutureProvider.autoDispose.family<List<Produk>, String>(
(ref, query) async {
// Debounce: batalkan request jika query berubah sebelum selesai
ref.onDispose(() => print('Search provider untuk "$query" di-dispose'));
final repo = ref.watch(produkRepositoryProvider);
return repo.search(query);
},
);
// Jika ingin provider tetap hidup meskipun tidak ada listener:
final cacheProvider = FutureProvider.autoDispose<Data>((ref) async {
final link = ref.keepAlive(); // cegah dispose
ref.onDispose(() => link.close()); // tutup keepAlive jika diperlukan
return fetchData();
});
ConsumerWidget dan ConsumerStatefulWidget #
// ConsumerWidget -- pengganti StatelessWidget
class ProdukScreen extends ConsumerWidget {
const ProdukScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// ref tersedia langsung di parameter build
final produk = ref.watch(produkProvider);
return ProdukList(produk: produk);
}
}
// ConsumerStatefulWidget -- pengganti StatefulWidget
class SearchScreen extends ConsumerStatefulWidget {
const SearchScreen({super.key});
@override
ConsumerState<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends ConsumerState<SearchScreen> {
final _controller = TextEditingController();
@override
Widget build(BuildContext context) {
// ref tersedia sebagai field -- tidak perlu di parameter
final hasil = ref.watch(searchProvider(_controller.text));
return Column(
children: [
TextField(controller: _controller, onChanged: (_) => setState(() {})),
hasil.when(
loading: () => const CircularProgressIndicator(),
error: (e, _) => Text('Error: $e'),
data: (data) => ProdukList(produk: data),
),
],
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
Struktur Folder yang Direkomendasikan #
lib/
├── main.dart
├── features/
│ ├── produk/
│ │ ├── data/
│ │ │ └── produk_repository.dart
│ │ ├── domain/
│ │ │ └── produk.dart
│ │ └── presentation/
│ │ ├── produk_screen.dart
│ │ └── produk_provider.dart ← providers di sini
│ └── keranjang/
│ ├── data/
│ ├── domain/
│ └── presentation/
│ ├── keranjang_screen.dart
│ └── keranjang_provider.dart
└── core/
├── network/
│ └── api_service_provider.dart
└── storage/
└── storage_provider.dart
Ringkasan #
- Riverpod mengatasi keterbatasan Provider — tidak bergantung
BuildContext, compile-time safe, mendukung multiple instance, dan penanganan async yang elegan.- Selalu bungkus aplikasi dengan
ProviderScopedimain().- Gunakan
Notifieruntuk state yang bisa diubah dengan method,AsyncNotifieruntuk state yang membutuhkan operasi async.FutureProvideruntuk data async sederhana,StreamProvideruntuk data real-time.- Gunakan
ref.watch()dibuild()untuk subscribe,ref.read()di callback untuk aksi tanpa subscribe, danref.listen()untuk side effect seperti navigasi atau snackbar.AsyncValue.when()menangani loading, error, dan data dengan elegan dalam satu metode.familymembuat provider terpisah per parameter — gunakan untuk data per-ID seperti detail produk.autoDisposeotomatis menghapus provider dari memory saat tidak ada listener — gunakan untuk screen-specific data.- Gunakan
ConsumerWidgetsebagai penggantiStatelessWidgetdanConsumerStatefulWidgetsebagai penggantiStatefulWidget.