Popular Searches
Popular Course Categories
Popular Courses

Riverpod in Flutter

Flutter State Management

Riverpod in Flutter

Riverpod is a state-management and dependency-management solution for Flutter and Dart applications. It provides a structured way to define, expose, observe, combine, test, and update application state.

Riverpod is built around the concept of providers. A provider describes a piece of state or a dependency, while widgets use Riverpod APIs such as ref.watch() and ref.read() to interact with that provider.


1. What is Riverpod?

Riverpod is a state-management library designed to make application state easier to share, test, combine, and maintain. Providers can expose simple values, computed values, services, asynchronous data, or modifiable application state.

Unlike traditional widget-only state management, Riverpod separates the description and management of application state from individual widgets.

Simple Concept

Provider
   ↓
State / Dependency
   ↓
Widget
   ↓
UI

A provider can be accessed from multiple widgets without manually passing the same value through every constructor.


2. Why Use Riverpod?

As an application becomes larger, state may need to be shared between many screens and components. Riverpod provides a structured way to manage this state.

Important benefits include:

  • Centralized state management.
  • Easy sharing of state between widgets.
  • Dependency management.
  • Support for synchronous and asynchronous state.
  • Automatic handling of loading and error states with asynchronous providers.
  • Provider composition.
  • Provider caching.
  • Fine-grained widget rebuilding.
  • Easy provider overriding for testing.
  • Support for automatic disposal.
  • Support for parameterized providers using family.
  • Separation of UI and business logic.

3. Riverpod vs Provider

Riverpod and Provider are related but have different APIs and architectural approaches. Riverpod was designed to solve several limitations developers may encounter with traditional Provider-based state management.

ProviderRiverpod
Uses BuildContext for common access patterns.Uses Ref and WidgetRef.
Requires providers to be available in the widget ancestor tree.Uses ProviderScope and a separate provider container.
Commonly uses ChangeNotifier.Provides modern Notifier and AsyncNotifier APIs.
Provider lookup depends heavily on widget context.Providers can be accessed through Riverpod's reference APIs.
Multiple providers are commonly organized using MultiProvider.Providers can be declared independently and composed together.
Uses Consumer, watch, read, etc.Uses ConsumerWidget, Consumer, ref.watch(), and ref.read().

Riverpod also provides a ChangeNotifierProvider for migration and mutable-state use cases, but its documentation recommends modern Notifier-based approaches for new scalable code.


4. Installing Riverpod

For a Flutter application, install the Flutter Riverpod package:

flutter pub add flutter_riverpod

After installation, import it into the Dart file where it is required:

import 'package:flutter_riverpod/flutter_riverpod.dart';

5. Basic Riverpod Setup

Flutter applications using Riverpod need a ProviderScope near the root of the application.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

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

ProviderScope creates the environment required for Riverpod providers and their state.


6. Creating Your First Provider

A simple Provider can expose a value that does not directly change.

final messageProvider = Provider((ref) {
  return 'Hello Flutter';
});

The provider is normally declared outside a widget or class as a top-level variable.


7. Reading a Provider with ref.watch()

Widgets using Riverpod can listen to providers using ref.watch().

class HomePage extends ConsumerWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final message = ref.watch(messageProvider);

    return Scaffold(
      body: Center(
        child: Text(message),
      ),
    );
  }
}

When the watched provider changes, Riverpod can rebuild the widget that depends on it.


8. What is ConsumerWidget?

ConsumerWidget is a Riverpod widget that provides a WidgetRef to the build() method.

class HomePage extends ConsumerWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final value = ref.watch(messageProvider);

    return Text(value);
  }
}

The WidgetRef allows the widget to interact with Riverpod providers.


9. What is WidgetRef?

WidgetRef is the object used by Riverpod widgets to interact with providers.

Common methods include:

  • ref.watch()
  • ref.read()
  • ref.listen()
  • ref.invalidate()

For example:

final user = ref.watch(userProvider);

10. ref.watch()

ref.watch() reads a provider and listens to it. If the provider's exposed value changes, the dependent widget or provider can react accordingly.

final counter = ref.watch(counterProvider);

It is commonly used when displaying provider state in the UI.


11. ref.read()

ref.read() obtains a provider's current value without establishing the same reactive listening relationship as ref.watch().

It is often useful for calling methods on a notifier.

ref.read(counterProvider.notifier).increment();

A useful rule is:

ref.watch() → read and react to changes
ref.read()  → access without listening

12. Provider Types in Riverpod

Riverpod provides different provider types for different kinds of values and state.

Provider TypeTypical Purpose
ProviderExpose a synchronous value, service, repository, or computed value.
StateProviderSimple mutable state such as a filter or small UI value.
FutureProviderExpose asynchronous data returned by a Future.
StreamProviderExpose data from a Stream.
NotifierProviderManage modifiable synchronous application state.
AsyncNotifierProviderManage modifiable asynchronous state.
StreamNotifierProviderManage state based on streams with notifier logic.
ChangeNotifierProviderExpose a ChangeNotifier, mainly for migration or mutable-state use cases.

13. Provider

Provider is the most basic provider type. It is useful for exposing values, services, repositories, and computed values.

final appNameProvider = Provider((ref) {
  return 'My Flutter App';
});

Reading it:

final appName = ref.watch(appNameProvider);

14. Provider for Services

Riverpod can be used for dependency management by providing service objects.

class ApiService {
  Future fetchData() async {
    return 'Server data';
  }
}

final apiServiceProvider = Provider((ref) {
  return ApiService();
});

The service can then be accessed from another provider:

final api = ref.watch(apiServiceProvider);

15. Provider for Repositories

Repositories can also be exposed using providers.

class UserRepository {
  final ApiService apiService;

  UserRepository(this.apiService);
}

final userRepositoryProvider = Provider((ref) {
  final api = ref.watch(apiServiceProvider);
  return UserRepository(api);
});

This creates a dependency chain:

UI
 ↓
Provider
 ↓
Repository
 ↓
API Service
 ↓
Backend

16. StateProvider

StateProvider can be used for simple state such as filters, selected values, or small pieces of mutable state.

final counterProvider = StateProvider((ref) {
  return 0;
});

Read the state:

final count = ref.watch(counterProvider);

Update it:

ref.read(counterProvider.notifier).state++;

For larger or more complex state logic, Riverpod's modern Notifier APIs are generally preferred.


17. Counter Example with StateProvider

final counterProvider = StateProvider((ref) => 0);

class CounterPage extends ConsumerWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);

    return Scaffold(
      body: Center(
        child: Text(
          '$count',
          style: const TextStyle(fontSize: 40),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ref.read(counterProvider.notifier).state++;
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

18. FutureProvider

FutureProvider is designed for asynchronous operations that return a Future.

It is useful for:

  • API requests.
  • Database operations.
  • Loading user profiles.
  • Fetching products.
  • Loading configuration data.
final userProvider = FutureProvider((ref) async {
  await Future.delayed(
    const Duration(seconds: 2),
  );

  return 'Flutter Student';
});

19. Handling FutureProvider with AsyncValue

When consuming asynchronous providers, Riverpod exposes an AsyncValue that can represent loading, data, and error states.

class UserPage extends ConsumerWidget {
  const UserPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final user = ref.watch(userProvider);

    return user.when(
      data: (value) => Text('User: $value'),
      loading: () => const CircularProgressIndicator(),
      error: (error, stack) => Text('Error: $error'),
    );
  }
}

20. AsyncValue

AsyncValue represents the state of an asynchronous operation.

Common states include:

  • AsyncLoading: Data is being loaded.
  • AsyncData: Data was successfully obtained.
  • AsyncError: An error occurred.
AsyncValue

This makes asynchronous UI states easier to represent consistently.


21. StreamProvider

StreamProvider is used when data arrives continuously through a Dart Stream.

final timeProvider = StreamProvider((ref) {
  return Stream.periodic(
    const Duration(seconds: 1),
    (_) => DateTime.now(),
  );
});

The UI can watch the stream provider:

final time = ref.watch(timeProvider);

return time.when(
  data: (value) => Text(value.toString()),
  loading: () => const CircularProgressIndicator(),
  error: (error, stack) => Text('$error'),
);

22. NotifierProvider

NotifierProvider is designed for state that can change in response to user interactions or other events. The Notifier centralizes the logic for modifying that state.

class CounterNotifier extends Notifier {
  @override
  int build() {
    return 0;
  }

  void increment() {
    state++;
  }

  void decrement() {
    state--;
  }
}

final counterProvider = NotifierProvider(
  CounterNotifier.new,
);

23. Reading a NotifierProvider

Read the state:

final count = ref.watch(counterProvider);

Call a method on the notifier:

ref.read(counterProvider.notifier).increment();

24. Complete Notifier Counter Example

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class CounterNotifier extends Notifier {
  @override
  int build() {
    return 0;
  }

  void increment() {
    state++;
  }

  void decrement() {
    if (state > 0) {
      state--;
    }
  }

  void reset() {
    state = 0;
  }
}

final counterProvider = NotifierProvider(
  CounterNotifier.new,
);

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: const CounterPage(),
    );
  }
}

class CounterPage extends ConsumerWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Riverpod Counter'),
      ),
      body: Center(
        child: Text(
          '$count',
          style: const TextStyle(fontSize: 50),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ref.read(counterProvider.notifier).increment();
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

25. How the Notifier Counter Works

  1. CounterNotifier extends Notifier.
  2. The build() method provides the initial state.
  3. counterProvider exposes the notifier and its state.
  4. ref.watch(counterProvider) listens to the counter state.
  5. The button uses ref.read(counterProvider.notifier) to access the notifier.
  6. increment() updates the state.
  7. Widgets watching the provider react to the new state.

26. Why Use Notifier?

A Notifier keeps the logic that modifies state in one dedicated class.

UI
 ↓
ref.read(provider.notifier)
 ↓
Notifier method
 ↓
state changes
 ↓
ref.watch(provider)
 ↓
UI rebuilds

This helps prevent business logic from being scattered across multiple widgets.


27. AsyncNotifierProvider

AsyncNotifierProvider is designed for state that is asynchronous and can also be modified through notifier methods.

It is particularly useful for applications that fetch remote data and then allow the user to perform actions on that data.

class UserNotifier extends AsyncNotifier {
  @override
  Future build() async {
    await Future.delayed(
      const Duration(seconds: 2),
    );

    return 'Flutter Student';
  }
}

final userProvider = AsyncNotifierProvider(
  UserNotifier.new,
);

28. AsyncNotifier with Loading and Error States

Because asynchronous notifier state is represented through AsyncValue, the UI can handle different states explicitly.

class UserPage extends ConsumerWidget {
  const UserPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final user = ref.watch(userProvider);

    return user.when(
      data: (value) => Text(value),
      loading: () => const CircularProgressIndicator(),
      error: (error, stack) => Text(
        'Error: $error',
      ),
    );
  }
}

29. Riverpod Code Generation

Riverpod also supports code generation through riverpod_annotation, riverpod_generator, and build_runner.

The packages can be added using:

flutter pub add flutter_riverpod
flutter pub add riverpod_annotation
flutter pub add dev:riverpod_generator
flutter pub add dev:build_runner

Code generation is optional. Riverpod can also be used without code generation.


30. Creating a Generated Provider

A generated provider can be declared using the @riverpod annotation.

import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'message_provider.g.dart';

@riverpod
String message(Ref ref) {
  return 'Hello Riverpod';
}

Generate the required code using:

dart run build_runner build

For active development, you can use:

dart run build_runner watch

31. Functional and Class-Based Providers

Riverpod's code-generation syntax supports both function-based and class-based providers.

Functional Provider

@riverpod
String greeting(Ref ref) {
  return 'Hello Flutter';
}

Class-Based Provider

@riverpod
class Counter extends _$Counter {
  @override
  int build() {
    return 0;
  }

  void increment() {
    state++;
  }
}

Class-based providers are useful when the provider needs public methods for modifying state.


32. ProviderScope

ProviderScope is the root container for Riverpod in Flutter applications.

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

It allows Riverpod to store and manage provider state independently from individual widget instances.


33. ProviderContainer

Under the Flutter widget layer, Riverpod uses a provider container to manage provider state. In Flutter applications, ProviderScope provides this environment for the widget tree.

ProviderContainer is particularly useful for testing and non-widget Dart code.

final container = ProviderContainer();

final value = container.read(messageProvider);

container.dispose();

34. Consumer Widget

Riverpod provides several ways to consume providers from Flutter widgets.

One common approach is ConsumerWidget:

class HomePage extends ConsumerWidget {
  const HomePage({super.key});

  @override
  Widget build(
    BuildContext context,
    WidgetRef ref,
  ) {
    final message = ref.watch(messageProvider);

    return Text(message);
  }
}

35. Consumer Stateful Widget

When a screen needs both StatefulWidget functionality and Riverpod access, ConsumerStatefulWidget can be used.

class HomePage extends ConsumerStatefulWidget {
  const HomePage({super.key});

  @override
  ConsumerState createState() => _HomePageState();
}

class _HomePageState extends ConsumerState {
  @override
  Widget build(BuildContext context) {
    final value = ref.watch(messageProvider);

    return Text(value);
  }
}

36. Consumer Widget Inside a StatelessWidget

If you do not want to convert an entire widget into a ConsumerWidget, you can use the Consumer widget.

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, ref, child) {
        final message = ref.watch(messageProvider);

        return Text(message);
      },
    );
  }
}

37. ref.listen()

ref.listen() can be used when you want to react to provider changes with side effects rather than simply rebuilding UI.

Typical use cases include:

  • Showing a SnackBar.
  • Showing a dialog.
  • Navigating to another screen.
  • Logging state changes.
  • Displaying notifications.
ref.listen(
  userProvider,
  (previous, next) {
    next.whenOrNull(
      error: (error, stack) {
        // Show error notification
      },
    );
  },
);

38. ref.invalidate()

ref.invalidate() can be used to invalidate a provider's current state so that it can be recomputed when needed.

ref.invalidate(userProvider);

This can be useful when data needs to be refreshed or a provider needs to be recomputed.


39. AutoDispose

The autoDispose modifier allows Riverpod to dispose of provider state when it is no longer being listened to, depending on the provider and lifecycle.

final temporaryProvider = Provider.autoDispose((ref) {
  return 'Temporary data';
});

Auto-disposal can be useful for temporary screens, search pages, and other state that does not need to remain alive indefinitely.


40. Family Providers

family allows a provider to receive an external parameter.

For example, a provider can load a user using a user ID.

final userProvider = FutureProvider.family(
  (ref, userId) async {
    await Future.delayed(
      const Duration(seconds: 1),
    );

    return 'User $userId';
  },
);

Use it like this:

final user = ref.watch(
  userProvider(10),
);

41. AutoDispose with Family

Modifiers can be combined when appropriate.

final userProvider = FutureProvider.autoDispose.family(
  (ref, userId) async {
    return 'User $userId';
  },
);

This pattern can be useful for parameterized screens where cached state should not remain indefinitely after the screen stops using it.


42. Combining Providers

Riverpod providers can depend on other providers. This makes provider composition straightforward.

final firstNameProvider = Provider(
  (ref) => 'John',
);

final lastNameProvider = Provider(
  (ref) => 'Doe',
);

final fullNameProvider = Provider((ref) {
  final firstName = ref.watch(firstNameProvider);
  final lastName = ref.watch(lastNameProvider);

  return '$firstName $lastName';
});

43. Why Provider Composition is Useful

Instead of putting all application logic into one large object, providers can depend on smaller providers.

Auth Provider
     ↓
User Provider
     ↓
Profile Provider
     ↓
UI

If an upstream dependency changes, dependent providers can react accordingly.


44. Select for Rebuild Optimization

When a widget needs only part of a larger state object, Riverpod provides APIs that can help reduce unnecessary rebuilds.

For example:

final userName = ref.watch(
  userProvider.select(
    (user) => user.name,
  ),
);

This allows the widget to focus on a selected property rather than the entire object.


45. Immutable State

Riverpod's modern Notifier-based architecture commonly works with immutable state. Instead of modifying an existing collection directly, create a new state containing the updated data.

Incorrect Approach

state.add(newItem);

Better Approach

state = [
  ...state,
  newItem,
];

Immutable state makes state changes easier to reason about and can work well with rebuild and comparison mechanisms.


46. Todo Example with Notifier

class Todo {
  final String title;
  final bool completed;

  const Todo({
    required this.title,
    required this.completed,
  });

  Todo copyWith({
    String? title,
    bool? completed,
  }) {
    return Todo(
      title: title ?? this.title,
      completed: completed ?? this.completed,
    );
  }
}

class TodoNotifier extends Notifier> {
  @override
  List build() {
    return [];
  }

  void addTodo(String title) {
    state = [
      ...state,
      Todo(
        title: title,
        completed: false,
      ),
    ];
  }

  void toggleTodo(int index) {
    final todos = [...state];

    todos[index] = todos[index].copyWith(
      completed: !todos[index].completed,
    );

    state = todos;
  }

  void removeTodo(int index) {
    state = [
      for (int i = 0; i < state.length; i++)
        if (i != index) state[i],
    ];
  }
}

final todoProvider = NotifierProvider>(
  TodoNotifier.new,
);

47. Displaying Todo State

class TodoPage extends ConsumerWidget {
  const TodoPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todos = ref.watch(todoProvider);

    return ListView.builder(
      itemCount: todos.length,
      itemBuilder: (context, index) {
        final todo = todos[index];

        return CheckboxListTile(
          value: todo.completed,
          onChanged: (_) {
            ref
                .read(todoProvider.notifier)
                .toggleTodo(index);
          },
          title: Text(todo.title),
        );
      },
    );
  }
}

48. Riverpod with API Calls

Riverpod works well with API-based applications. A FutureProvider or AsyncNotifierProvider can represent remote data.

final productsProvider = FutureProvider>((ref) async {
  await Future.delayed(
    const Duration(seconds: 2),
  );

  return [
    'Laptop',
    'Mobile',
    'Tablet',
  ];
});

49. API Loading, Success, and Error UI

class ProductsPage extends ConsumerWidget {
  const ProductsPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final products = ref.watch(productsProvider);

    return products.when(
      loading: () {
        return const Center(
          child: CircularProgressIndicator(),
        );
      },
      error: (error, stack) {
        return Center(
          child: Text('Error: $error'),
        );
      },
      data: (items) {
        return ListView.builder(
          itemCount: items.length,
          itemBuilder: (context, index) {
            return ListTile(
              title: Text(items[index]),
            );
          },
        );
      },
    );
  }
}

50. Riverpod with Firebase

Riverpod can be used together with Firebase to separate Firebase services and application state from UI code.

Firebase Service
       ↓
Repository
       ↓
Riverpod Provider
       ↓
Notifier / AsyncNotifier
       ↓
Flutter UI

For example, an authentication service can be provided through Riverpod and an AuthNotifier can expose the application's authentication state.


51. Riverpod Authentication Example

class AuthState {
  final bool isLoggedIn;
  final String? email;

  const AuthState({
    required this.isLoggedIn,
    this.email,
  });
}

class AuthNotifier extends Notifier {
  @override
  AuthState build() {
    return const AuthState(
      isLoggedIn: false,
    );
  }

  void login(String email) {
    state = AuthState(
      isLoggedIn: true,
      email: email,
    );
  }

  void logout() {
    state = const AuthState(
      isLoggedIn: false,
    );
  }
}

final authProvider = NotifierProvider(
  AuthNotifier.new,
);

52. Riverpod and Dependency Injection

Riverpod can also act as a dependency-management mechanism. Services and repositories can be defined once and consumed where required.

final databaseProvider = Provider((ref) {
  return DatabaseService();
});

final userRepositoryProvider = Provider((ref) {
  final database = ref.watch(databaseProvider);

  return UserRepository(database);
});

53. Riverpod and Repository Pattern

A repository can isolate data-access logic from application state.

class UserRepository {
  final ApiService api;

  UserRepository(this.api);

  Future getUser() {
    return api.fetchUser();
  }
}

Then expose the repository:

final userRepositoryProvider = Provider((ref) {
  final api = ref.watch(apiServiceProvider);

  return UserRepository(api);
});

54. Provider Overrides

One useful Riverpod feature is the ability to override providers. This is particularly useful in testing or when different environments require different implementations.

ProviderScope(
  overrides: [
    apiServiceProvider.overrideWithValue(
      MockApiService(),
    ),
  ],
  child: const MyApp(),
)

This allows the application to use a mock service instead of the real implementation in a controlled environment.


55. Testing with Riverpod

Because providers are independent of individual widget constructors, they can be tested using a ProviderContainer.

void main() {
  final container = ProviderContainer();

  final value = container.read(
    messageProvider,
  );

  print(value);

  container.dispose();
}

For Flutter widget tests, Riverpod providers can also be overridden within a ProviderScope.


56. Provider Lifecycle

Riverpod manages provider state according to how providers are used and configured.

Important lifecycle-related features include:

  • Provider caching.
  • Automatic disposal.
  • Provider invalidation.
  • Dependency tracking.
  • Provider recomputation.

57. Riverpod and Caching

Providers can cache the result of computations and make the same provider value available to multiple consumers.

final expensiveProvider = Provider((ref) {
  // Expensive calculation
  return 100;
});

Multiple widgets can watch the same provider instead of independently calculating the same value.


58. Riverpod Project Structure

A larger Flutter application can organize Riverpod code into separate directories.

lib/
├── main.dart
├── providers/
│   ├── auth_provider.dart
│   ├── counter_provider.dart
│   └── theme_provider.dart
├── models/
│   ├── user.dart
│   └── product.dart
├── repositories/
│   ├── user_repository.dart
│   └── product_repository.dart
├── services/
│   ├── api_service.dart
│   └── auth_service.dart
├── screens/
│   ├── home_screen.dart
│   ├── login_screen.dart
│   └── profile_screen.dart
└── widgets/
    └── loading_view.dart

59. Recommended Separation of Responsibilities

Screen / Widget
      ↓
Riverpod Provider
      ↓
Notifier / AsyncNotifier
      ↓
Repository
      ↓
Service
      ↓
API / Firebase / Database

This structure helps keep UI code focused on presentation while state and data-access responsibilities remain separated.


60. Riverpod Common Mistakes

  • Forgetting to add ProviderScope.
  • Forgetting to import flutter_riverpod.
  • Using ref.read() when the UI needs reactive updates.
  • Using ref.watch() unnecessarily for one-time actions.
  • Putting too much unrelated business logic in one notifier.
  • Mutating immutable state directly.
  • Forgetting to handle loading and error states for asynchronous data.
  • Ignoring provider scope and lifecycle.
  • Creating providers inside widget build methods unnecessarily.
  • Using older state-management APIs when a modern Notifier approach better fits new code.

61. ref.watch() vs ref.read()

Featureref.watch()ref.read()
Gets provider valueYesYes
Listens to changesYesNo
Can trigger widget rebuildYesNo
Common UI usageDisplaying stateButton actions / one-time access

62. Provider vs NotifierProvider

ProviderNotifierProvider
Good for exposing values or dependencies.Good for state that changes through defined methods.
Usually exposes an unmodifiable value.Provides a notifier that can modify state.
Logic is usually in the provider function.State-changing logic is centralized in the Notifier.
Useful for services and repositories.Useful for application state.

63. FutureProvider vs AsyncNotifierProvider

FutureProviderAsyncNotifierProvider
Useful for exposing asynchronous results.Useful for asynchronous state that also needs modification methods.
Simple API request or calculation.More complex interactive asynchronous state.
Generally read-only from consumers.Notifier can expose methods for actions.

64. StateNotifierProvider

Riverpod supports StateNotifierProvider for applications using StateNotifier. However, current Riverpod documentation recommends the newer NotifierProvider approach for new code.

final counterProvider = StateNotifierProvider(
  (ref) => CounterNotifier(),
);

Understanding StateNotifierProvider is still useful when working with existing projects that use it.


65. ChangeNotifierProvider in Riverpod

Riverpod also includes ChangeNotifierProvider to support migration from the traditional Provider package and specific mutable-state use cases.

class Counter extends ChangeNotifier {
  int count = 0;

  void increment() {
    count++;
    notifyListeners();
  }
}

final counterProvider = ChangeNotifierProvider((ref) {
  return Counter();
});

For new scalable Riverpod code, modern Notifier APIs are generally preferred over ChangeNotifierProvider.


66. Riverpod with Forms

Riverpod can manage form-related state when the form becomes complex or needs to interact with application logic.

class FormState {
  final String email;
  final bool submitting;

  const FormState({
    this.email = '',
    this.submitting = false,
  });
}

class FormNotifier extends Notifier {
  @override
  FormState build() {
    return const FormState();
  }

  void updateEmail(String email) {
    state = FormState(
      email: email,
      submitting: state.submitting,
    );
  }
}

final formProvider = NotifierProvider(
  FormNotifier.new,
);

67. Riverpod with Theme State

Application theme settings can also be managed through Riverpod.

class ThemeNotifier extends Notifier {
  @override
  ThemeMode build() {
    return ThemeMode.light;
  }

  void toggleTheme() {
    state = state == ThemeMode.light
        ? ThemeMode.dark
        : ThemeMode.light;
  }
}

final themeProvider = NotifierProvider(
  ThemeNotifier.new,
);

The application can then watch the provider and update its ThemeMode.


68. Riverpod for Shopping Cart

class CartNotifier extends Notifier> {
  @override
  List build() {
    return [];
  }

  void addItem(String item) {
    state = [
      ...state,
      item,
    ];
  }

  void removeItem(String item) {
    state = [
      for (final currentItem in state)
        if (currentItem != item) currentItem,
    ];
  }

  void clearCart() {
    state = [];
  }
}

final cartProvider = NotifierProvider>(
  CartNotifier.new,
);

69. Watching Shopping Cart State

class CartPage extends ConsumerWidget {
  const CartPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final items = ref.watch(cartProvider);

    return ListView.builder(
      itemCount: items.length,
      itemBuilder: (context, index) {
        return ListTile(
          title: Text(items[index]),
        );
      },
    );
  }
}

70. Calling Cart Methods

ElevatedButton(
  onPressed: () {
    ref.read(cartProvider.notifier).addItem(
      'Flutter Course',
    );
  },
  child: const Text('Add to Cart'),
)

71. Riverpod with Loading State

Asynchronous providers make it possible to represent loading, success, and error states consistently.

final dataProvider = FutureProvider((ref) async {
  await Future.delayed(
    const Duration(seconds: 2),
  );

  return 'Data loaded';
});

final data = ref.watch(dataProvider);

return data.when(
  loading: () => const CircularProgressIndicator(),
  error: (error, stack) => Text('$error'),
  data: (value) => Text(value),
);

72. Riverpod and Error Handling

Riverpod's asynchronous providers expose errors through AsyncValue. This makes it easier to keep error handling close to the UI that displays the asynchronous state.

final result = ref.watch(dataProvider);

return result.when(
  data: (data) => Text(data),
  loading: () => const CircularProgressIndicator(),
  error: (error, stack) => Column(
    children: [
      const Text('Something went wrong'),
      Text('$error'),
    ],
  ),
);

73. Refreshing Provider Data

When data needs to be refreshed, a provider can be invalidated so that its state is recomputed.

ref.invalidate(productsProvider);

This can be useful for refresh actions and retry functionality.


74. Retry Example

ElevatedButton(
  onPressed: () {
    ref.invalidate(productsProvider);
  },
  child: const Text('Retry'),
)

75. Riverpod Best Practices

  • Keep providers focused on a specific responsibility.
  • Use Provider for services, repositories, and computed dependencies.
  • Use modern NotifierProvider APIs for interactive synchronous state.
  • Use AsyncNotifierProvider for interactive asynchronous state.
  • Use FutureProvider for straightforward asynchronous values.
  • Use StreamProvider for stream-based data.
  • Use ref.watch() when the UI should react to changes.
  • Use ref.read() for actions and non-listening access.
  • Use family for parameterized providers.
  • Use autoDispose when temporary provider state should be disposed when no longer needed.
  • Prefer immutable state for modern Notifier-based designs.
  • Keep API and database logic in services or repositories.
  • Keep widgets focused on presentation.
  • Use provider overrides for controlled testing and alternate implementations.
  • Handle loading, success, empty, and error states explicitly.

76. Complete Practical Riverpod Example

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class CounterNotifier extends Notifier {
  @override
  int build() {
    return 0;
  }

  void increment() {
    state++;
  }

  void decrement() {
    if (state > 0) {
      state--;
    }
  }

  void reset() {
    state = 0;
  }
}

final counterProvider = NotifierProvider(
  CounterNotifier.new,
);

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Riverpod Demo',
      home: const CounterScreen(),
    );
  }
}

class CounterScreen extends ConsumerWidget {
  const CounterScreen({super.key});

  @override
  Widget build(
    BuildContext context,
    WidgetRef ref,
  ) {
    final count = ref.watch(counterProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Riverpod Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              'Current Count',
              style: TextStyle(fontSize: 20),
            ),
            const SizedBox(height: 10),
            Text(
              '$count',
              style: const TextStyle(
                fontSize: 50,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () {
                    ref
                        .read(counterProvider.notifier)
                        .decrement();
                  },
                  child: const Text('Decrease'),
                ),
                const SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () {
                    ref
                        .read(counterProvider.notifier)
                        .increment();
                  },
                  child: const Text('Increase'),
                ),
                const SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () {
                    ref
                        .read(counterProvider.notifier)
                        .reset();
                  },
                  child: const Text('Reset'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

77. Step-by-Step Flow of the Practical Example

  1. The Flutter application starts.
  2. ProviderScope initializes Riverpod.
  3. CounterNotifier defines the counter logic.
  4. counterProvider exposes the notifier and its state.
  5. ConsumerWidget receives a WidgetRef.
  6. ref.watch(counterProvider) listens to the counter.
  7. The UI displays the current value.
  8. The user presses the Increase button.
  9. ref.read(counterProvider.notifier) accesses the notifier.
  10. The notifier changes state.
  11. The widget watching the provider receives the updated state.
  12. The UI rebuilds with the new counter value.

78. Riverpod Advantages

  • Strong separation between state and UI.
  • Powerful dependency management.
  • Supports multiple provider types.
  • Good support for asynchronous operations.
  • Automatic loading and error representation through AsyncValue.
  • Provider composition.
  • Provider overrides improve testability.
  • Fine-grained rebuild control.
  • Auto-disposal support.
  • Family providers support parameters.
  • Modern Notifier APIs centralize state-changing logic.

79. Riverpod Limitations and Learning Considerations

  • There are several provider types and APIs to learn.
  • New developers may initially find ref.watch(), ref.read(), and notifier APIs confusing.
  • Code generation adds an additional build step if used.
  • Large applications still require good architecture and naming conventions.
  • Choosing between Provider, FutureProvider, NotifierProvider, and AsyncNotifierProvider requires understanding the type of state being managed.

80. Interview Questions

Q1. What is Riverpod?

Riverpod is a state-management and dependency-management solution for Dart and Flutter applications based around providers.

Q2. What is Provider in Riverpod?

A provider encapsulates a value, state, computation, or dependency and makes it available to consumers.

Q3. What is ProviderScope?

ProviderScope establishes the Riverpod environment required by Flutter applications.

Q4. What is ref.watch()?

ref.watch() obtains a provider value and listens for changes.

Q5. What is ref.read()?

ref.read() obtains a provider value without establishing the same reactive listening relationship.

Q6. What is ConsumerWidget?

ConsumerWidget is a Riverpod widget that provides a WidgetRef to its build method.

Q7. What is NotifierProvider?

It exposes a Notifier whose state can change through methods defined on the notifier.

Q8. What is AsyncNotifierProvider?

It exposes an AsyncNotifier for asynchronous state that can also be modified through notifier methods.

Q9. What is FutureProvider?

It exposes the result of an asynchronous Future and represents loading, data, and error states through AsyncValue.

Q10. What is family?

family allows a provider to receive external parameters.

Q11. What is autoDispose?

autoDispose can automatically dispose provider state when it is no longer being used, depending on the provider lifecycle.

Q12. Is code generation mandatory in Riverpod?

No. Riverpod can be used without code generation. Code generation is an optional development approach.


81. Quick Revision

ConceptPurpose
RiverpodState and dependency management.
ProviderScopeInitializes the Riverpod environment.
ProviderExposes a value or dependency.
StateProviderSimple state management.
FutureProviderAsynchronous Future data.
StreamProviderStream-based data.
NotifierProviderInteractive synchronous state.
AsyncNotifierProviderInteractive asynchronous state.
ConsumerWidgetWidget that can access WidgetRef.
WidgetRefBridge between Flutter widgets and Riverpod.
ref.watch()Read and listen to provider changes.
ref.read()Read without listening.
ref.listen()React to changes with side effects.
familyPass parameters to providers.
autoDisposeDispose unused provider state.
AsyncValueRepresent loading, data, and error states.

82. Practical Exercises

  1. Create a counter application using NotifierProvider.
  2. Create a Todo application using immutable state.
  3. Create a product API using FutureProvider.
  4. Create a live-data application using StreamProvider.
  5. Create a login and logout state using NotifierProvider.
  6. Create a shopping cart using NotifierProvider.
  7. Create a search screen using a family provider.
  8. Create a temporary screen using autoDispose.
  9. Create an API service and repository using Provider.
  10. Create a Firebase authentication state manager using AsyncNotifierProvider.
  11. Write tests using ProviderContainer.
  12. Create a development mock API using provider overrides.

83. Learning Outcome

After completing this topic, you should be able to:

  • Explain what Riverpod is.
  • Understand the provider architecture.
  • Install and configure Riverpod.
  • Use ProviderScope.
  • Create basic providers.
  • Use ConsumerWidget and WidgetRef.
  • Use ref.watch() and ref.read().
  • Understand StateProvider, FutureProvider, and StreamProvider.
  • Create NotifierProvider-based state.
  • Use AsyncNotifierProvider for asynchronous application state.
  • Handle loading, success, and error states.
  • Use family and autoDispose.
  • Combine providers.
  • Use Riverpod for dependency injection.
  • Organize API, repository, and application state layers.
  • Understand provider overrides and testing.

84. Useful Flutter and Riverpod Resources

For official Riverpod documentation:

Riverpod Official Documentation

For getting started with Riverpod:

Riverpod Getting Started Guide

For Flutter's state-management documentation:

Flutter State Management Documentation


85. JustAcademy Flutter Resources

Learn more about Flutter development through the JustAcademy Flutter training program:

JustAcademy Flutter Training Course

Register for Flutter Course Demo


86. Summary

Riverpod is a powerful approach to state and dependency management in Flutter. It is centered around providers, which can expose values, services, repositories, synchronous state, asynchronous state, and streams.

For simple values and dependencies, Provider is useful. For asynchronous results, FutureProvider and StreamProvider are available. For interactive application state, modern Riverpod applications can use NotifierProvider and AsyncNotifierProvider. The ref.watch() API is commonly used when the UI needs to react to state changes, while ref.read() is useful for actions and non-listening access.

With features such as ProviderScope, ConsumerWidget, AsyncValue, family, autoDispose, provider composition, overrides, and Notifiers, Riverpod provides a structured foundation for building maintainable Flutter applications.

whatsapp