Using Riverpod and Freezed together in a Flutter application

by Chanan Braunstein, Owner

What is Riverpod?

Riverpod is a state management library that allows you to inject dependencies and encapsulate state in a flexible and testable way. It is created by the same author of Provider, which is another popular state management solution in Flutter. Riverpod has some advantages over Provider, such as:

  • It does not depend on Flutter's context, which means you can access your state from anywhere in your app, without using widgets like Consumer or Provider.of.
  • It supports multiple providers with the same type, which means you can have different instances of the same state without conflicts or naming issues.

You can learn more about Riverpod from its official documentation.

What is Freezed?

Freezed is a code generator that helps you create immutable classes with useful features, such as:

  • Union types, which allow you to define multiple possible states for your class, and use pattern matching to handle them.
  • Copy with, which allows you to create a new instance of your class with some fields modified.
  • From and to json, which allow you to serialize and deserialize your class to and from json format.

Freezed works well with Riverpod, as it allows you to define your state classes with union types, and use them with StateNotifier, which is a state management tool that Riverpod provides.

You can learn more about Freezed from its official documentation.

How to use Riverpod and Freezed together?

To demonstrate how to use Riverpod and Freezed together, we will create a simple app that fetches data from a REST API and displays it in a list view. The API we will use is jsonplaceholder, which provides fake data for testing purposes. We will use the posts endpoint, which returns a list of posts with id, title, and body fields.

Step 1: Add dependencies

First, we need to add the dependencies for Riverpod, Freezed, Dio, and Build Runner in our pubspec.yaml file. Dio is a HTTP client library that we will use to make API requests. Build Runner is a tool that we will use to generate code for Freezed.

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  hooks_riverpod: ^2.3.8
  flutter_hooks: ^0.20.0
  riverpod_annotation: ^2.1.2
  freezed_annotation: ^2.4.1
  json_annotation: ^4.8.1
  dio: ^5.3.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0
  riverpod_generator: ^2.2.6
  build_runner: ^2.4.6
  custom_lint: ^0.5.2
  riverpod_lint: ^2.0.1
  freezed: ^2.4.2
  json_serializable: ^6.7.1

After adding the dependencies, run flutter pub get to install them.

You can also add all the dependencies at once via the command:

flutter pub add hooks_riverpod flutter_hooks riverpod_annotation freezed_annotation json_annotation dio dev:riverpod_generator dev:build_runner dev:custom_lint dev:riverpod_lint dev:freezed dev:json_serializable

Step 2: Create models

Next, we need to create the models for our data. We will use Freezed to generate immutable classes for our posts and API responses. To do that, we need to create two files: post.dart and api_response.dart in the lib/models folder.

In the post.dart file, we will define a class called Post with three fields: id, title, and body. We will also add the @freezed annotation and the part statement to indicate that we want to use Freezed.

import 'package:freezed_annotation/freezed_annotation.dart';

part 'post.freezed.dart';
part 'post.g.dart';

@freezed
class Post with _$Post {
  const factory Post({
    required int id,
    required int userId,
    required String title,
    required String body,
  }) = _Post;

  factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);
}

After creating the model file, we need to run the following command to generate the code for Freezed:

dart run build_runner watch --delete-conflicting-outputs

This will create the post.freezed.dart, post.g.dart files in the same folder. We don't need to worry about the content of these files, as they are generated by Freezed.

Step 3: Create providers

Now, we need to create the providers that will handle the API requests and return the models objects. We will use Dio to make the HTTP requests and handle the errors. We will create a file called dio.dart in the lib/providers folder. This will provide our posts provider with an instance of Dio:

import 'package:dio/dio.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'dio.g.dart';

@riverpod
Dio dio(DioRef ref) => Dio();

In the post.dart file, we will a Riverpod AsyncNotifierProvider a class called Posts. The initial state will be an empty list which is return in the build method. The method getPosts is an async/Future method that will return the results for the API. The method starts by setting the state to AsyncValue.loading() which the UI can use to show a loading indicator. Then the method gets an instance of DIO, which it uses to make a GET request to the posts endpoint and parse the response data into a list of Post objects. If the request is successful, it will set the state to the List of Posts. If the request fails, it will return a Failure state with the error message. This is done using the AsyncValue.guard() function.

import 'package:dio/dio.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../models/post.dart';
import '../providers/dio.dart';

part 'posts.g.dart';

@riverpod
class Posts extends _$Posts {
  @override
  FutureOr<List<Post>> build() {
    return [];
  }

  Future<void> getPosts() async {
    state = const AsyncValue.loading();
    final dio = ref.read(dioProvider);
    state = await AsyncValue.guard(() async {
      final response = await dio.get(
        'https://jsonplaceholder.typicode.com/posts',
        options: Options(
          contentType: 'application/json',
        ),
      );
      return List<Post>.from(response.data.map((item) => Post.fromJson(item)));
    });
  }
}

Step 4: Create UI

Finally, we need to create the UI that will display the posts from the API. We will update the main.dart to accomplish this.First we need to initialize the Riverpod Provider:

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

Then we will use Riverpod's HookConsumerWidget to access our state and update our UI accordingly. We will also create an IconButton to send the API request to load the data:

class MyHomePage extends HookConsumerWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ref.watch(postsProvider).when(
          data: (posts) => Scaffold(
            appBar: AppBar(
              title: const Text('Posts'),
            ),
            body: Column(
              children: [
                IconButton(
                  onPressed: () => ref.read(postsProvider.notifier).getPosts(),
                  icon: const Icon(Icons.refresh),
                ),
                _MyHomePage(posts: posts),
              ],
            ),
          ),
          error: (err, stack) => throw Error(),
          loading: () => Scaffold(
            appBar: AppBar(
              title: const Text('Posts'),
            ),
            body: const CircularProgressIndicator(),
          ),
        );
  }
}

In the snippet above we handle the three states that the AsyncNotifierProvider can be in: data, loading, and error. The states Error and loading are handled inline, but the data state is forwarded to _MyHomePage widget which is shown here:

class _MyHomePage extends StatelessWidget {
  const _MyHomePage({required this.posts});

  final List<Post> posts;

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: ListView.builder(
        itemCount: posts.length,
        itemBuilder: (context, index) {
          final post = posts[index];
          return ListTile(title: Text(post.title));
        },
      ),
    );
  }
}

The full code of main.dart is below:

import 'package:blog/models/post.dart';
import 'package:blog/providers/posts.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Posts',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends HookConsumerWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ref.watch(postsProvider).when(
          data: (posts) => Scaffold(
            appBar: AppBar(
              title: const Text('Posts'),
            ),
            body: Column(
              children: [
                IconButton(
                  onPressed: () => ref.read(postsProvider.notifier).getPosts(),
                  icon: const Icon(Icons.refresh),
                ),
                _MyHomePage(posts: posts),
              ],
            ),
          ),
          error: (err, stack) => throw Error(),
          loading: () => Scaffold(
            appBar: AppBar(
              title: const Text('Posts'),
            ),
            body: const CircularProgressIndicator(),
          ),
        );
  }
}

class _MyHomePage extends StatelessWidget {
  const _MyHomePage({required this.posts});

  final List<Post> posts;

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: ListView.builder(
        itemCount: posts.length,
        itemBuilder: (context, index) {
          final post = posts[index];
          return ListTile(title: Text(post.title));
        },
      ),
    );
  }
}

Finally, if we did everything write the app should like so:

Conclusion

Riverpod and Freezed make developing interactive Flutter applications a breeze. Contact BK Software Development and let us develop the app of your dreams!

More articles

Scoring a Computer Adaptive Test

How to score a Computer Adaptive Test using JavaScript.

Read more

Using Flutter to develop a mobile app for an MDM application

Using Flutter along with an MDM makes it easy to build beautiful, interactive, real-time mobiles applications.

Read more

Tell us about your project