Riverpod: The Future of Flutter State Management
Flutter Development
12 min read
September 10, 2025

Riverpod: The Future of Flutter State Management

Discover why Riverpod is revolutionizing Flutter state management. A friendly guide to understanding this powerful tool that makes app development smoother and more enjoyable.

Muhammad Nabi Rahmani

Muhammad Nabi Rahmani

Flutter Developer passionate about creating beautiful mobile experiences

Riverpod: The Future of Flutter State Management

Provider was the go-to state management for Flutter. Then Riverpod came along and fixed every problem Provider had — no BuildContext dependency, compile-time safety, auto-disposal, and proper testing support. Here's how it works in practice.

Setup

Add the dependency and wrap your app:

# pubspec.yaml
dependencies:
  flutter_riverpod: ^2.5.1
  riverpod_annotation: ^2.3.5

dev_dependencies:
  riverpod_generator: ^2.4.3
  build_runner: ^2.4.8
void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

ProviderScope stores all your provider state. Every provider you create lives inside this scope. That's it for setup.

The Five Provider Types You Actually Use

1. Provider — Static Values

For values that never change during the app's lifetime:

@riverpod
Dio dio(Ref ref) {
  return Dio(BaseOptions(
    baseUrl: 'https://api.example.com',
    connectTimeout: const Duration(seconds: 10),
  ));
}

2. FutureProvider — Async Data

For one-shot async operations like fetching data:

@riverpod
Future<User> currentUser(Ref ref) async {
  final dio = ref.watch(dioProvider);
  final response = await dio.get('/me');
  return User.fromJson(response.data);
}

In your widget:

class ProfileScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsync = ref.watch(currentUserProvider);

    return userAsync.when(
      loading: () => const CircularProgressIndicator(),
      error: (err, stack) => Text('Error: $err'),
      data: (user) => Text('Hello, ${user.name}'),
    );
  }
}

No manual loading states. No setState. The .when() method handles loading, error, and data — every time.

3. StreamProvider — Reactive Data

For real-time data streams:

@riverpod
Stream<List<ChatMessage>> chatMessages(Ref ref, String chatId) {
  final db = ref.watch(databaseProvider);
  return db.watchMessages(chatId);
}

4. NotifierProvider — Mutable State

For state that changes over time with defined mutations:

@riverpod
class CartNotifier extends _$CartNotifier {
  @override
  List<CartItem> build() => [];

  void addItem(Product product) {
    state = [...state, CartItem(product: product, quantity: 1)];
  }

  void removeItem(String productId) {
    state = state.where((item) => item.product.id != productId).toList();
  }

  double get total => state.fold(0, (sum, item) => sum + item.product.price * item.quantity);
}

5. AsyncNotifierProvider — Async Mutable State

The workhorse for most features. Combines async data fetching with mutations:

@riverpod
class TaskListNotifier extends _$TaskListNotifier {
  @override
  Future<List<Task>> build() async {
    final repo = ref.watch(taskRepositoryProvider);
    return repo.getAllTasks();
  }

  Future<void> toggleComplete(String taskId) async {
    final repo = ref.read(taskRepositoryProvider);
    await repo.toggleComplete(taskId);
    ref.invalidateSelf(); // Refetch the list
    await future; // Wait for rebuild to complete
  }
}

Provider Dependencies

Providers can depend on other providers. Riverpod tracks the dependency graph and rebuilds only what's needed:

@riverpod
Future<List<Task>> filteredTasks(Ref ref) async {
  final allTasks = await ref.watch(taskListNotifierProvider.future);
  final filter = ref.watch(taskFilterProvider);

  return switch (filter) {
    TaskFilter.all => allTasks,
    TaskFilter.active => allTasks.where((t) => !t.isCompleted).toList(),
    TaskFilter.completed => allTasks.where((t) => t.isCompleted).toList(),
  };
}

When taskFilterProvider changes, filteredTasks recalculates. When taskListNotifier refetches, filteredTasks also updates. No manual wiring.

Auto-Dispose: Memory Management That Works

By default, @riverpod providers auto-dispose when no widget is watching them. When a user navigates away from a screen, the provider's state is cleaned up automatically.

Want to keep state alive? Use keepAlive:

@Riverpod(keepAlive: true)
class AuthNotifier extends _$AuthNotifier {
  @override
  Future<AuthState> build() async {
    // This stays alive for the entire app lifecycle
    return _checkAuthStatus();
  }
}

Testing Without Pain

Override any provider in tests — no mocking frameworks needed:

void main() {
  test('filtered tasks returns only active tasks', () async {
    final container = ProviderContainer(
      overrides: [
        taskRepositoryProvider.overrideWithValue(FakeTaskRepository()),
      ],
    );

    final tasks = await container.read(filteredTasksProvider.future);
    expect(tasks.every((t) => !t.isCompleted), isTrue);
  });
}

You swap the real repository with a fake one. The rest of the provider graph works exactly the same. No BuildContext needed, no widget tree required.

ref.watch vs ref.read vs ref.listen

This trips up every beginner. Here's the rule:

MethodWhen to Use
ref.watchIn build() methods. Rebuilds when value changes.
ref.readIn callbacks, event handlers. One-time read, no rebuild.
ref.listenFor side effects (show snackbar, navigate).
// GOOD
final tasks = ref.watch(taskListProvider); // Rebuilds widget on change

// GOOD
onPressed: () => ref.read(cartProvider.notifier).addItem(product); // One-time action

// GOOD
ref.listen(authProvider, (prev, next) {
  if (next is AsyncData && next.value == null) {
    context.go('/login'); // Side effect: navigate on logout
  }
});

Why Riverpod Over Provider

FeatureProviderRiverpod
Needs BuildContextYesNo
Compile-time safetyNoYes
Auto-disposeManualBuilt-in
TestingComplex mockingSimple overrides
Multiple providers of same typeHackyNatural
Code generationNoYes (optional)

Provider requires you to climb the widget tree with context.read. Riverpod lets you access any provider from anywhere — your service layer, your tests, your other providers. No widget tree dependency.

What to Do Next

Start with one feature. Create an AsyncNotifierProvider for it. Use .when() in the UI. Override the repository in a test. Once you see how clean the code is compared to setState or Provider, you won't go back.

Share:

Keep Reading

More articles you might enjoy

© 2026 Mohammad Nabi RahmaniBuilt with Next.js & Tailwind