Riverpod 3 is the current stable state-management library for Flutter, and its code-generation story has matured significantly. The @riverpod annotation eliminates boilerplate, while AsyncNotifier and Notifier provide typed state machines that scale from simple counters to complex offline-first architectures.
>Setup
yaml
# pubspec.yaml
dependencies:
flutter_riverpod: ^3.0.0
riverpod_annotation: ^3.0.0
dev_dependencies:
build_runner: ^2.4.0
riverpod_generator: ^3.0.0
riverpod_lint: ^3.0.0Dart
// main.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
const ProviderScope(child: MyApp()),
);
}>@riverpod code generation
Dart
// providers/user_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'user_provider.g.dart'; // generated
// Simple async provider — fetches once
@riverpod
Future<User> user(Ref ref, {required String id}) async {
final repo = ref.watch(userRepositoryProvider);
return repo.getById(id);
}
// Family variant — same, auto-inferred from parameters
// Generated: userProvider(id: '42')
// Run code generation
// dart run build_runner watch --delete-conflicting-outputs>AsyncNotifier — async state machine
Dart
// providers/posts_notifier.dart
part 'posts_notifier.g.dart';
@riverpod
class PostsNotifier extends _$PostsNotifier {
@override
Future<List<Post>> build() async {
// build() is called once — returns the initial state
return _loadPosts();
}
Future<List<Post>> _loadPosts() async {
final repo = ref.read(postRepositoryProvider);
return repo.fetchAll();
}
Future<void> create(String title) async {
// Optimistic update
state = AsyncData([
...state.requireValue,
Post.optimistic(title: title),
]);
try {
final repo = ref.read(postRepositoryProvider);
final post = await repo.create(title: title);
state = AsyncData([
...state.requireValue.where((p) => !p.isOptimistic),
post,
]);
} catch (e) {
// Revert — reload from source of truth
state = AsyncLoading();
state = await AsyncValue.guard(_loadPosts);
}
}
}
// UI — ConsumerWidget
class PostsList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final postsAsync = ref.watch(postsNotifierProvider);
return postsAsync.when(
data: (posts) => ListView(children: posts.map(PostTile.new).toList()),
loading: () => const CircularProgressIndicator(),
error: (err, _) => Text('Error: $err'),
);
}
}>keepAlive — prevent provider disposal
Dart
@Riverpod(keepAlive: true)
UserRepository userRepository(Ref ref) {
// This provider stays alive even if no widget is listening.
// Useful for repositories, HTTP clients, database connections.
return UserRepository(ref.watch(httpClientProvider));
}
// Manual keepAlive with cancel link
@riverpod
Future<SearchResults> search(Ref ref, String query) async {
final link = ref.keepAlive(); // prevent disposal while in flight
final results = await searchApi(query);
// link.close() — call to allow disposal again (e.g. on page leave)
return results;
}>Offline-first pattern
Dart
@riverpod
class OfflineFirstPosts extends _$OfflineFirstPosts {
@override
Future<List<Post>> build() async {
// 1. Return cached data immediately
final cache = ref.read(localCacheProvider);
final cached = await cache.getPosts();
if (cached.isNotEmpty) {
state = AsyncData(cached); // show stale data
}
// 2. Fetch fresh data in background
try {
final fresh = await ref.read(postRepositoryProvider).fetchAll();
await cache.savePosts(fresh);
return fresh;
} catch (_) {
// Network error — return cached if available
if (cached.isNotEmpty) return cached;
rethrow;
}
}
}