DEV Community

Cover image for Riverpod Flutter: A Beginner's Guide
Harshal Ranjhani for CodeParrot

Posted on • Originally published at codeparrot.ai

Riverpod Flutter: A Beginner's Guide

Flutter is a popular framework for building mobile applications. It provides a rich set of widgets and tools for creating beautiful and functional apps. Flutter was developed by Google and released in 2017. It is used by so many popular apps like Google Pay, Google Ads, Supercell and so many more. Managing state in Flutter apps can be tricky, and this is where Riverpod comes in.

What is Riverpod?

It is a state management solution for Flutter that makes handling app state straightforward and type-safe. Think of it as a smart container that holds and manages your app's data.

Why Should You Use It?

Riverpod solves several common problems that Flutter developers face:

  1. Dependency Management: It helps you organize and access your app's data from anywhere without passing it through multiple widget layers.

  2. Type Safety: Unlike some other state management solutions, Riverpod is completely type-safe. This means you'll catch errors at compile-time rather than runtime.

  3. Testing: Riverpod makes testing your app's state and business logic straightforward.

  4. Caching: Built-in caching mechanisms for network requests and expensive computations.

  5. Error Handling: Elegant ways to handle and display errors in your app.

Let's dive deeper and see how to install Riverpod in your Flutter project and we'll also explore some of the features of Riverpod.

Getting Started with Riverpod

Before starting, you can try out this solution online in your browser without having to install anything. Test it on DartPad or on Zapp.

Installation

Let's start by adding the necessary packages to your Flutter project. Open your pubspec.yaml file and add the following dependencies:

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.4.9
  riverpod_annotation: ^2.3.3
  dev_dependencies:
    build_runner: ^2.4.8
    riverpod_generator: ^2.3.9
Enter fullscreen mode Exit fullscreen mode

After adding these dependencies, run the following command in your terminal:

flutter pub get
Enter fullscreen mode Exit fullscreen mode

This will install the necessary packages and add them to your project.

Setting up Your App

To use Riverpod, you need to wrap your app with a ProviderScope widget. This widget initializes Riverpod for your entire app:

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

void main() {
  runApp(
    // Adding ProviderScope enables Riverpod for the entire app
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Riverpod Demo',
      home: HomePage(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Creating Your First Provider

Let's create a simple counter example to understand how Riverpod works. We'll start with a basic provider:

// Define a provider that stores a simple integer value
final counterProvider = StateProvider<int>((ref) => 0);
Enter fullscreen mode Exit fullscreen mode

Now, let's create a widget that uses this provider:

class HomePage extends ConsumerWidget {
    @override
    Widget build(BuildContext context, WidgetRef ref) {
        // Watch the counter value
        final count = ref.watch(counterProvider);

        return Scaffold(
            appBar: AppBar(title: Text('RiverPod Counter Example')),
            body: Center(
                child: Text(
                    'Count: $count',
                    style: Theme.of(context).textTheme.headlineMedium,
                )
            ),
            floatingActionButton: FloatingActionButton(
                onPressed: () => ref.read(counterProvider.notifier).state++,
                child: Icon(Icons.add),
            ),
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Understanding the Basic Concepts

Let's break down what we just did:

  1. Provider: This is where you define your data. In our example, counterProvider holds an integer value.

  2. ConsumerWidget: This is a special widget that can listen to providers. It's similar to StatelessWidget but with an extra WidgetRef parameter.

  3. ref.watch(): This method subscribes to a provider and rebuilds the widget whenever the value changes.

  4. ref.read(): Used to read the provider's value once or modify it without listening to changes.

Different Types of Providers

The framework offers several types of providers for different use cases:

  1. Provider: For values that never change
final nameProvider = Provider((ref) => 'John Doe');
Enter fullscreen mode Exit fullscreen mode
  1. StateProvider: For simple state that can change
final counterProvider = StateProvider((ref) => 0);
Enter fullscreen mode Exit fullscreen mode
  1. NotifierProvider: For complex state management:
class CounterNotifier extends Notifier<int> {
    @override
    int build() => 0;

    void increment() => state++;
    void decrement() => state--;
}

final counterNotifierProvider = NotifierProvider<CounterNotifier, int>(
  () => CounterNotifier(),
);
Enter fullscreen mode Exit fullscreen mode

Common use cases where Riverpod can be useful

API data fetching

One of the most common use cases is handling API calls and managing their states. Here's an example that demonstrates how to fetch user data from an API and handle loading, error, and success states elegantly:

// A provider that fetches user data
@riverpod
Future<User> fetchUser(FetchUserRef ref, {required String userId}) async {
  final response = await http.get('api.example.com/users/$userId');
  return User.fromJson(response.data);
}

// Using the data in a widget
class UserProfile extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsync = ref.watch(fetchUserProvider(userId: '123'));

    return userAsync.when(
      loading: () => CircularProgressIndicator(),
      error: (error, stack) => Text('Error: $error'),
      data: (user) => Text('Welcome ${user.name}!'),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we've created an AsyncNotifierProvider that handles the API call. The when method provides a clean way to handle all possible states of the async operation. This pattern is particularly useful because:

  • It automatically handles loading states
  • It provides built-in error handling
  • The UI automatically updates when the data changes
  • It's type-safe and null-safe
  • The data can be easily cached and reused across the app

Form Management

Another powerful use case for Riverpod is managing form state. Here's an example of how to handle a login form with validation:

@riverpod
class LoginFormNotifier extends Notifier<LoginFormState> {
  @override
  LoginFormState build() => LoginFormState.initial();

  void updateEmail(String email) {
    state = state.copyWith(
      email: email,
      emailError: email.contains('@') ? null : 'Invalid email',
    );
  }

  void updatePassword(String password) {
    state = state.copyWith(
      password: password,
      passwordError: password.length >= 6 ? null : 'Password too short',
    );
  }

  Future<void> submit() async {
    state = state.copyWith(isLoading: true);
    try {
      await ref.read(authServiceProvider).login(state.email, state.password);
      state = state.copyWith(isSuccess: true);
    } catch (e) {
      state = state.copyWith(error: e.toString());
    } finally {
      state = state.copyWith(isLoading: false);
    }
  }
}

// Using the form in a widget
class LoginForm extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final formState = ref.watch(loginFormNotifierProvider);

    return Form(
      child: Column(
        children: [
          TextField(
            onChanged: (value) => ref.read(loginFormNotifierProvider.notifier).updateEmail(value),
            decoration: InputDecoration(
              errorText: formState.emailError,
            ),
          ),
          TextField(
            onChanged: (value) => ref.read(loginFormNotifierProvider.notifier).updatePassword(value),
            decoration: InputDecoration(
              errorText: formState.passwordError,
            ),
          ),
          ElevatedButton(
            onPressed: formState.isValid 
              ? () => ref.read(loginFormNotifierProvider.notifier).submit()
              : null,
            child: formState.isLoading 
              ? CircularProgressIndicator() 
              : Text('Login'),
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

The form management example above showcases how Riverpod elegantly handles complex form state and validation. By using a dedicated LoginFormNotifier, we keep all form-related logic and state neatly organized in one place, making it easy to maintain and test.

The form provides real-time validation as users type, instantly showing feedback for invalid email formats or short passwords. During form submission, the UI automatically updates to show loading states and disable the submit button, preventing duplicate submissions. Both field-level validation errors and submission errors are handled smoothly, ensuring users always know what's wrong and how to fix it.

What's particularly nice about this approach is how clean the UI code remains. The widget itself focuses purely on layout and presentation, while all the business logic lives in the notifier. This pattern works well for any form complexity - from simple login forms to multi-step wizards with complex validation rules.

Conclusion

This state management solution brings type safety, dependency management, and elegant error handling to Flutter applications. Its flexibility makes it suitable for various use cases, from simple counter apps to complex forms and API integrations.

This is just the beginning of what you can do. For more information and advanced usage, check out the official Riverpod documentation.

Happy coding! ๐Ÿš€

Top comments (1)

Collapse
 
randalschwartz profile image
Randal L. Schwartz

Avoid legacy riverpod tools. In brief, avoid legacy ChangeNotifier, StateNotifier (and their providers) and StateProvider. Use only Provider, FutureProvider, StreamProvider, and Notifier, AsyncNotifier, StreamNotifier (and their providers).