How to start using Riverpod, StateNotifier and Freezed in your Flutter applications.
Tags: flutter, riverpod, provider, dart
Published: 2021-04-14 15:55:27

How to use the 3 tools together to manage the state of your application.

[Haz clic aquí para la versión en español](https://elian-mortega.medium.com/como-empezar-con-riverpod-statenotifier-freezed-a47402c772ea)

More and more we hear of `Riverpod` as the new state management solution in Flutter. The big problem is that people trying to use it do not even know where to start.

In this article I am going to focus on how a project can be structured using [Riverpod](https://pub.dev/packages/riverpod),[StateNotifer](https://pub.dev/packages/state_notifier) and [Freezed](https://pub.dev/packages/freezed) together to create the complete flow of state management.

## What are we going to build?

We are going to build a jokes application, it's going to use the [Jokes API](https://v2.jokeapi.dev/) to get a new programming joke and then show it in the screen. The  result will look something like this:

![https://raw.githubusercontent.com/NoScopeDevs/riverpod_jokes_basic/main/assets/readme/app_result.gif](https://raw.githubusercontent.com/NoScopeDevs/riverpod_jokes_basic/main/assets/readme/app_result.gif)

## Basics

I could make an explanation of each of these tools, but there is no need to reinvent the wheel. My friend  [Marcos Sevilla](https://twitter.com/MarcosJSevilla)  has some excellent articles explaining these tools.

- [Riverpod article](https://dev.to/marcossevilla/riverpod-rewriting-provider-359n)
    - A tool that allows us to inject dependencies and encapsulate a state and then listen to this state.
- [StateNotifier article](https://dev.to/marcossevilla/statenotifier-an-improved-changenotifier-3f84)
    - It is a “re-implementation” of ValueNotifier, with the difference that it does not depend on Flutter.
- [Freezed article](https://dev.to/marcossevilla/how-to-generate-even-more-code-with-freezed-2ijg)
    - It is a code generator for immutable classes.

Now that you know what each of these tools are and how they work, let's see how we can use them together as a state management solution.

> ⚠️ Before continuing it is important that you know the basics concepts of the tools so read the documentation!

Now let's get started!

![https://media.giphy.com/media/Y3MbPtRn74uR3Ziq4P/source.gif](https://media.giphy.com/media/Y3MbPtRn74uR3Ziq4P/source.gif)

## Folder Structure

In order to maintain a clean code base, the folder structure is key, so let's start with the idea of separating our application by features. If you have used `flutter_bloc`, you have seen that it is very common to create a bloc folder with its respective **states,** **events** and a **views** folder for the UI part of our feature, something like this:

![https://raw.githubusercontent.com/NoScopeDevs/riverpod_jokes_basic/main/assets/readme/bloc_folder_structure.png](https://raw.githubusercontent.com/NoScopeDevs/riverpod_jokes_basic/main/assets/readme/bloc_folder_structure.png)

Carpeta generada con la extensión de VS Code de bloc v5.6.0

As you can see we have a **feature** called **jokes** in this you will find the logic and views of the jokes app. This structure may be familiar to many who have already used `flutter_bloc`.

But before looking at the code let's see the structure we want to achieve using **Riverpod**, **StateNotifier** and **Freezed**:

![https://raw.githubusercontent.com/NoScopeDevs/riverpod_jokes_basic/main/assets/readme/riverpod_folder_structure.png](https://raw.githubusercontent.com/NoScopeDevs/riverpod_jokes_basic/main/assets/readme/riverpod_folder_structure.png)

You can see that it is a very similar structure to the one proposed by **flutter_bloc.** But in this case, because of how riverpod works we have the following files:

- **jokes_state.dart**
    - This file is where we define the possible states that the **StateNotifier** can emit. Commonly they are: **initial**, **loading**, **data** and **error**.
- **jokes_state.freezed.dart**
    - This is a file that is generated with all the information of the classes that we define in `jokes_state.dart`, we shouldn't worry that much about its content, since it is generated code.
- **jokes_state_notifier.dart**
    - This file contains the definition of the `JokesNotifier`, which is an implementation of  **StateNotifier. T**his will be the core of our state management, since in this class is where we define the methods that will be in charge of changing and emitting the new states when necessary.
- **jokes_provider.dart**
    - This file is where we define the different types of **Providers** that we are going to use in this feature. In this case  we will need 2, the first is a common `Provider` for the repository from where we will obtain a new joke and the second a `StateNotifierProvider` that is a special provider for **StateNotifiers** objects. Or in other words, we could say that this file is where the feature's dependency injection is done.

But with this last point, what happens if we have 2 different features that use the same provider?

Very simple, we can create a file where we are going to define global providers that may be used by several features at the same time. Something like this:

![https://raw.githubusercontent.com/elian-ortega/Riverpod-StateNotifier-Freezed-Example/main/assets/shared_providers.png](https://raw.githubusercontent.com/elian-ortega/Riverpod-StateNotifier-Freezed-Example/main/assets/shared_providers.png)

And if you've already read the documentation, you know it, but just in case I remind you that combining providers with riverpod is very simple.

For example, let's imagine the scenario of a store where we define a `Provider` for the payment methods (`PaymentMethods`) and another provider for the `Checkout`, in this case the `Checkout` provider needs the `PaymentMethods` provider to be able to work and thanks to the `ProviderReference` parameter available when creating a new provider with riverpod, creating the relationship It is very simple:

```dart
///PaymentMethods Provider
final _paymentMethodsProvider = Provider<PaymentMethods>(
  (ProviderReference _) => PaymentMethods(),
);

///CheckOut Provider
final checkoutProvider = Provider<Checkout>(
  (ProviderReference ref) => Checkout(
    paymentMethodsProvider: ref.watch(_paymentMethodsProvider),
  ),
);
```

> 📖 If you still do not understand this last part very well, I recommend you reading the articles of the tools that I left at the beginning, here.

Let's see the code 🔥

![https://media.giphy.com/media/LmNwrBhejkK9EFP504/source.gif](https://media.giphy.com/media/LmNwrBhejkK9EFP504/source.gif)

## Project setup

You may laugh at this step, but you would be amazed at how many times it is forgotten. To use riverpod you have to add a `ProviderScope` widget as the root of our application, this allows the communication of providers and is required to use riverpod.

```dart
///main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_jokes_basic/src/app.dart';

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

Personally, I like to separate the **main.dart** from the rest of the application and create an **app.dart** file that contains the root of the application and any extra configuration that is needed.

```dart
///app.dart

import 'package:flutter/material.dart';
import 'package:riverpod_jokes_basic/src/features/jokes/views/jokes_page.dart';

class RiverpodJokesApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: JokesPage(),
    );
  }
}
```

And just like that, we are ready to start using riverpod. Remember to include the dependencies in the **pubspec.yaml** file**,** as of the moment I write this article they are:

```yaml
# pubspec.yaml

name: riverpod_jokes_basic
description: A new Flutter project.

publish_to: 'none'

version: 1.0.0+1

environment:
  sdk: ">=2.12.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  dio: ^4.0.0
  equatable: ^2.0.0
  flutter_riverpod: ^0.14.0+1
  freezed_annotation: ^0.14.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^1.12.2
  freezed: ^0.14.1+2
  json_serializable: ^4.1.0

flutter:

  uses-material-design: true
```

## Models

Before we can code the logic side of the application, we need to create the models that represent the objects from the API. In this case, we are going to be guided by the documentation provided by Jokes API, where they tell us that the response format looks like this:

```json
/// JOKES API Response
{
    "error": false,
    "category": "Programming",
    "type": "twopart",
    "setup": "How many programmers does it take to screw in a light bulb?",
    "delivery": "None. It's a hardware problem.",
    "flags": {
        "nsfw": false,
        "religious": false,
        "political": false,
        "racist": false,
        "sexist": false,
        "explicit": false
    },
    "id": 1,
    "safe": true,
    "lang": "en"
}
```

Just by looking at the response format, we can identify the 2 models that we are going to use. The first one is a model for the possible flags of the joke, we will call it `FlagsModel` and another model for the whole joke that we will call `JokesModel`.

These objects use **equatable** and **json_serializable** to generate the `fromJson()` and `toJson()` methods.

```dart
// flags_model.dart

import 'package:equatable/equatable.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'flags_model.g.dart';

@JsonSerializable()
class FlagsModel extends Equatable {
  FlagsModel({
    this.explicit,
    this.nsfw,
    this.political,
    this.racist,
    this.religious,
    this.sexist,
  });

  //Json Serializable
  factory FlagsModel.fromJson(Map<String, dynamic> json) =>
      _$FlagsModelFromJson(json);

  Map<String, dynamic> toJson() => _$FlagsModelToJson(this);

  final bool? explicit;
  final bool? nsfw;
  final bool? political;
  final bool? racist;
  final bool? religious;
  final bool? sexist;

  @override
  List<Object?> get props => [
        explicit,
        nsfw,
        political,
        racist,
        religious,
        sexist,
      ];
}
```

```dart
// jokes_model.dart

import 'package:equatable/equatable.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

import './flags_model.dart';

part 'joke_model.g.dart';

@JsonSerializable()
class JokeModel extends Equatable {
  JokeModel({
    required this.safe,
    this.category,
    this.delivery,
    this.flags,
    this.id,
    this.lang,
    this.setup,
    this.type,
  });

  //Json Serializable
  factory JokeModel.fromJson(Map<String, dynamic> json) =>
      _$JokeModelFromJson(json);

  Map<String, dynamic> toJson() => _$JokeModelToJson(this);

  final String? category;
  final String? delivery;
  final FlagsModel? flags;
  final int? id;
  final String? lang;
  final bool safe;
  final String? setup;
  final String? type;

  @override
  List<Object?> get props => [
        category,
        delivery,
        flags,
        id,
        lang,
        safe,
        setup,
        type,
      ];
}
```

Once these 2 classes are created, it is necessary to run the `build_runner` commands since **json_serializable** must generate the parsing methods of the classes.

We run the following commands:

```bash
# When the project depends on Flutter

flutter pub run build_runner build

# In project is pure dart

pub run build_runner build
```

With this all syntax errors should disappear, and we can get to the next step.

## Repository

On this occasion I am not going to focus much on this step or any clean architecture layer/component, since it is not the main goal. If you want to see more examples or documentation of clean architecture and its components you can check my networks where I have other articles, code repositories and videos talking about these concepts.

The reason for creating the repository with an interface, of course good coding practices, but also that you can see how the dependency injection with interfaces can be done when we create the providers.

The **jokes_repository.dart** contains the `IJokesRepository` interface and `JokesRepository` which is the implementation. These only have a `Future<JokeModel> getJoke();` method that makes an API call to get a new joke. If **the response is successful it returns the joke, otherwise an exception is thrown.**

```dart
/// jokes_repository.dart

import 'package:dio/dio.dart';
import 'package:riverpod_jokes_basic/src/features/jokes/data/models/joke_model.dart';

abstract class IJokesRepository {
  Future<JokeModel> getJoke();
}

class JokesRepository implements IJokesRepository {
  final _dioClient = Dio();
  final url = 'https://v2.jokeapi.dev/joke/Programming?type=twopart';

  @override
  Future<JokeModel> getJoke() async {
    try {
      final result = await _dioClient.get(url);
      if (result.statusCode == 200) {
        return JokeModel.fromJson(result.data);
      } else {
        throw Exception();
      }
    } catch (_) {
      throw Exception();
    }
  }
}
```

## States using Freezed

In this case, our feature of generating random numbers is not very complex, the objective is to simulate the call to an API, and react to the different states in the UI. So, we are going to have **4 states: initial, loading (loading), data** when we already have the new joke and **error** to give feedback to the user about what is happening.

```dart
/// jokes_state.dart

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_jokes_basic/src/features/jokes/data/models/joke_model.dart';

part 'jokes_state.freezed.dart';

///Extension Method for easy comparison
extension JokesGetters on JokesState {
  bool get isLoading => this is _JokesStateLoading;
}

@freezed
abstract class JokesState with _$JokesState {
  ///Initial
  const factory JokesState.initial() = _JokesStateInitial;

  ///Loading
  const factory JokesState.loading() = _JokesStateLoading;

  ///Data
  const factory JokesState.data({required JokeModel joke}) = _JokesStateData;

  ///Error
  const factory JokesState.error([String? error]) = _JokesStateError;
}
```

Also, we added an extension method to simplify the comparison made to know if the current state is loading.

Once we have defined the structure of the states with the respective **Freezed** **annotations**, we must run a command to create the **jokes_state.freezed.dart** file with generated code.

In the terminal:

```bash
# In this case our project depend on flutter
# so we run

flutter pub run build_runner build

# If the project was pure dart we run

pub run build_runner build
```

With this the new file will be generated resolving the syntax error from the states file.

> ⚠️ The **jokes_state.freezed.dart** code is generated so don't be stressed or intimidated if you don't understand it, it shouldn't be modified.

## Implementing the StateNotifier

As I mentioned earlier, this is the core of our state management.

![https://media.giphy.com/media/xUPGchIuMGVrjvVrDq/source.gif](https://media.giphy.com/media/xUPGchIuMGVrjvVrDq/source.gif)

In this class is where we will make the calls to the repository, and we will assign the states so that they are notified to the components in the UI.

```dart
/// jokes_state_notifier.dart

part of 'jokes_provider.dart';

class JokesNotifier extends StateNotifier<JokesState> {
  JokesNotifier({
    required IJokesRepository jokesRepository,
  })   : _jokesRepository = jokesRepository,
        super(const JokesState.initial());

  final IJokesRepository _jokesRepository;

  Future<void> getJoke() async {
    state = const JokesState.loading();

    try {
      final joke = await _jokesRepository.getJoke();
      state = JokesState.data(joke: joke);
    } catch (_) {
      state = JokesState.error('Error!');
    }
  }
}
```

You can see that the first thing we do is in the `super()` method of the class we assign the first state to `JokesState.initial()`. Then, we only have a `getJoke()` method that makes a call to the repository within a `try {} catch {}` because as we saw previously, in some scenarios we are going to throw an exception. Based on this we will assign the new state to either `state = JokesState.data(joke: joke);` **when the API call is successfull** or `state = JokesState.error('Error!');` when an exception is thrown and caught by the `catch()`.

## Exposing providers and injecting dependencies

Up to this point we have already created all the necessary components for the application logic to work. Now we must create 2 providers, first the **Providers that inject the dependencies** in this case of the `JokesRepository` with their respective **interface** and second the **Provider that expose the StateNotifier** in this case a **StateNotifierProvider** that exposes the `JokesNotifier` so that we can react to changes in states in the UI.

```dart
/// jokes_provider.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_jokes_basic/src/features/jokes/data/repositories/jokes_repository.dart';
import 'jokes_state.dart';
export 'jokes_state.dart';

part 'jokes_state_notifier.dart';

///Dependency Injection

//* Logic / StateNotifier
final jokesNotifierProvider = StateNotifierProvider<JokesNotifier, JokesState>(
  (ref) => JokesNotifier(
    jokesRepository: ref.watch(_jokesRepositoryProvider),
  ),
);

//* Repository
final _jokesRepositoryProvider = Provider<IJokesRepository>(
  (ref) => JokesRepository(),
);
```

You can see that as I showed you previously in the **PaymentMethods/Checkout** example, combining the providers is very simple, in this case we use the `ProviderReference (ref)` **to inject the dependency** that the `JokesNotifier` has of the repository.

Some might wonder why I declare `_jokesRepositoryProvider` as a private variable? This is simply for good practices, with this you avoid making the mistake of making calls directly to the repository from the UI.

With this we have concluded the implementation of all the necessary logic components for this application. Now we only have to implement the UI and react to the states.

## Reacting to states in the UI

In this case, to simplify the example we only have one page in the UI. But before, I show you the code I want to emphasize 2 important points.

Obviously remember to read the documentation, but just to remind you:

### How to call a method on the StateNotifierProvider

If you only want to call one method, in this case we need to call the `getJoke()` method of the **StateNotifier**:

```dart
/// To call a method 
context.read(providerVariable.notifier).methodToCall();

/// In our case to call the state notifier
context.read(jokesNotifierProvider.notifier).getJoke();
```

### How to listen to changes in states

If you want to listen to the state changes emitted by a **StateNotifier** there are different ways, but the simplest is to use a `ConsumerWidget` this gives us access to a property called watch of type `ScopedReader` with this we can listen to the different state changes of the wanted provider.

```dart
/// Using a ConsumerWidget
class NewWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final state = watch(randomNumberNotifierProvider);
    return ///Here you can do whatever you want with the state
  }
}
```

In our case it is even simpler because when we use freezed to create states we have access to the `when ()` method, this allows us to react to each of the states more easily avoiding the problem of many `if - else` conditions in the code .

For this implementation we could have something like this:

```dart
class _JokeConsumer extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final state = watch(jokesNotifierProvider);

    return state.when(
      initial: () => Text('Press the button to start'),
      loading: () => Center(child: CircularProgressIndicator()),
      data: (joke) => Text('${joke.setup}\n${joke.delivery}'),
      error: (error) => Text('Error Occured!'),
    );
  }
}
```

This `ConsumerWidget` return a different widget for each of the different states. 

With this  we are ready to create the page for our application: 

```dart
/// jokes_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_jokes_basic/src/features/jokes/logic/jokes_provider.dart';

///JokesPage
class JokesPage extends StatelessWidget {
  ///JokesPage constructor
  const JokesPage({Key? key}) : super(key: key);

  ///JokesPage [routeName]
  static const routeName = 'JokesPage';

  ///Router for JokesPage
  static Route route() {
    return MaterialPageRoute<void>(builder: (_) => const JokesPage());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Riverpod Jokes'),
      ),
      body: Padding(
        padding: EdgeInsets.symmetric(horizontal: 20.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            _JokeConsumer(),
            const SizedBox(height: 50),
            _ButtonConsumer(),
          ],
        ),
      ),
    );
  }
}

class _JokeConsumer extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final state = watch(jokesNotifierProvider);

    return state.when(
      initial: () => Text(
        'Press the button to start',
        textAlign: TextAlign.center,
      ),
      loading: () => Center(
        child: CircularProgressIndicator(),
      ),
      data: (joke) => Text('${joke.setup}\n${joke.delivery}'),
      error: (error) => Text('Error Occured!'),
    );
  }
}

class _ButtonConsumer extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final state = watch(jokesNotifierProvider);
    return ElevatedButton(
      child: Text('Press me to get a joke'),
      onPressed: !state.isLoading
          ? () {
              context.read(jokesNotifierProvider.notifier).getJoke();
            }
          : null,
    );
  }
}
```

And this would be the result:

![https://raw.githubusercontent.com/elian-ortega/Riverpod-StateNotifier-Freezed-Example/main/assets/functionality.gif](https://raw.githubusercontent.com/elian-ortega/Riverpod-StateNotifier-Freezed-Example/main/assets/functionality.gif)

It's not the prettiest UI in the world, but that wasn't the goal, it was learning how to use riverpod, freezed, and statenotifier together as a state manager.

This is a fairly basic example if you want to see an example with a complete clean architecture setup I leave you these links:

[Jokes application created with riverpod following clean architecture principles](https://github.com/NoScopeDevs/flutter_jokes)

[App with COVID 19 API and minimal architecture](https://github.com/NoScopeDevs/covid_19_clean_scope_minimal)

[Qr generator application with testing flutter_bloc and riverpod implementations + clean architecture](https://github.com/elian-ortega/mobile-test/tree/master/qr_generator_flutter)

For the future I plan to share an article on how to do the testing and other things.

### As always...

Thanks to my friend  [Marcos Sevilla](https://twitter.com/MarcosJSevilla) for this outro 😂, he also shares great content so here is his  [Twitter](https://twitter.com/MarcosJSevilla) if you want to stay tuned of the content.

You can share this article to help another developer to continue improving their productivity when writing applications with Flutter.

There's a Spanish version of this article on Medium. [Click here.](https://elian-mortega.medium.com/como-empezar-con-riverpod-statenotifier-freezed-a47402c772ea) You're welcome. 🇪🇸

Also, if you liked this content, you can find even more and keep in contact with me on my socials:

- [dev.to](https://dev.to/elianortega) - where you're reading this article.
- [GitHub](https://github.com/elian-ortega) - where are my code repositories in case you like the examples.
- [GitHub NoScope](https://github.com/NoScopeDevs) - where you can find the code repositories used in the YouTube and Twitch channels.
- [LinkedIn](https://www.linkedin.com/in/elian-ortega/) - where I connect professionally.
- [Medium](https://elian-mortega.medium.com/) - where I publish my Spanish articles.
- [Twitter](https://twitter.com/ElianMOrtega) -  where I express my short thoughts and share my content.
- [Twitch](https://www.twitch.tv/noscopedevs) - where I do informal live shows from which I take clips with specific information.
- [YouTube](https://www.youtube.com/c/noscopedevs) - where I publish the clips that come out of my lives.