627 lines
14 KiB
Markdown
627 lines
14 KiB
Markdown
# 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 package
|
|
- `riverpod_annotation: ^3.0.0` - Annotations for code generation
|
|
|
|
**Development Dependencies:**
|
|
- `build_runner: ^2.4.11` - Code generation runner
|
|
- `riverpod_generator: ^3.0.0` - Generates provider code from annotations
|
|
- `riverpod_lint: ^3.0.0` - Riverpod-specific linting rules
|
|
- `custom_lint: ^0.7.0` - Required for riverpod_lint
|
|
|
|
### 2. Build Configuration (build.yaml)
|
|
|
|
Configured to generate code for:
|
|
- `**_provider.dart` files
|
|
- 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`:
|
|
```dart
|
|
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
|
|
|
|
```bash
|
|
flutter pub get
|
|
```
|
|
|
|
### 2. Generate Provider Code
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
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:**
|
|
|
|
1. **connectivityProvider** - Connectivity instance
|
|
```dart
|
|
final connectivity = ref.watch(connectivityProvider);
|
|
```
|
|
|
|
2. **connectivityStreamProvider** - Real-time connectivity stream
|
|
```dart
|
|
final status = ref.watch(connectivityStreamProvider);
|
|
status.when(
|
|
data: (status) => Text('Status: $status'),
|
|
loading: () => CircularProgressIndicator(),
|
|
error: (e, _) => Text('Error: $e'),
|
|
);
|
|
```
|
|
|
|
3. **currentConnectivityProvider** - One-time connectivity check
|
|
```dart
|
|
final status = await ref.read(currentConnectivityProvider.future);
|
|
```
|
|
|
|
4. **isOnlineProvider** - Boolean online/offline stream
|
|
```dart
|
|
final isOnline = ref.watch(isOnlineProvider);
|
|
```
|
|
|
|
**Usage Example:**
|
|
|
|
```dart
|
|
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:
|
|
|
|
```dart
|
|
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`:
|
|
|
|
```dart
|
|
@riverpod
|
|
Future<String> example(ExampleRef ref) async {
|
|
ref.watch(provider1);
|
|
ref.read(provider2);
|
|
ref.listen(provider3, (prev, next) {});
|
|
}
|
|
```
|
|
|
|
### 3. Family as Function Parameters
|
|
|
|
```dart
|
|
// 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
|
|
|
|
```dart
|
|
// 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:
|
|
|
|
```dart
|
|
@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
|
|
|
|
```dart
|
|
@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)
|
|
|
|
```dart
|
|
@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)
|
|
|
|
```dart
|
|
@riverpod
|
|
Future<User> currentUser(CurrentUserRef ref) async {
|
|
final token = await ref.watch(authTokenProvider.future);
|
|
return await fetchUser(token);
|
|
}
|
|
```
|
|
|
|
### 3. StreamProvider (Real-time Data)
|
|
|
|
```dart
|
|
@riverpod
|
|
Stream<List<Message>> chatMessages(ChatMessagesRef ref, String roomId) {
|
|
return ref.watch(webSocketProvider).messages(roomId);
|
|
}
|
|
```
|
|
|
|
### 4. Notifier (Mutable State)
|
|
|
|
```dart
|
|
@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)
|
|
|
|
```dart
|
|
@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)
|
|
|
|
```dart
|
|
@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
|
|
|
|
```dart
|
|
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
|
|
|
|
```dart
|
|
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)
|
|
|
|
```dart
|
|
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
|
|
|
|
```dart
|
|
// 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
|
|
|
|
```dart
|
|
@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
|
|
|
|
```dart
|
|
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
|
|
|
|
```dart
|
|
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
|
|
|
|
```dart
|
|
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
|
|
|
|
```bash
|
|
# 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 usage
|
|
- `scoped_providers_should_specify_dependencies` - Scoped provider safety
|
|
- `avoid_public_notifier_properties` - Encapsulation
|
|
- `avoid_ref_read_inside_build` - Performance (don't use ref.read in build)
|
|
- `avoid_manual_providers_as_generated_provider_dependency` - Use generated providers
|
|
- `functional_ref` - Proper ref usage
|
|
- `notifier_build` - Proper Notifier implementation
|
|
|
|
## Common Issues & Solutions
|
|
|
|
### Issue 1: Generated files not found
|
|
|
|
**Solution:**
|
|
```bash
|
|
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
|
|
|
|
```dart
|
|
// 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
|
|
|
|
```dart
|
|
// 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
|
|
|
|
- [Riverpod Documentation](https://riverpod.dev)
|
|
- [Code Generation Guide](https://riverpod.dev/docs/concepts/about_code_generation)
|
|
- [Migration Guide](https://riverpod.dev/docs/migration/from_state_notifier)
|
|
- [Provider Examples](./lib/core/providers/provider_examples.dart)
|
|
|
|
## Next Steps
|
|
|
|
1. Run `flutter pub get` to install dependencies
|
|
2. Run `dart run build_runner watch -d` to start code generation
|
|
3. Create feature-specific providers in `lib/features/*/presentation/providers/`
|
|
4. Follow the patterns in `provider_examples.dart`
|
|
5. Use connectivity_provider as a reference for real-world implementation
|
|
|
|
## Support
|
|
|
|
For questions or issues:
|
|
1. Check provider_examples.dart for patterns
|
|
2. Review the Riverpod documentation
|
|
3. Run custom_lint to catch common mistakes
|
|
4. Use ref.watch() in build methods, ref.read() in event handlers
|