14 KiB
Riverpod 3.0 Setup - Worker Flutter App
Overview
This document provides a complete guide to the Riverpod 3.0 state management setup for the Worker Flutter app.
What's Configured
1. Dependencies (pubspec.yaml)
Production Dependencies:
flutter_riverpod: ^3.0.0- Main Riverpod packageriverpod_annotation: ^3.0.0- Annotations for code generation
Development Dependencies:
build_runner: ^2.4.11- Code generation runnerriverpod_generator: ^3.0.0- Generates provider code from annotationsriverpod_lint: ^3.0.0- Riverpod-specific linting rulescustom_lint: ^0.7.0- Required for riverpod_lint
2. Build Configuration (build.yaml)
Configured to generate code for:
**_provider.dartfiles- Files in
**/providers/directories - Files in
**/notifiers/directories
3. Analysis Options (analysis_options.yaml)
Configured with:
- Custom lint plugin enabled
- Exclusion of generated files (*.g.dart, *.freezed.dart)
- Riverpod-specific lint rules
- Comprehensive code quality rules
4. App Initialization (main.dart)
Wrapped with ProviderScope:
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
Directory Structure
lib/core/providers/
├── connectivity_provider.dart # Network connectivity monitoring
├── provider_examples.dart # Comprehensive Riverpod 3.0 examples
└── README.md # Provider architecture documentation
Quick Start
1. Install Dependencies
flutter pub get
2. Generate Provider Code
# One-time generation
dart run build_runner build --delete-conflicting-outputs
# Watch mode (auto-regenerates on file changes)
dart run build_runner watch -d
3. Use the Setup Script
chmod +x scripts/setup_riverpod.sh
./scripts/setup_riverpod.sh
Core Providers
Connectivity Provider
Location: /lib/core/providers/connectivity_provider.dart
Purpose: Monitor network connectivity status across the app.
Providers Available:
-
connectivityProvider - Connectivity instance
final connectivity = ref.watch(connectivityProvider); -
connectivityStreamProvider - Real-time connectivity stream
final status = ref.watch(connectivityStreamProvider); status.when( data: (status) => Text('Status: $status'), loading: () => CircularProgressIndicator(), error: (e, _) => Text('Error: $e'), ); -
currentConnectivityProvider - One-time connectivity check
final status = await ref.read(currentConnectivityProvider.future); -
isOnlineProvider - Boolean online/offline stream
final isOnline = ref.watch(isOnlineProvider);
Usage Example:
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final connectivityState = ref.watch(connectivityStreamProvider);
return connectivityState.when(
data: (status) {
if (status == ConnectivityStatus.offline) {
return OfflineBanner();
}
return OnlineContent();
},
loading: () => LoadingIndicator(),
error: (error, _) => ErrorWidget(error),
);
}
}
Riverpod 3.0 Key Features
1. @riverpod Annotation (Code Generation)
The modern, recommended approach:
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'my_provider.g.dart';
// Simple value
@riverpod
String greeting(GreetingRef ref) => 'Hello';
// Async value
@riverpod
Future<User> user(UserRef ref, String id) async {
return await fetchUser(id);
}
// Mutable state
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state++;
}
2. Unified Ref Type
No more separate FutureProviderRef, StreamProviderRef, etc. - just Ref:
@riverpod
Future<String> example(ExampleRef ref) async {
ref.watch(provider1);
ref.read(provider2);
ref.listen(provider3, (prev, next) {});
}
3. Family as Function Parameters
// Simple parameter
@riverpod
Future<User> user(UserRef ref, String id) async {
return await fetchUser(id);
}
// Multiple parameters with named, optional, defaults
@riverpod
Future<List<Post>> posts(
PostsRef ref, {
required String userId,
int page = 1,
int limit = 20,
String? category,
}) async {
return await fetchPosts(userId, page, limit, category);
}
// Usage
ref.watch(userProvider('user123'));
ref.watch(postsProvider(userId: 'user123', page: 2));
4. AutoDispose vs KeepAlive
// AutoDispose (default) - cleaned up when not watched
@riverpod
String autoExample(AutoExampleRef ref) => 'Auto disposed';
// KeepAlive - stays alive until app closes
@Riverpod(keepAlive: true)
String keepExample(KeepExampleRef ref) => 'Kept alive';
5. ref.mounted Check
New in Riverpod 3.0 - check if provider is still alive after async operations:
@riverpod
class DataManager extends _$DataManager {
@override
String build() => 'Initial';
Future<void> updateData() async {
await Future.delayed(Duration(seconds: 2));
// Check if provider is still mounted
if (!ref.mounted) return;
state = 'Updated';
}
}
6. AsyncValue.guard() for Error Handling
@riverpod
class UserProfile extends _$UserProfile {
@override
Future<User> build() async => await fetchUser();
Future<void> update(String name) async {
state = const AsyncValue.loading();
// AsyncValue.guard catches errors automatically
state = await AsyncValue.guard(() async {
return await updateUser(name);
});
}
}
Provider Patterns
1. Simple Provider (Immutable Value)
@riverpod
String appVersion(AppVersionRef ref) => '1.0.0';
@riverpod
int pointsMultiplier(PointsMultiplierRef ref) {
final tier = ref.watch(userTierProvider);
return tier == 'diamond' ? 3 : 2;
}
2. FutureProvider (Async Data)
@riverpod
Future<User> currentUser(CurrentUserRef ref) async {
final token = await ref.watch(authTokenProvider.future);
return await fetchUser(token);
}
3. StreamProvider (Real-time Data)
@riverpod
Stream<List<Message>> chatMessages(ChatMessagesRef ref, String roomId) {
return ref.watch(webSocketProvider).messages(roomId);
}
4. Notifier (Mutable State)
@riverpod
class Cart extends _$Cart {
@override
List<CartItem> build() => [];
void addItem(Product product) {
state = [...state, CartItem.fromProduct(product)];
}
void removeItem(String id) {
state = state.where((item) => item.id != id).toList();
}
void clear() {
state = [];
}
}
5. AsyncNotifier (Async Mutable State)
@riverpod
class UserProfile extends _$UserProfile {
@override
Future<User> build() async {
return await ref.read(userRepositoryProvider).getCurrentUser();
}
Future<void> updateName(String name) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final updated = await ref.read(userRepositoryProvider).updateName(name);
return updated;
});
}
Future<void> refresh() async {
ref.invalidateSelf();
}
}
6. StreamNotifier (Stream Mutable State)
@riverpod
class LiveChat extends _$LiveChat {
@override
Stream<List<Message>> build(String roomId) {
return ref.watch(chatServiceProvider).messagesStream(roomId);
}
Future<void> sendMessage(String text) async {
await ref.read(chatServiceProvider).send(roomId, text);
}
}
Usage in Widgets
ConsumerWidget
class ProductList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return products.when(
data: (list) => ListView.builder(
itemCount: list.length,
itemBuilder: (context, index) => ProductCard(list[index]),
),
loading: () => CircularProgressIndicator(),
error: (error, stack) => ErrorView(error),
);
}
}
ConsumerStatefulWidget
class OrderPage extends ConsumerStatefulWidget {
@override
ConsumerState<OrderPage> createState() => _OrderPageState();
}
class _OrderPageState extends ConsumerState<OrderPage> {
@override
void initState() {
super.initState();
// Can use ref in all lifecycle methods
Future.microtask(
() => ref.read(ordersProvider.notifier).loadOrders(),
);
}
@override
Widget build(BuildContext context) {
final orders = ref.watch(ordersProvider);
return OrderList(orders);
}
}
Consumer (Optimization)
Column(
children: [
const StaticHeader(),
Consumer(
builder: (context, ref, child) {
final count = ref.watch(cartCountProvider);
return CartBadge(count);
},
),
],
)
Performance Optimization
Use .select() to Watch Specific Fields
// Bad - rebuilds on any user change
final user = ref.watch(userProvider);
// Good - rebuilds only when name changes
final name = ref.watch(userProvider.select((user) => user.name));
// Good with AsyncValue
final userName = ref.watch(
userProfileProvider.select((async) => async.value?.name),
);
Provider Composition
@riverpod
Future<Dashboard> dashboard(DashboardRef ref) async {
// Depend on other providers
final user = await ref.watch(userProvider.future);
final stats = await ref.watch(statsProvider.future);
final orders = await ref.watch(recentOrdersProvider.future);
return Dashboard(
user: user,
stats: stats,
recentOrders: orders,
);
}
Testing
Unit Testing Providers
test('counter increments', () {
final container = ProviderContainer();
addTearDown(container.dispose);
expect(container.read(counterProvider), 0);
container.read(counterProvider.notifier).increment();
expect(container.read(counterProvider), 1);
});
test('async provider fetches data', () async {
final container = ProviderContainer();
addTearDown(container.dispose);
final user = await container.read(userProvider.future);
expect(user.name, 'John Doe');
});
Widget Testing
testWidgets('displays user name', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
userProvider.overrideWith((ref) => User(name: 'Test User')),
],
child: MaterialApp(home: UserScreen()),
),
);
expect(find.text('Test User'), findsOneWidget);
});
Mocking Providers
testWidgets('handles loading state', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
userProvider.overrideWith((ref) {
return Future.delayed(
Duration(seconds: 10),
() => User(name: 'Test'),
);
}),
],
child: MaterialApp(home: UserScreen()),
),
);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
Linting
Run Riverpod Lint
# Check for Riverpod-specific issues
dart run custom_lint
# Auto-fix issues
dart run custom_lint --fix
Riverpod Lint Rules Enabled
provider_dependencies- Ensure proper dependency usagescoped_providers_should_specify_dependencies- Scoped provider safetyavoid_public_notifier_properties- Encapsulationavoid_ref_read_inside_build- Performance (don't use ref.read in build)avoid_manual_providers_as_generated_provider_dependency- Use generated providersfunctional_ref- Proper ref usagenotifier_build- Proper Notifier implementation
Common Issues & Solutions
Issue 1: Generated files not found
Solution:
dart run build_runner build --delete-conflicting-outputs
Issue 2: Provider not updating
Solution: Check if you're using ref.watch() not ref.read() in build method.
Issue 3: Memory leaks
Solution: Use autoDispose (default) for providers that should clean up. Only use keepAlive for global state.
Issue 4: Too many rebuilds
Solution: Use .select() to watch specific fields instead of entire objects.
Migration from Riverpod 2.x
StateNotifierProvider → Notifier
// Old (2.x)
class Counter extends StateNotifier<int> {
Counter() : super(0);
void increment() => state++;
}
final counterProvider = StateNotifierProvider<Counter, int>(Counter.new);
// New (3.0)
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state++;
}
Provider.family → Function Parameters
// Old (2.x)
final userProvider = FutureProvider.family<User, String>((ref, id) async {
return fetchUser(id);
});
// New (3.0)
@riverpod
Future<User> user(UserRef ref, String id) async {
return fetchUser(id);
}
Examples
Comprehensive examples are available in:
/lib/core/providers/provider_examples.dart- All Riverpod 3.0 patterns/lib/core/providers/connectivity_provider.dart- Real-world connectivity monitoring
Resources
Next Steps
- Run
flutter pub getto install dependencies - Run
dart run build_runner watch -dto start code generation - Create feature-specific providers in
lib/features/*/presentation/providers/ - Follow the patterns in
provider_examples.dart - Use connectivity_provider as a reference for real-world implementation
Support
For questions or issues:
- Check provider_examples.dart for patterns
- Review the Riverpod documentation
- Run custom_lint to catch common mistakes
- Use ref.watch() in build methods, ref.read() in event handlers