Flutter Advanced State Management: Riverpod 2.0 Deep Dive
State management remains one of the most discussed topics in Flutter development. Riverpod 2.0 has emerged as the leading solution for complex applications, offering compile-time safety, dependency injection, and powerful async state handling.
Why Riverpod Over Provider?
Riverpod solves several fundamental limitations of Provider:
- No BuildContext required: Providers are declared globally and accessed anywhere
- Compile-time safety: Invalid provider combinations fail at compile time
- Multiple providers of same type: No Provider.of
conflicts - Testability: Easy to override providers in tests
- DevTools integration: Built-in state inspection
Setting Up Riverpod 2.0 with Code Generation
# pubspec.yaml
dependencies:
flutter_riverpod: ^2.5.0
riverpod_annotation: ^2.3.0
dev_dependencies:
riverpod_generator: ^2.4.0
build_runner: ^2.4.0
riverpod_lint: ^2.3.0
Run code generation:
dart run build_runner watch --delete-conflicting-outputs
Core Riverpod 2.0 Concepts
Riverpod 2.0 with codegen uses a single @riverpod annotation:
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'providers.g.dart';
// Simple synchronous provider
@riverpod
String appVersion(AppVersionRef ref) => '2.0.0';
// Future provider (async)
@riverpod
Future<List<User>> users(UsersRef ref) async {
final repository = ref.watch(userRepositoryProvider);
return repository.fetchAll();
}
// Stream provider
@riverpod
Stream<List<Message>> messages(MessagesRef ref, {required String roomId}) {
final repository = ref.watch(messageRepositoryProvider);
return repository.streamMessages(roomId);
}
// Notifier (replaces ChangeNotifier)
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state++;
void decrement() => state--;
void reset() => state = 0;
}
AsyncNotifier for Complex Async State
@riverpod
class UserProfile extends _$UserProfile {
@override
Future<UserData> build(String userId) async {
final repository = ref.watch(userRepositoryProvider);
return repository.fetchUser(userId);
}
Future<void> updateName(String newName) async {
state = AsyncData(state.requireValue.copyWith(name: newName));
try {
await ref.read(userRepositoryProvider).updateName(
userId: state.requireValue.id,
name: newName,
);
} catch (e, stack) {
state = AsyncError(e, stack);
ref.invalidateSelf();
}
}
}
Advanced Provider Patterns
Family Providers
@riverpod
Future<Product> product(ProductRef ref, int productId) async {
final repository = ref.watch(productRepositoryProvider);
return repository.fetchProduct(productId);
}
// Usage in widget
class ProductCard extends ConsumerWidget {
final int productId;
const ProductCard({required this.productId, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final productAsync = ref.watch(productProvider(productId));
return productAsync.when(
data: (product) => ProductView(product: product),
loading: () => const ProductSkeleton(),
error: (e, _) => ErrorCard(message: e.toString()),
);
}
}
Provider Dependencies and Auto-Dispose
@Riverpod(keepAlive: false)
Future<SearchResults> searchResults(
SearchResultsRef ref,
String query,
) async {
final cancelToken = CancelToken();
ref.onDispose(cancelToken.cancel);
return ref.read(searchRepositoryProvider).search(
query: query,
cancelToken: cancelToken,
);
}
Migration from Provider
Before (Provider):
class UserNotifier extends ChangeNotifier {
UserData? _user;
bool _loading = false;
Future<void> loadUser(String id) async {
_loading = true;
notifyListeners();
_user = await UserRepository().fetchUser(id);
_loading = false;
notifyListeners();
}
}
After (Riverpod 2.0):
@riverpod
Future<UserData> user(UserRef ref, String id) async {
final repository = ref.watch(userRepositoryProvider);
return repository.fetchUser(id);
}
class UserScreen extends ConsumerWidget {
final String userId;
const UserScreen({required this.userId, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ref.watch(userProvider(userId)).when(
data: (user) => UserView(user: user),
loading: () => const CircularProgressIndicator(),
error: (e, _) => Text(e.toString()),
);
}
}
Async State Handling: Refresh and Retry
class UserList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final usersAsync = ref.watch(usersProvider);
return RefreshIndicator(
onRefresh: () => ref.refresh(usersProvider.future),
child: usersAsync.when(
skipLoadingOnReload: true,
data: (users) => ListView.builder(
itemCount: users.length,
itemBuilder: (_, i) => UserTile(user: users[i]),
),
loading: () => const ShimmerList(),
error: (e, _) => ErrorView(
message: e.toString(),
onRetry: () => ref.invalidate(usersProvider),
),
),
);
}
}
Testing with Riverpod
void main() {
test('loads user successfully', () async {
final container = ProviderContainer(
overrides: [
userRepositoryProvider.overrideWith(
() => MockUserRepository(user: fakeUser),
),
],
);
addTearDown(container.dispose);
await container.read(userProfileProvider('user-1').future);
expect(
container.read(userProfileProvider('user-1')),
isA<AsyncData<UserData>>()
.having((d) => d.value.name, 'name', 'Jane Doe'),
);
});
}
Performance: select() to Prevent Rebuilds
// Only rebuilds when userName changes, not other user fields
final userName = ref.watch(
userProfileProvider(userId).select(
(userAsync) => userAsync.whenData((u) => u.name),
),
);
Architecture: Feature-First with Riverpod
lib/
features/
auth/
data/auth_repository.dart
domain/user.dart
presentation/
providers/auth_providers.dart
screens/login_screen.dart
products/...
shared/
providers/
dio_provider.dart
hive_provider.dart
Conclusion
Riverpod 2.0 with code generation represents a major leap forward in Flutter state management. Its compile-time safety, powerful async handling, and clean dependency injection make it the right choice for production Flutter applications. The migration path from Provider is incremental and well-documented, scaling from simple counters to complex multi-feature applications.