Back to Posts

Flutter State Management with Riverpod: A Comprehensive Guide

9 min read

Riverpod is a powerful state management solution for Flutter that provides compile-time safety, dependency injection, and testability. This guide covers everything you need to know about using Riverpod effectively in your Flutter applications.

1. Getting Started with Riverpod

Basic Setup

// pubspec.yaml
dependencies:
  flutter_riverpod: ^2.4.0
  riverpod_annotation: ^2.3.0

dev_dependencies:
  riverpod_generator: ^2.3.0
  build_runner: ^2.4.0

// main.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

Simple Providers

// Basic provider
final counterProvider = StateProvider<int>((ref) => 0);

// Provider usage
class CounterWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    
    return Text('Count: $count');
  }
}

// Modifying state
class CounterButton extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ElevatedButton(
      onPressed: () => ref.read(counterProvider.notifier).state++,
      child: Text('Increment'),
    );
  }
}

2. Advanced Provider Types

StateNotifier Provider

// State class
@immutable
class TodoState {
  final List<Todo> todos;
  final bool isLoading;
  
  const TodoState({
    required this.todos,
    this.isLoading = false,
  });
  
  TodoState copyWith({
    List<Todo>? todos,
    bool? isLoading,
  }) {
    return TodoState(
      todos: todos ?? this.todos,
      isLoading: isLoading ?? this.isLoading,
    );
  }
}

// StateNotifier class
class TodoNotifier extends StateNotifier<TodoState> {
  TodoNotifier() : super(TodoState(todos: []));
  
  Future<void> fetchTodos() async {
    state = state.copyWith(isLoading: true);
    try {
      final todos = await TodoRepository().fetchTodos();
      state = state.copyWith(
        todos: todos,
        isLoading: false,
      );
    } catch (e) {
      state = state.copyWith(isLoading: false);
      throw e;
    }
  }
  
  void addTodo(Todo todo) {
    state = state.copyWith(
      todos: [...state.todos, todo],
    );
  }
  
  void removeTodo(String id) {
    state = state.copyWith(
      todos: state.todos.where((todo) => todo.id != id).toList(),
    );
  }
}

// Provider definition
final todoProvider = StateNotifierProvider<TodoNotifier, TodoState>((ref) {
  return TodoNotifier();
});

// Usage in widget
class TodoList extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todoState = ref.watch(todoProvider);
    
    if (todoState.isLoading) {
      return CircularProgressIndicator();
    }
    
    return ListView.builder(
      itemCount: todoState.todos.length,
      itemBuilder: (context, index) {
        final todo = todoState.todos[index];
        return ListTile(
          title: Text(todo.title),
          trailing: IconButton(
            icon: Icon(Icons.delete),
            onPressed: () => ref.read(todoProvider.notifier).removeTodo(todo.id),
          ),
        );
      },
    );
  }
}

Future Provider

@riverpod
Future<List<User>> users(UsersRef ref) async {
  final repository = ref.watch(repositoryProvider);
  return repository.fetchUsers();
}

// Usage with AsyncValue
class UserList extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final usersAsync = ref.watch(usersProvider);
    
    return usersAsync.when(
      data: (users) => ListView.builder(
        itemCount: users.length,
        itemBuilder: (context, index) => UserTile(user: users[index]),
      ),
      loading: () => CircularProgressIndicator(),
      error: (error, stack) => ErrorWidget(error.toString()),
    );
  }
}

Stream Provider

@riverpod
Stream<List<Message>> messages(MessagesRef ref) {
  final repository = ref.watch(repositoryProvider);
  return repository.watchMessages();
}

// Usage
class MessageList extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final messagesAsync = ref.watch(messagesProvider);
    
    return messagesAsync.when(
      data: (messages) => ListView.builder(
        itemCount: messages.length,
        itemBuilder: (context, index) => MessageTile(message: messages[index]),
      ),
      loading: () => CircularProgressIndicator(),
      error: (error, stack) => ErrorWidget(error.toString()),
    );
  }
}

3. Dependency Injection

Service Providers

// API client provider
@riverpod
HttpClient apiClient(ApiClientRef ref) {
  return HttpClient(
    baseUrl: 'https://api.example.com',
    timeout: Duration(seconds: 30),
  );
}

// Repository provider
@riverpod
UserRepository userRepository(UserRepositoryRef ref) {
  final client = ref.watch(apiClientProvider);
  return UserRepository(client);
}

// Service provider
@riverpod
AuthService authService(AuthServiceRef ref) {
  final repository = ref.watch(userRepositoryProvider);
  return AuthService(repository);
}

Scoped Providers

@riverpod
class ThemeNotifier extends _$ThemeNotifier {
  @override
  ThemeMode build() => ThemeMode.system;
  
  void toggleTheme() {
    state = state == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
  }
}

// Usage with override
void main() {
  runApp(
    ProviderScope(
      overrides: [
        themeProvider.overrideWith((ref) => ThemeMode.dark),
      ],
      child: MyApp(),
    ),
  );
}

4. State Management Patterns

Repository Pattern

// Repository interface
abstract class Repository<T> {
  Future<List<T>> getAll();
  Future<T> getById(String id);
  Future<void> create(T item);
  Future<void> update(T item);
  Future<void> delete(String id);
}

// Implementation
class UserRepository implements Repository<User> {
  final HttpClient _client;
  
  UserRepository(this._client);
  
  @override
  Future<List<User>> getAll() async {
    final response = await _client.get('/users');
    return response.map((json) => User.fromJson(json)).toList();
  }
  
  // Other implementations...
}

// Provider
@riverpod
UserRepository userRepository(UserRepositoryRef ref) {
  final client = ref.watch(apiClientProvider);
  return UserRepository(client);
}

Service Pattern

class AuthService {
  final UserRepository _repository;
  
  AuthService(this._repository);
  
  Future<User> login(String email, String password) async {
    try {
      final user = await _repository.login(email, password);
      return user;
    } catch (e) {
      throw AuthException('Login failed');
    }
  }
}

// Provider
@riverpod
class AuthNotifier extends _$AuthNotifier {
  @override
  FutureOr<User?> build() => null;
  
  Future<void> login(String email, String password) async {
    state = const AsyncValue.loading();
    try {
      final service = ref.read(authServiceProvider);
      final user = await service.login(email, password);
      state = AsyncValue.data(user);
    } catch (e, stack) {
      state = AsyncValue.error(e, stack);
    }
  }
}

5. Testing with Riverpod

Provider Tests

void main() {
  test('Counter increments', () {
    final container = ProviderContainer();
    addTearDown(container.dispose);
    
    expect(container.read(counterProvider), 0);
    
    container.read(counterProvider.notifier).state++;
    expect(container.read(counterProvider), 1);
  });
  
  test('TodoNotifier adds todo', () {
    final container = ProviderContainer();
    addTearDown(container.dispose);
    
    final notifier = container.read(todoProvider.notifier);
    final todo = Todo(id: '1', title: 'Test');
    
    notifier.addTodo(todo);
    
    expect(container.read(todoProvider).todos, contains(todo));
  });
}

Mock Providers

class MockUserRepository implements UserRepository {
  @override
  Future<List<User>> getAll() async => [
    User(id: '1', name: 'Test User'),
  ];
}

void main() {
  test('UserService with mock repository', () {
    final container = ProviderContainer(
      overrides: [
        userRepositoryProvider.overrideWithValue(MockUserRepository()),
      ],
    );
    addTearDown(container.dispose);
    
    expect(
      container.read(userServiceProvider).getUsers(),
      completion(isA<List<User>>()),
    );
  });
}

Best Practices

  1. Provider Organization
// Organize providers by feature
class UserProviders {
  static final userProvider = StateNotifierProvider<UserNotifier, UserState>((ref) {
    return UserNotifier(ref.watch(userRepositoryProvider));
  });
  
  static final userListProvider = FutureProvider<List<User>>((ref) {
    return ref.watch(userRepositoryProvider).getUsers();
  });
}
  1. Error Handling
@riverpod
class ApiNotifier extends _$ApiNotifier {
  @override
  FutureOr<T> build<T>(Future<T> Function() apiCall) async {
    try {
      return await apiCall();
    } catch (e) {
      if (e is NetworkException) {
        // Handle network errors
      }
      rethrow;
    }
  }
}
  1. State Immutability
@immutable
class AppState {
  final User? user;
  final ThemeMode themeMode;
  final List<Todo> todos;
  
  const AppState({
    this.user,
    required this.themeMode,
    required this.todos,
  });
  
  AppState copyWith({
    User? user,
    ThemeMode? themeMode,
    List<Todo>? todos,
  }) {
    return AppState(
      user: user ?? this.user,
      themeMode: themeMode ?? this.themeMode,
      todos: todos ?? this.todos,
    );
  }
}

Conclusion

Effective state management with Riverpod requires:

  1. Provider Organization

    • Group related providers
    • Use appropriate provider types
    • Maintain clear dependencies
  2. State Design

    • Immutable state objects
    • Clear state updates
    • Proper error handling
  3. Testing

    • Unit test providers
    • Mock dependencies
    • Test error cases
  4. Performance

    • Minimize rebuilds
    • Use select for granular updates
    • Cache expensive computations

Remember to:

  • Keep providers focused and single-purpose
  • Handle errors appropriately
  • Test thoroughly
  • Document provider dependencies
  • Use code generation for boilerplate reduction

By following these practices, you can create maintainable and scalable Flutter applications with Riverpod state management.