Setting up Riverpod Architecture for Flutter Applications
Riverpod is a rethinking of Provider by the same author (Remi Rousselet). It eliminates the main Provider weakness: dependency on BuildContext. In Riverpod, providers are declared globally as constants, accessible from anywhere — including non-widget code, tests, services. With Riverpod 2.x and code generation via riverpod_annotation, boilerplate is reduced to a minimum.
Modern Riverpod with @riverpod
@riverpod
UserRepository userRepository(UserRepositoryRef ref) {
return UserRepositoryImpl(ref.watch(httpClientProvider));
}
@riverpod
class ProfileNotifier extends _$ProfileNotifier {
@override
FutureOr<UserProfile> build(String userId) async {
return ref.watch(userRepositoryProvider).getProfile(userId);
}
Future<void> refresh() async {
ref.invalidateSelf();
await future;
}
}
Generator riverpod_generator creates profileNotifierProvider(userId) from this code. AsyncNotifier automatically manages AsyncValue<UserProfile> — loading, data, error.
In a widget:
class ProfileScreen extends ConsumerWidget {
final String userId;
const ProfileScreen({required this.userId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final profileAsync = ref.watch(profileNotifierProvider(userId));
return profileAsync.when(
loading: () => const CircularProgressIndicator(),
error: (err, _) => ErrorView(message: err.toString()),
data: (profile) => ProfileView(profile: profile),
);
}
}
Advantage over Provider: Provider Composition
Riverpod lets providers depend on each other via ref.watch. This works outside the widget tree:
@riverpod
Future<List<Post>> filteredFeed(FilteredFeedRef ref) async {
final feed = await ref.watch(feedProvider.future);
final filters = ref.watch(contentFiltersProvider);
return feed.where((p) => filters.allows(p)).toList();
}
If contentFiltersProvider changes — filteredFeed automatically recalculates. In Provider, this requires ProxyProvider with manual invalidation.
Testing
test('ProfileNotifier returns profile', () async {
final container = ProviderContainer(overrides: [
userRepositoryProvider.overrideWithValue(MockUserRepository()),
]);
final notifier = container.read(profileNotifierProvider('user-1').notifier);
expect(
await container.read(profileNotifierProvider('user-1').future),
equals(tProfile),
);
container.dispose();
});
Test needs no pumpWidget, Flutter SDK not required. This makes Riverpod tests the fastest among all Flutter state management solutions.
What We Configure
ProviderScope at app root. Configure riverpod_annotation + build_runner. Layered structure: repository providers → notifier providers → widget. Examples of ref.invalidate, ref.listen, ref.onDispose for lifecycle management.
Timeline
Setting up Riverpod architecture from scratch: 2–3 days. Migration from Provider to Riverpod: 1 week for projects up to 20 screens. Cost — individually.







