Riverpod
Riverpod is the modern Flutter state-management library — same author as Provider, compile-time safety, easier testing, async support. Use ConsumerWidget/ConsumerStatefulWidget with providers (StateProvider, FutureProvider, StreamProvider, NotifierProvider) to scope state cleanly.
Provider, Notifier, ref, async
EXAMPLE
// 1) Install
// pubspec.yaml
// dependencies:
// flutter_riverpod: ^2.5.0
// riverpod_annotation: ^2.3.0
// dev_dependencies:
// build_runner: ^2.4.0
// riverpod_generator: ^2.4.0
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 2) Root — wrap MaterialApp in ProviderScope
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: CounterPage());
}
}
// 3) Simplest — Provider for a constant value
final greetingProvider = Provider<String>((ref) => 'Hello');
class GreetingView extends ConsumerWidget {
const GreetingView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final greeting = ref.watch(greetingProvider);
return Text(greeting);
}
}
// 4) StateProvider — simple mutable state
final counterProvider = StateProvider<int>((ref) => 0);
class CounterPage extends ConsumerWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
body: Center(child: Text('Count: $count', style: const TextStyle(fontSize: 36))),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: const Icon(Icons.add),
),
);
}
}
// 5) NotifierProvider — richer state with methods
class Counter extends Notifier<int> {
@override
int build() => 0;
void increment() => state++;
void decrement() => state--;
void reset() => state = 0;
Future<void> incrementAsync() async {
await Future.delayed(const Duration(seconds: 1));
state++;
}
}
final counterProvider2 = NotifierProvider<Counter, int>(Counter.new);
class CounterPage2 extends ConsumerWidget {
const CounterPage2({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider2);
final notifier = ref.read(counterProvider2.notifier);
return Column(
children: [
Text('$count'),
ElevatedButton(onPressed: notifier.increment, child: const Text('+1')),
ElevatedButton(onPressed: notifier.reset, child: const Text('Reset')),
],
);
}
}
// 6) FutureProvider — async data
final userProvider = FutureProvider<User>((ref) async {
final id = ref.watch(currentUserIdProvider);
return await api.fetchUser(id);
});
class UserView extends ConsumerWidget {
const UserView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider);
return userAsync.when(
data: (user) => Text('Hello ${user.name}'),
loading: () => const CircularProgressIndicator(),
error: (e, _) => Text('Error: $e'),
);
}
}
// 7) StreamProvider — real-time streams
final messagesProvider = StreamProvider<List<Message>>((ref) {
return firestore.collection('messages').snapshots().map((snap) =>
snap.docs.map((d) => Message.fromJson(d.data())).toList(),
);
});
// 8) Family — parameterised providers
final userByIdProvider = FutureProvider.family<User, int>((ref, id) async {
return await api.fetchUser(id);
});
// Usage:
ref.watch(userByIdProvider(42));
// 9) autoDispose — clean up when no longer watched
final searchProvider = FutureProvider.autoDispose.family<List<Result>, String>((ref, query) async {
return await api.search(query);
});
// When all widgets stop watching, the cache is dropped. Saves memory.
// 10) ref methods
// ref.watch(provider) — subscribe; rebuild on change
// ref.read(provider) — one-time read; no subscription
// ref.listen(provider, (prev, next) { ... }) — side effects on change
// ref.invalidate(provider) — clear cached value; forces re-fetch
// ref.refresh(provider) — invalidate + return new value
// 11) AsyncNotifier — modern async state
class UserAsyncNotifier extends AsyncNotifier<User> {
@override
Future<User> build() async {
return await api.fetchUser(ref.watch(currentUserIdProvider));
}
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => api.fetchUser(ref.read(currentUserIdProvider)));
}
Future<void> updateName(String name) async {
await api.updateName(state.value!.id, name);
state = AsyncValue.data(state.value!.copyWith(name: name));
}
}
final userAsyncProvider = AsyncNotifierProvider<UserAsyncNotifier, User>(UserAsyncNotifier.new);
// 12) Code generation (cleaner with @riverpod)
// import 'package:riverpod_annotation/riverpod_annotation.dart';
// part 'counter.g.dart';
//
// @riverpod
// int counter(CounterRef ref) => 0;
//
// @riverpod
// Future<User> user(UserRef ref) async => api.fetchUser(42);
//
// @riverpod
// class CounterNotifier extends _$CounterNotifier {
// @override int build() => 0;
// void increment() => state++;
// }
//
// Run: flutter pub run build_runner build
// 13) Testing — override providers
testWidgets('counter increments', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [counterProvider.overrideWith((ref) => 100)],
child: const MaterialApp(home: CounterPage()),
),
);
expect(find.text('Count: 100'), findsOneWidget);
});
// 14) ProviderScope override at runtime
ProviderScope(
overrides: [
apiProvider.overrideWithValue(MockApi()),
],
child: const App(),
);
// 15) Provider dependencies
final authTokenProvider = StateProvider<String?>((ref) => null);
final apiProvider = Provider<Api>((ref) => Api(token: ref.watch(authTokenProvider)));
final userProvider = FutureProvider<User>((ref) async {
final api = ref.watch(apiProvider);
return await api.fetchMe();
});
// When authTokenProvider changes → apiProvider rebuilds → userProvider re-fetches.
// 16) Common bugs
// • Reading providers in initState → use addPostFrameCallback or ConsumerStatefulWidget
// • Using ref.watch inside callbacks → infinite rebuild; use ref.read
// • Forgetting ProviderScope at the root → 'No ProviderScope above this widget'
// • autoDispose without retainer → frequently re-fetched data; consider keepAlive
// • Riverpod 1.x API confused with 2.x — many examples online are outdated
// • Notifier.build() doing heavy work synchronously — use AsyncNotifier instead
// • Side effects in build() — Riverpod warns; use ref.listen instead
// • Mixing Provider and Riverpod in same app — confusing; pick one
// • Forgetting code generator for @riverpod annotation
// • Provider scope shadowed by nested ProviderScope — overrides may not apply
Why it matters
Riverpod 2.x is the modern Flutter state-management default: providers (Provider, StateProvider, NotifierProvider, FutureProvider, StreamProvider, AsyncNotifier) wrap state, ConsumerWidget subscribes via ref.watch. Use family for parameterised providers, autoDispose to free memory, and ref.listen for side effects. Provider overrides make testing trivial.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
final counterProvider = StateProvider<int>((_) => 0);
class Counter extends ConsumerWidget {
@override
Widget build(BuildContext c, WidgetRef r) {
final n = r.watch(counterProvider);
return TextButton(
onPressed: () => r.read(counterProvider.notifier).state++,
child: Text('$n'),
);
}
}
Try it Yourself »
Discussion
Loading…