473 lines
14 KiB
Dart
473 lines
14 KiB
Dart
// ignore_for_file: unreachable_from_main
|
|
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
|
|
part 'provider_examples.g.dart';
|
|
|
|
/// ============================================================================
|
|
/// RIVERPOD 3.0 PROVIDER EXAMPLES WITH CODE GENERATION
|
|
/// ============================================================================
|
|
/// This file contains comprehensive examples of Riverpod 3.0 patterns
|
|
/// using the @riverpod annotation and code generation.
|
|
///
|
|
/// Key Changes in Riverpod 3.0:
|
|
/// - Unified Ref type (no more FutureProviderRef, StreamProviderRef, etc.)
|
|
/// - Simplified Notifier classes (no more separate AutoDisposeNotifier)
|
|
/// - Automatic retry for failed providers
|
|
/// - ref.mounted check after async operations
|
|
/// - Improved family parameters (just function parameters!)
|
|
/// ============================================================================
|
|
|
|
// ============================================================================
|
|
// 1. SIMPLE IMMUTABLE VALUE PROVIDER
|
|
// ============================================================================
|
|
|
|
/// Simple provider that returns an immutable value
|
|
/// Use this for constants, configurations, or computed values
|
|
@riverpod
|
|
String appVersion(Ref ref) {
|
|
return '1.0.0';
|
|
}
|
|
|
|
/// Provider with computation
|
|
@riverpod
|
|
int pointsMultiplier(Ref ref) {
|
|
// Can read other providers
|
|
final userTier = 'diamond'; // This would come from another provider
|
|
return userTier == 'diamond'
|
|
? 3
|
|
: userTier == 'platinum'
|
|
? 2
|
|
: 1;
|
|
}
|
|
|
|
// ============================================================================
|
|
// 2. ASYNC DATA FETCHING (FutureProvider pattern)
|
|
// ============================================================================
|
|
|
|
/// Async provider for fetching data once
|
|
/// Automatically handles loading and error states via AsyncValue
|
|
@riverpod
|
|
Future<String> userData(Ref ref) async {
|
|
// Simulate API call
|
|
await Future<void>.delayed(const Duration(seconds: 1));
|
|
return 'User Data';
|
|
}
|
|
|
|
/// Async provider with parameters (Family pattern)
|
|
/// Parameters are just function parameters - much simpler than before!
|
|
@riverpod
|
|
Future<String> userProfile(Ref ref, String userId) async {
|
|
// Simulate API call with userId
|
|
await Future<void>.delayed(const Duration(seconds: 1));
|
|
return 'Profile for user: $userId';
|
|
}
|
|
|
|
/// Async provider with multiple parameters
|
|
/// Named parameters, optional parameters, defaults - all supported!
|
|
@riverpod
|
|
Future<List<String>> productList(
|
|
Ref ref, {
|
|
required String category,
|
|
int page = 1,
|
|
int limit = 20,
|
|
String? searchQuery,
|
|
}) async {
|
|
// Simulate API call with parameters
|
|
await Future<void>.delayed(const Duration(milliseconds: 500));
|
|
return ['Product 1', 'Product 2', 'Product 3'];
|
|
}
|
|
|
|
// ============================================================================
|
|
// 3. STREAM PROVIDER
|
|
// ============================================================================
|
|
|
|
/// Stream provider for real-time data
|
|
/// Use this for WebSocket connections, real-time updates, etc.
|
|
@riverpod
|
|
Stream<int> timer(Ref ref) {
|
|
return Stream.periodic(const Duration(seconds: 1), (count) => count);
|
|
}
|
|
|
|
/// Stream provider with parameters
|
|
@riverpod
|
|
Stream<String> chatMessages(Ref ref, String roomId) {
|
|
// Simulate WebSocket stream
|
|
return Stream.periodic(
|
|
const Duration(seconds: 2),
|
|
(count) => 'Message $count in room $roomId',
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// 4. NOTIFIER - MUTABLE STATE WITH METHODS
|
|
// ============================================================================
|
|
|
|
/// Notifier for mutable state with methods
|
|
/// Use this when you need to expose methods to modify state
|
|
///
|
|
/// The @riverpod annotation generates:
|
|
/// - counterProvider: Access the state
|
|
/// - counterProvider.notifier: Access the notifier methods
|
|
@riverpod
|
|
class Counter extends _$Counter {
|
|
/// Build method returns the initial state
|
|
@override
|
|
int build() {
|
|
return 0;
|
|
}
|
|
|
|
/// Methods to modify state
|
|
void increment() {
|
|
state++;
|
|
}
|
|
|
|
void decrement() {
|
|
state--;
|
|
}
|
|
|
|
void reset() {
|
|
state = 0;
|
|
}
|
|
|
|
void add(int value) {
|
|
state += value;
|
|
}
|
|
}
|
|
|
|
/// Notifier with parameters (Family pattern)
|
|
@riverpod
|
|
class CartQuantity extends _$CartQuantity {
|
|
/// Parameters become properties you can access
|
|
@override
|
|
int build(String productId) {
|
|
// Initialize with 0 or load from local storage
|
|
return 0;
|
|
}
|
|
|
|
void increment() {
|
|
state++;
|
|
}
|
|
|
|
void decrement() {
|
|
if (state > 0) {
|
|
state--;
|
|
}
|
|
}
|
|
|
|
void setQuantity(int quantity) {
|
|
state = quantity.clamp(0, 99);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 5. ASYNC NOTIFIER - MUTABLE STATE WITH ASYNC INITIALIZATION
|
|
// ============================================================================
|
|
|
|
/// AsyncNotifier for state that requires async initialization
|
|
/// Perfect for fetching data that can then be modified
|
|
///
|
|
/// State type: AsyncValue<UserProfile>
|
|
/// - AsyncValue.data(profile) when loaded
|
|
/// - AsyncValue.loading() when loading
|
|
/// - AsyncValue.error(error, stack) when error
|
|
@riverpod
|
|
class UserProfileNotifier extends _$UserProfileNotifier {
|
|
/// Build method returns Future of the initial state
|
|
@override
|
|
Future<UserProfileData> build() async {
|
|
// Fetch initial data
|
|
await Future<void>.delayed(const Duration(seconds: 1));
|
|
return UserProfileData(name: 'John Doe', email: 'john@example.com');
|
|
}
|
|
|
|
/// Method to update profile
|
|
/// Uses AsyncValue.guard() for proper error handling
|
|
Future<void> updateName(String name) async {
|
|
// Set loading state
|
|
state = const AsyncValue.loading();
|
|
|
|
// Update state with error handling
|
|
state = await AsyncValue.guard(() async {
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
|
|
final currentProfile = await future; // Get current data
|
|
return currentProfile.copyWith(name: name);
|
|
});
|
|
}
|
|
|
|
/// Refresh data
|
|
Future<void> refresh() async {
|
|
state = const AsyncValue.loading();
|
|
|
|
state = await AsyncValue.guard(() async {
|
|
await Future.delayed(const Duration(seconds: 1));
|
|
return UserProfileData(name: 'Refreshed', email: 'refresh@example.com');
|
|
});
|
|
}
|
|
|
|
/// Method with ref.mounted check (Riverpod 3.0 feature)
|
|
Future<void> updateWithMountedCheck(String name) async {
|
|
await Future.delayed(const Duration(seconds: 2));
|
|
|
|
// Check if provider is still mounted after async operation
|
|
if (!ref.mounted) return;
|
|
|
|
state = AsyncValue.data(
|
|
UserProfileData(name: name, email: 'email@example.com'),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Simple data class for example
|
|
class UserProfileData {
|
|
UserProfileData({required this.name, required this.email});
|
|
final String name;
|
|
final String email;
|
|
|
|
UserProfileData copyWith({String? name, String? email}) {
|
|
return UserProfileData(name: name ?? this.name, email: email ?? this.email);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 6. STREAM NOTIFIER - MUTABLE STATE FROM STREAM
|
|
// ============================================================================
|
|
|
|
/// StreamNotifier for state that comes from a stream but can be modified
|
|
/// Use for WebSocket connections with additional actions
|
|
@riverpod
|
|
class LiveChatNotifier extends _$LiveChatNotifier {
|
|
@override
|
|
Stream<List<String>> build() {
|
|
// Return the stream
|
|
return Stream.periodic(
|
|
const Duration(seconds: 1),
|
|
(count) => ['Message $count'],
|
|
);
|
|
}
|
|
|
|
/// Send a new message
|
|
Future<void> sendMessage(String message) async {
|
|
// Send via WebSocket/API
|
|
await Future.delayed(const Duration(milliseconds: 100));
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 7. PROVIDER DEPENDENCIES (COMPOSITION)
|
|
// ============================================================================
|
|
|
|
/// Provider that depends on other providers
|
|
@riverpod
|
|
Future<String> dashboardData(Ref ref) async {
|
|
// Watch other providers
|
|
final userData = await ref.watch(userDataProvider.future);
|
|
final version = ref.watch(appVersionProvider);
|
|
|
|
return 'Dashboard for $userData on version $version';
|
|
}
|
|
|
|
/// Provider that selectively watches for optimization
|
|
/// Note: userProfileProvider is actually a Family provider (takes userId parameter)
|
|
/// In real code, you would use the generated AsyncNotifier provider
|
|
@riverpod
|
|
String userDisplayName(Ref ref) {
|
|
// Example: Watch a simple computed value based on other providers
|
|
final version = ref.watch(appVersionProvider);
|
|
|
|
// In a real app, you would watch the UserProfileNotifier like:
|
|
// final asyncProfile = ref.watch(userProfileNotifierProvider);
|
|
// return asyncProfile.when(...)
|
|
|
|
return 'User on version $version';
|
|
}
|
|
|
|
// ============================================================================
|
|
// 8. KEEP ALIVE vs AUTO DISPOSE
|
|
// ============================================================================
|
|
|
|
/// By default, generated providers are autoDispose
|
|
/// They clean up when no longer used
|
|
@riverpod
|
|
String autoDisposeExample(Ref ref) {
|
|
// This provider will be disposed when no longer watched
|
|
ref.onDispose(() {
|
|
// Clean up resources
|
|
});
|
|
|
|
return 'Auto disposed';
|
|
}
|
|
|
|
/// Keep alive provider - never auto-disposes
|
|
/// Use for global state, singletons, services
|
|
@Riverpod(keepAlive: true)
|
|
String keepAliveExample(Ref ref) {
|
|
// This provider stays alive until the app closes
|
|
return 'Kept alive';
|
|
}
|
|
|
|
// ============================================================================
|
|
// 9. LIFECYCLE HOOKS
|
|
// ============================================================================
|
|
|
|
/// Provider with lifecycle hooks
|
|
@riverpod
|
|
String lifecycleExample(Ref ref) {
|
|
// Called when provider is first created
|
|
ref.onDispose(() {
|
|
// Clean up when provider is disposed
|
|
print('Provider disposed');
|
|
});
|
|
|
|
ref.onCancel(() {
|
|
// Called when last listener is removed (before dispose)
|
|
print('Last listener removed');
|
|
});
|
|
|
|
ref.onResume(() {
|
|
// Called when a new listener is added after onCancel
|
|
print('New listener added');
|
|
});
|
|
|
|
return 'Lifecycle example';
|
|
}
|
|
|
|
// ============================================================================
|
|
// 10. INVALIDATION AND REFRESH
|
|
// ============================================================================
|
|
|
|
/// Provider showing how to invalidate and refresh
|
|
@riverpod
|
|
class DataManager extends _$DataManager {
|
|
@override
|
|
Future<String> build() async {
|
|
await Future.delayed(const Duration(seconds: 1));
|
|
return 'Initial data';
|
|
}
|
|
|
|
/// Refresh this provider's data
|
|
Future<void> refresh() async {
|
|
// Method 1: Use ref.invalidateSelf()
|
|
ref.invalidateSelf();
|
|
|
|
// Method 2: Manually update state
|
|
state = const AsyncValue.loading();
|
|
state = await AsyncValue.guard(() async {
|
|
await Future.delayed(const Duration(seconds: 1));
|
|
return 'Refreshed data';
|
|
});
|
|
}
|
|
|
|
/// Invalidate another provider
|
|
void invalidateOther() {
|
|
ref.invalidate(userDataProvider);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 11. ERROR HANDLING PATTERNS
|
|
// ============================================================================
|
|
|
|
/// Provider with comprehensive error handling
|
|
@riverpod
|
|
class ErrorHandlingExample extends _$ErrorHandlingExample {
|
|
@override
|
|
Future<String> build() async {
|
|
return await _fetchData();
|
|
}
|
|
|
|
Future<String> _fetchData() async {
|
|
try {
|
|
// Simulate API call
|
|
await Future.delayed(const Duration(seconds: 1));
|
|
return 'Success';
|
|
} catch (e) {
|
|
// Errors are automatically caught by AsyncValue
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Update with error handling
|
|
Future<void> updateData(String newData) async {
|
|
state = const AsyncValue.loading();
|
|
|
|
// AsyncValue.guard automatically catches errors
|
|
state = await AsyncValue.guard(() async {
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
return newData;
|
|
});
|
|
|
|
// Handle the result
|
|
state.when(
|
|
data: (data) {
|
|
// Success
|
|
print('Updated successfully: $data');
|
|
},
|
|
loading: () {
|
|
// Still loading
|
|
},
|
|
error: (error, stack) {
|
|
// Handle error
|
|
print('Update failed: $error');
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// USAGE IN WIDGETS
|
|
// ============================================================================
|
|
|
|
/*
|
|
|
|
// 1. Watching a simple provider
|
|
final version = ref.watch(appVersionProvider);
|
|
|
|
// 2. Watching async provider
|
|
final userData = ref.watch(userDataProvider);
|
|
userData.when(
|
|
data: (data) => Text(data),
|
|
loading: () => CircularProgressIndicator(),
|
|
error: (error, stack) => Text('Error: $error'),
|
|
);
|
|
|
|
// 3. Watching with family (parameters)
|
|
final profile = ref.watch(userProfileProvider('user123'));
|
|
|
|
// 4. Watching state from Notifier
|
|
final count = ref.watch(counterProvider);
|
|
|
|
// 5. Calling Notifier methods
|
|
ref.read(counterProvider.notifier).increment();
|
|
|
|
// 6. Watching AsyncNotifier state
|
|
final profileState = ref.watch(userProfileNotifierProvider);
|
|
|
|
// 7. Calling AsyncNotifier methods
|
|
await ref.read(userProfileNotifierProvider.notifier).updateName('New Name');
|
|
|
|
// 8. Selective watching for optimization
|
|
final userName = ref.watch(
|
|
userProfileNotifierProvider.select((async) => async.value?.name),
|
|
);
|
|
|
|
// 9. Invalidating a provider
|
|
ref.invalidate(userDataProvider);
|
|
|
|
// 10. Refreshing a provider
|
|
ref.refresh(userDataProvider);
|
|
|
|
// 11. Pattern matching (Dart 3.0+)
|
|
final profileState = ref.watch(userProfileNotifierProvider);
|
|
switch (profileState) {
|
|
case AsyncData(:final value):
|
|
return Text(value.name);
|
|
case AsyncError(:final error):
|
|
return Text('Error: $error');
|
|
case AsyncLoading():
|
|
return CircularProgressIndicator();
|
|
}
|
|
|
|
*/
|