Loading...
Loading...
Expert knowledge in Flutter Riverpod state management (2025 best practices). Use when working with Riverpod, Flutter state management, AsyncNotifier, provider types, code generation with riverpod_generator, state synchronization, or when the user mentions data fetching, mutations, reactive state, performance optimization, or testing in Flutter apps. Covers AsyncNotifierProvider patterns, repository architecture, autoDispose, family providers, and common anti-patterns to avoid.
npx skill4agent add juparave/dotfiles flutter-riverpod-expert@riverpodriverpod_generatorselect()Provider
String apiKey(Ref ref) => 'YOUR_API_KEY';
int totalPrice(Ref ref) {
final cart = ref.watch(cartProvider);
return cart.items.fold(0, (sum, item) => sum + item.price);
}NotifierProvider
class Counter extends _$Counter {
int build() => 0;
void increment() => state++;
void decrement() => state = max(0, state - 1);
}AsyncNotifierProvider
class TodoList extends _$TodoList {
Future<List<Todo>> build() async {
final repo = ref.watch(todoRepositoryProvider);
return repo.fetchTodos();
}
Future<void> addTodo(String title) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final repo = ref.read(todoRepositoryProvider);
await repo.createTodo(title);
return repo.fetchTodos();
});
}
Future<void> deleteTodo(String id) async {
// Optimistic update
state = AsyncData(state.value!.where((t) => t.id != id).toList());
try {
await ref.read(todoRepositoryProvider).deleteTodo(id);
} catch (e) {
ref.invalidateSelf(); // Rollback on error
}
}
}StreamProvider
Stream<User?> authState(Ref ref) {
return FirebaseAuth.instance.authStateChanges();
}dependencies:
flutter_riverpod: ^2.5.0
riverpod_annotation: ^2.3.0
dev_dependencies:
build_runner: ^2.4.0
riverpod_generator: ^2.4.0
custom_lint: ^0.6.0
riverpod_lint: ^2.3.0import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'filename.g.dart'; // REQUIRED
class MyProvider extends _$MyProvider {
Future<Data> build() async => fetchData();
}# Watch mode (RECOMMENDED during development)
dart run build_runner watch -d
# One-time generation
dart run build_runner build --delete-conflicting-outputs// ❌ BAD: Rebuilds on ANY product change
final product = ref.watch(productProvider);
return Text('\$${product.price}');
// ✅ GOOD: Only rebuilds when price changes
final price = ref.watch(productProvider.select((p) => p.price));
return Text('\$$price');
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(todoListProvider);
return ListView(...);
}final count = ref.watch(todoListProvider.select((todos) => todos.length));
final isAdult = ref.watch(personProvider.select((p) => p.age >= 18));onPressed: () {
ref.read(todoListProvider.notifier).addTodo('New task');
}
// ❌ NEVER use read() in build to "optimize" - it won't rebuild!ref.listen<AsyncValue<List<Todo>>>(
todoListProvider,
(previous, next) {
next.whenOrNull(
error: (error, stack) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $error')),
);
},
);
},
);// ❌ BAD: Causes performance issues
ListView.builder(
itemBuilder: (context, index) {
final todo = ref.watch(todoProvider(ids[index])); // DON'T!
return ListTile(...);
},
);
// ✅ GOOD: Separate widget for each item
class TodoItem extends ConsumerWidget {
const TodoItem({required this.todoId});
final String todoId;
Widget build(BuildContext context, WidgetRef ref) {
final todo = ref.watch(todoProvider(todoId));
return ListTile(title: Text(todo.title));
}
}
class TodoList extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final ids = ref.watch(todoIdsProvider);
return ListView.builder(
itemCount: ids.length,
itemBuilder: (context, index) => TodoItem(todoId: ids[index]),
);
}
}// ✅ GOOD: Separate provider for computed state
List<Todo> filteredSortedTodos(Ref ref) {
final todos = ref.watch(todoListProvider);
final filter = ref.watch(filterProvider);
final sortOrder = ref.watch(sortOrderProvider);
final filtered = todos.where((t) => t.matches(filter)).toList();
return filtered..sort(sortOrder.comparator);
}
TodoRepository todoRepository(Ref ref) {
return TodoRepository(dio: ref.watch(dioProvider));
}
class TodoRepository {
TodoRepository({required this.dio});
final Dio dio;
Future<List<Todo>> fetchTodos() async {
final response = await dio.get('/todos');
return (response.data as List)
.map((json) => Todo.fromJson(json))
.toList();
}
Future<Todo> createTodo(String title) async {
final response = await dio.post('/todos', data: {'title': title});
return Todo.fromJson(response.data);
}
Future<void> deleteTodo(String id) async {
await dio.delete('/todos/$id');
}
}
class TodoList extends _$TodoList {
Future<List<Todo>> build() async {
final repository = ref.watch(todoRepositoryProvider);
return repository.fetchTodos();
}
Future<void> addTodo(String title) async {
final repository = ref.read(todoRepositoryProvider);
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await repository.createTodo(title);
return repository.fetchTodos();
});
}
}class TodoListScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final todosAsync = ref.watch(todoListProvider);
return Scaffold(
appBar: AppBar(title: const Text('Todos')),
body: todosAsync.when(
data: (todos) => ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) => TodoTile(todos[index]),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorView(error: error),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddDialog(context, ref),
child: const Icon(Icons.add),
),
);
}
}// Services
Dio dio(Ref ref) {
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
dio.interceptors.add(LogInterceptor());
return dio;
}
Future<SharedPreferences> sharedPreferences(Ref ref) async {
return await SharedPreferences.getInstance();
}
AuthService authService(Ref ref) {
return AuthService(
dio: ref.watch(dioProvider),
storage: ref.watch(sharedPreferencesProvider).value!,
);
}
// Repositories depend on services
UserRepository userRepository(Ref ref) {
return UserRepository(
dio: ref.watch(dioProvider),
authService: ref.watch(authServiceProvider),
);
}
// State providers depend on repositories
class CurrentUser extends _$CurrentUser {
Future<User?> build() async {
final authService = ref.watch(authServiceProvider);
final userId = await authService.getCurrentUserId();
if (userId == null) return null;
final repository = ref.watch(userRepositoryProvider);
return repository.fetchUser(userId);
}
Future<void> logout() async {
final authService = ref.read(authServiceProvider);
await authService.logout();
ref.invalidateSelf();
}
}// Simple family provider
Future<User> user(Ref ref, String id) async {
final dio = ref.watch(dioProvider);
final response = await dio.get('/users/$id');
return User.fromJson(response.data);
}
// Usage
final user = ref.watch(userProvider('123'));
// Family with AsyncNotifier
class UserNotifier extends _$UserNotifier {
Future<User> build(String id) async {
final repo = ref.watch(userRepositoryProvider);
return repo.fetchUser(id);
}
Future<void> updateName(String newName) async {
final userId = arg; // Access the parameter via 'arg'
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await ref.read(userRepositoryProvider).updateUser(userId, name: newName);
return ref.read(userRepositoryProvider).fetchUser(userId);
});
}
}
// Complex parameters need proper equality
class UserFilter {
const UserFilter({required this.role, required this.active});
final String role;
final bool active;
bool operator ==(Object other) =>
identical(this, other) ||
other is UserFilter &&
role == other.role &&
active == other.active;
int get hashCode => Object.hash(role, active);
}
Future<List<User>> filteredUsers(Ref ref, UserFilter filter) async {
return fetchUsers(filter);
}// Default: auto-dispose when no listeners
Future<String> data(Ref ref) async => fetchData();
// Keep alive permanently
(keepAlive: true)
Future<Config> config(Ref ref) async => loadConfig();
// Conditional keep alive - cache on success
Future<String> cachedData(Ref ref) async {
final data = await fetchData();
ref.keepAlive(); // Cache this result forever
return data;
}
// Timed cache (5 minutes)
Future<String> timedCache(Ref ref) async {
final data = await fetchData();
final link = ref.keepAlive();
Timer(const Duration(minutes: 5), link.close);
return data;
}
// Manual disposal - cleanup resources
Stream<int> websocket(Ref ref) {
final client = WebSocketClient();
ref.onDispose(() {
client.close(); // Cleanup when provider is disposed
});
return client.stream;
}
class TodoList extends _$TodoList {
Future<List<Todo>> build() async {
try {
final repository = ref.watch(todoRepositoryProvider);
return await repository.fetchTodos();
} on DioException catch (e) {
if (e.response?.statusCode == 401) {
ref.read(authServiceProvider).logout();
throw UnauthorizedException();
}
throw NetworkException(e.message);
} catch (e) {
throw UnexpectedException(e.toString());
}
}
Future<void> addTodo(String title) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final repository = ref.read(todoRepositoryProvider);
await repository.createTodo(title);
return repository.fetchTodos();
});
}
}// Using .when()
todosAsync.when(
data: (todos) => ListView.builder(...),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) {
if (error is NetworkException) {
return ErrorView(
message: 'Network error. Check your connection.',
onRetry: () => ref.invalidate(todoListProvider),
);
}
if (error is UnauthorizedException) {
return const ErrorView(message: 'Please log in again.');
}
return ErrorView(message: 'Error: $error');
},
);
// Listen for errors (side effects)
ref.listen<AsyncValue<List<Todo>>>(
todoListProvider,
(previous, next) {
next.whenOrNull(
error: (error, stack) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(error.toString()),
backgroundColor: Colors.red,
),
);
},
);
},
);test('TodoList fetches todos correctly', () async {
final container = ProviderContainer.test(
overrides: [
todoRepositoryProvider.overrideWithValue(MockTodoRepository()),
],
);
final todos = await container.read(todoListProvider.future);
expect(todos.length, 2);
expect(todos[0].title, 'Test Todo 1');
});
test('TodoList adds todo correctly', () async {
final mockRepo = MockTodoRepository();
final container = ProviderContainer.test(
overrides: [
todoRepositoryProvider.overrideWithValue(mockRepo),
],
);
await container.read(todoListProvider.notifier).addTodo('New Todo');
verify(() => mockRepo.createTodo('New Todo')).called(1);
});testWidgets('TodoListScreen displays todos', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
todoRepositoryProvider.overrideWithValue(MockTodoRepository()),
],
child: const MaterialApp(home: TodoListScreen()),
),
);
await tester.pumpAndSettle();
expect(find.text('Test Todo 1'), findsOneWidget);
expect(find.text('Test Todo 2'), findsOneWidget);
});// ❌ Using ref.read() to avoid rebuilds
final todos = ref.read(todoListProvider); // Won't rebuild!
// ✅ Use ref.watch() or ref.select()
final count = ref.watch(todoListProvider.select((todos) => todos.length));// ❌ Not disposing resources
Stream<int> badWebsocket(Ref ref) {
final client = WebSocketClient();
return client.stream; // Never closed!
}
// ✅ Dispose resources
Stream<int> goodWebsocket(Ref ref) {
final client = WebSocketClient();
ref.onDispose(() => client.close());
return client.stream;
}// ❌ BAD: Which is the source of truth?
class BadWidget extends StatefulWidget {
int localCount = 0; // Local state
Widget build(BuildContext context, WidgetRef ref) {
final providerCount = ref.watch(counterProvider); // Provider state
return Text('$localCount vs $providerCount'); // Confusing!
}
}
// ✅ GOOD: Single source of truth
class GoodWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}// ❌ BAD
Future<void> logout() async {
state = null;
// Other providers still have old user data!
}
// ✅ GOOD: Invalidate dependent providers
Future<void> logout() async {
state = null;
ref.invalidate(userProfileProvider);
ref.invalidate(userSettingsProvider);
ref.invalidate(userNotificationsProvider);
}
// ✅ EVEN BETTER: Make providers watch auth
Future<UserProfile> userProfile(Ref ref) async {
final user = ref.watch(authProvider);
if (user == null) throw UnauthenticatedException();
return fetchUserProfile(user.id); // Auto-refetches when user changes
}@riverpodselect()/Users/pablito/EVOworkspace/flutter/CesarferPromotoresFlutter/promotores/RIVERPOD_2025_BEST_PRACTICES.md