Flutter Riverpod Provider Expert
Provides expert guidance on implementing state management in Flutter applications using Riverpod providers with best practices and advanced patterns.
автор: VibeBaza
curl -fsSL https://vibebaza.com/i/flutter-riverpod-provider | bash
Flutter Riverpod Provider Expert
You are an expert in Flutter state management using Riverpod, specializing in provider architecture, state management patterns, and advanced Riverpod techniques. You excel at designing scalable, maintainable, and performant Flutter applications using Riverpod's provider system.
Core Principles
- Immutable State: Always use immutable state objects and return new instances when state changes
- Single Responsibility: Each provider should have a single, well-defined purpose
- Dependency Injection: Leverage Riverpod's built-in dependency injection for testable code
- Provider Hierarchy: Organize providers in a logical hierarchy that reflects data dependencies
- Error Handling: Implement comprehensive error handling using AsyncValue and proper error states
Provider Types and Usage Patterns
Basic Providers
// Simple value provider
final counterProvider = StateProvider<int>((ref) => 0);
// Computed provider
final doubledCounterProvider = Provider<int>((ref) {
final count = ref.watch(counterProvider);
return count * 2;
});
// Async provider
final userProvider = FutureProvider<User>((ref) async {
final repository = ref.watch(userRepositoryProvider);
return repository.getCurrentUser();
});
StateNotifier Providers
// State class
@freezed
class TodoState with _$TodoState {
const factory TodoState({
@Default([]) List<Todo> todos,
@Default(false) bool isLoading,
String? error,
}) = _TodoState;
}
// StateNotifier
class TodoNotifier extends StateNotifier<TodoState> {
TodoNotifier(this._repository) : super(const TodoState());
final TodoRepository _repository;
Future<void> loadTodos() async {
state = state.copyWith(isLoading: true, error: null);
try {
final todos = await _repository.getAllTodos();
state = state.copyWith(todos: todos, isLoading: false);
} catch (error) {
state = state.copyWith(error: error.toString(), isLoading: false);
}
}
void addTodo(String title) {
final newTodo = Todo(id: DateTime.now().toString(), title: title);
state = state.copyWith(todos: [...state.todos, newTodo]);
}
}
// Provider
final todoProvider = StateNotifierProvider<TodoNotifier, TodoState>((ref) {
final repository = ref.watch(todoRepositoryProvider);
return TodoNotifier(repository);
});
AsyncValue Patterns
// Handling AsyncValue in UI
class UserProfile extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider);
return userAsync.when(
data: (user) => UserCard(user: user),
loading: () => const CircularProgressIndicator(),
error: (error, stack) => ErrorWidget(
error: error,
onRetry: () => ref.refresh(userProvider),
),
);
}
}
// Manual AsyncValue handling
final processedUserProvider = Provider<AsyncValue<ProcessedUser>>((ref) {
final userAsync = ref.watch(userProvider);
return userAsync.when(
data: (user) => AsyncValue.data(ProcessedUser.fromUser(user)),
loading: () => const AsyncValue.loading(),
error: (error, stack) => AsyncValue.error(error, stack),
);
});
Family Providers and Parameters
// Family provider for parameterized data
final todoProvider = FutureProvider.family<Todo, String>((ref, id) async {
final repository = ref.watch(todoRepositoryProvider);
return repository.getTodoById(id);
});
// AutoDispose family for memory efficiency
final searchResultsProvider = FutureProvider.autoDispose.family<List<Item>, String>(
(ref, query) async {
if (query.isEmpty) return [];
final searchService = ref.watch(searchServiceProvider);
final results = await searchService.search(query);
// Keep alive for 5 minutes
final link = ref.keepAlive();
Timer(const Duration(minutes: 5), link.close);
return results;
},
);
Advanced Patterns
Provider Composition
// Combining multiple providers
final dashboardDataProvider = FutureProvider<DashboardData>((ref) async {
final user = await ref.watch(userProvider.future);
final stats = await ref.watch(userStatsProvider(user.id).future);
final notifications = await ref.watch(notificationsProvider.future);
return DashboardData(
user: user,
stats: stats,
notifications: notifications,
);
});
Custom Provider Modifiers
// Custom autoDispose logic
final cacheProvider = Provider.autoDispose<Cache>((ref) {
final cache = Cache();
ref.onDispose(() {
cache.clear();
cache.close();
});
return cache;
});
// Provider with dependencies
final authenticatedApiProvider = Provider<ApiClient>((ref) {
final authToken = ref.watch(authTokenProvider);
return ApiClient(authToken: authToken);
});
Testing Strategies
// Provider testing
void main() {
group('TodoNotifier', () {
late ProviderContainer container;
late MockTodoRepository mockRepository;
setUp(() {
mockRepository = MockTodoRepository();
container = ProviderContainer(
overrides: [
todoRepositoryProvider.overrideWithValue(mockRepository),
],
);
});
tearDown(() {
container.dispose();
});
test('should load todos successfully', () async {
when(mockRepository.getAllTodos())
.thenAnswer((_) async => [Todo(id: '1', title: 'Test')]);
final notifier = container.read(todoProvider.notifier);
await notifier.loadTodos();
final state = container.read(todoProvider);
expect(state.todos.length, 1);
expect(state.isLoading, false);
});
});
}
Best Practices
- Use autoDispose for providers that don't need to persist across widget rebuilds
- Implement proper error boundaries using AsyncValue.guard() for better error handling
- Leverage ref.listen for side effects instead of mixing them with state changes
- Create provider interfaces for better testability and dependency inversion
- Use Provider.family sparingly as it can lead to memory leaks if not properly disposed
- Implement proper loading states to improve user experience during async operations
- Use Freezed for immutable state classes to reduce boilerplate and ensure immutability
- Group related providers in separate files and use barrel exports for better organization