How To Use Riverpod In Flutter Apps – A Practical Approach

Flutter category image

Introduction

Here is my practical guide about Riverpod. Learn how to use Riverpod in Flutter apps with simple code examples. Explore AsyncNotifierProvider, NotifierProvider, and FutureProvider in real-world scenarios.

Riverpod is probably one of the top 2 state management solutions in Flutter apps together with BLoC according to my observations. In this guide, I want to show you some real-world use cases and how you can implement them with Riverpod. I assume you already used Riverpod before and gained some first-hand experience. So, let’s get started on how to use Riverpod in Flutter apps.

Before we begin, here is what I am going to cover:

  • Fetch data with AsyncNotifierProvider
  • Fetch data with FutureProvider
  • Filter data with multiple providers
  • Load more items in a list with AsyncNotifierProvider
  • Refresh a list with FutureProvider
  • Sort data with AsyncNotifierProvider

The code examples use the Riverpod Architecture presented by codewithandrea. It’s my favorite approach but you are free to use whatever you like. It consists of the layers data (getting data from APIs, files, etc.), domain (model classes), presentation (UI widgets and controllers), and application (optional abstraction layer for complex business logic). I will only talk briefly about those so check out the linked articles to learn more.

💡 Tip

I am not using any annotations or the riverpod generator in the examples!
Read here for information about the Riverpod Generator.

Fetch data with AsyncNotifierProvider

In this example, we assume that we want to read some data from an API and display it in our app. That’s probably the most common use case for the majority of apps around. Let’s see how this can be done:

Data

First, we need to get some data. I created dummy repository classes for all code examples. In this case, it’s a class that returns some Article objects. Alongside we have a simple Provider to access the class.

Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_example/fetch_with_async_notifier_provider_example/domain/article.dart';

final articleRepositoryProvider = Provider((ref) => ArticleRepository());

class ArticleRepository {
  Future<List<Article>> fetchArticles() async {
    await Future.delayed(const Duration(seconds: 2));
    return [
      const Article(
        id: "1",
        title: 'Article 1',
        body: 'This is article 1',
      ),
      const Article(
        id: "2",
        title: 'Article 2',
        body: 'This is article 2',
      ),
      const Article(
        id: "3",
        title: 'Article 3',
        body: 'This is article 3',
      ),
    ];
  }
}

Domain

Here is the corresponding Article object with fromJson, toJson, and Equatable implementation. They are not required for these examples but I tend to have them always. Just a coding habit of mine 🙂

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

class Article extends Equatable {
  final String id;
  final String title;
  final String body;

  const Article({
    required this.id,
    required this.title,
    required this.body,
  });

  factory Article.fromJson(Map<String, dynamic> json) {
    return Article(
      id: json['id'],
      title: json['title'],
      body: json['body'],
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'title': title,
      'body': body,
    };
  }

  @override
  List<Object?> get props => [id, title, body];
}

Presentation

Now, let’s look at the UI layer. Here we have two classes, the ArticleListController and the ArticleListWidget.

The widget is a ConsumerStatefulWidget (Riverpod version of a StatefulWidget) and the State class extends ConsumerState<T> instead of State<T>. In the build() method, we watch the controller and based on the state we display the corresponding content with the when() method. To kick things off initially, we use the initState() method and call the controller to fetch data.

Dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_example/fetch_with_async_notifier_provider_example/presentation/article_list_controller.dart';

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

  @override
  ConsumerState<ConsumerStatefulWidget> createState() => _State();
}

class _State extends ConsumerState<ArticleListWidget> {
  @override
  void initState() {
    super.initState();

    Future.delayed(const Duration(seconds: 1)).then((_) async =>
        await ref.read(articleListControllerProvider.notifier).fetchArticles());
  }

  @override
  Widget build(BuildContext context) {
    final articleState = ref.watch(articleListControllerProvider);

    return Scaffold(
        body: articleState.when(
            data: (data) => ListView(
                children: data.map((item) => Text(item.title)).toList()),
            loading: () => const Center(child: CircularProgressIndicator()),
            error: (error, stackTrace) =>
                Center(child: Text('Error: $error'))));
  }
}

The controller extends AutoDisposeAsyncNotifier<List<Article>> (because we want to display a list of articles) which disposes of the controller automatically when it is removed from the widget tree (no memory leaks!). The build() method returns an empty list. To actually get data, we need to call the fetchArticles() method. It will return a loading state first and eventually the data from the repository.

An AsyncNotifierProvider expects two type arguments: The controller (ArticleListController) class and the type of the controller class (List<Article>). It connects the UI layer with the data layer.

Dart
import 'dart:async';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_example/fetch_with_async_notifier_provider_example/domain/article.dart';
import 'package:riverpod_example/fetch_with_async_notifier_provider_example/data/article_repository.dart';

final articleListControllerProvider =
    AsyncNotifierProvider.autoDispose<ArticleListController, List<Article>>(
        () => ArticleListController());

class ArticleListController extends AutoDisposeAsyncNotifier<List<Article>> {
  @override
  FutureOr<List<Article>> build() => [];

  Future<void> fetchArticles() async {
    state = const AsyncValue.loading();

    state = await AsyncValue.guard(
        ref.read(articleRepositoryProvider).fetchArticles);
  }
}

To summarize the flow:

  • The UI watches the controller
  • The controller uses the repository to grab data
  • To kick things off initially, we trigger the controller once to fetch data

💡 Tip

No idea about AsyncValue.guard?
It’s just a try-catch alternative. For details, read this article.

While this solutions works, it has some drawbacks.

  • A lot of code for just fetching some data
  • The controller needs to be triggered manually

I will address both problems in the next example. But you get the point that there are multiple ways to achieve your goals with Riverpod. It takes practice to be really efficient.

Example video of fetching data with an AsyncNotifierProvider
Example video of fetching data with an AsyncNotifierProvider

Here is the link to the source code of this example.

Fetch data with FutureProvider

Now we have the exact same use case as in the example before. But we are going to use FutureProvider instead of AsyncNotifierProvider this time. In the end, you’ll see the difference and know why FutureProvider is better suited for this scenario. Here are the details:

Data

A similar repository like before. But this time, it’s about posts and not articles. Apart from the name, they are identical.

Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_example/fetch_with_future_provider_example/domain/post.dart';

final postRepositoryProvider = Provider((ref) => PostRepository());

class PostRepository {
  Future<List<Post>> fetchPosts() async {
    await Future.delayed(const Duration(seconds: 2));
    return [
      const Post(
        id: "1",
        title: 'Article 1',
        body: 'This is article 1',
      ),
      const Post(
        id: "2",
        title: 'Article 2',
        body: 'This is article 2',
      ),
      const Post(
        id: "3",
        title: 'Article 3',
        body: 'This is article 3',
      ),
    ];
  }
}

Domain

The corresponding model class looks like this:

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

class Post extends Equatable {
  final String id;
  final String title;
  final String body;

  const Post({
    required this.id,
    required this.title,
    required this.body,
  });

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      id: json['id'],
      title: json['title'],
      body: json['body'],
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'title': title,
      'body': body,
    };
  }

  @override
  List<Object?> get props => [id, title, body];
}

Presentation

The widget is now a ConsumerWidget (Riverpod version of a StatelessWidget) instead of a ConsumerStatefulWidget, because we don’t need the initState() method anymore. The code is already shorter and more performant.

Dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_example/fetch_with_future_provider_example/presentation/post_list_controller.dart';

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

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

    return Scaffold(
        body: postsState.when(
            data: (data) => ListView(
                children: data.map((item) => Text(item.title)).toList()),
            loading: () => const Center(child: CircularProgressIndicator()),
            error: (error, stackTrace) =>
                Center(child: Text('Error: $error'))));
  }
}

And look at the controller! It’s just an auto-disposable FutureProvider of type List<Post> in 2 lines of code and it does the same as the controller in the previous example including an initial fetch without manual interaction. A FutureProvider gives you things automatically (like a loading state) while an AsyncNotifierProvider requires setting the state manually for that. We don’t need a specialized controller class with this approach.

Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_example/fetch_with_future_provider_example/domain/post.dart';
import 'package:riverpod_example/fetch_with_future_provider_example/data/post_repository.dart';

final postListControllerProvider = FutureProvider.autoDispose<List<Post>>(
    (ref) => ref.read(postRepositoryProvider).fetchPosts());

You see that this approach is much shorter, easier to read and understand, and even faster than the previous example. A FutureProvider works best if you only want to call async methods from other layers. When you have more logic in your controller, choose an AsyncNotifierProvider instead.

Example video of fetching data with a FutureProvider
Example video of fetching data with a FutureProvider

Follow the link to the code of this example.

Filter data with multiple providers

After the initial examples, let’s make it a bit more complex. In this case, we fetch data and implement a filter function. For that, we will combine a NotifierProvider and an AsyncNotifierProvider. Let’s explore how that can look like:

Data

Once again, we have a simple repository that returns some data to play with.

Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_example/filter_data_multiple_providers_example/domain/book.dart';

final bookRepositoryProvider = Provider<BookRepository>((ref) {
  return BookRepository();
});

class BookRepository {
  Future<List<Book>> fetchBooks() async {
    await Future.delayed(const Duration(seconds: 1));
    return [
      const Book(title: 'Book 1', author: 'Author 1', price: 9.99),
      const Book(title: 'Book 2', author: 'Author 2', price: 19.99),
      const Book(title: 'Book 3', author: 'Author 3', price: 29.99),
      const Book(title: 'Book 4', author: 'Author 4', price: 39.99),
    ];
  }
}

Domain

The Book class looks like this:

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

class Book extends Equatable {
  final String title;
  final String author;
  final double price;

  const Book({
    required this.title,
    required this.author,
    required this.price,
  });

  @override
  List<Object?> get props => [title, author, price];

  factory Book.fromJson(Map<String, dynamic> json) {
    return Book(
      title: json['title'] as String,
      author: json['author'] as String,
      price: json['price'] as double,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'title': title,
      'author': author,
      'price': price,
    };
  }
}

And we also this enum to represent the filter options:

Dart
enum FilterOptionEnum {
  priceMoreThan5,
  priceMoreThan15,
  priceMoreThan25,
  priceMoreThan35
}

For simplicity, the user can only filter the books by price. Of course, you can easily extend this to support other properties if you get the idea.

Application

In this example we make use of the optional application layer because the filter logic does not belong to the presentation layer. Here is the FilterService class that filters a list of books based on the given filter value. We expose the service with a provider so that we can use it in the controller.

Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_example/filter_data_multiple_providers_example/domain/book.dart';
import 'package:riverpod_example/filter_data_multiple_providers_example/domain/filter_option_enum.dart';

final filterServiceProvider = Provider<FilterService>((ref) {
  return FilterService();
});

class FilterService {
  FilterService();

  Future<List<Book>> filterBooks(
      List<Book> books, FilterOptionEnum filter) async {
    switch (filter) {
      case FilterOptionEnum.priceMoreThan5:
        return books.where((book) => book.price > 5).toList();
      case FilterOptionEnum.priceMoreThan15:
        return books.where((book) => book.price > 15).toList();
      case FilterOptionEnum.priceMoreThan25:
        return books.where((book) => book.price > 25).toList();
      case FilterOptionEnum.priceMoreThan35:
        return books.where((book) => book.price > 35).toList();
    }
  }
}

In addition, we have a Notifier that holds the currently selected filter option by the user. Here is how you can implement it:

Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_example/filter_data_multiple_providers_example/domain/filter_option_enum.dart';

final filterOptionProvider =
    NotifierProvider<FilterOption, FilterOptionEnum>(FilterOption.new);

class FilterOption extends Notifier<FilterOptionEnum> {
  @override
  build() => FilterOptionEnum.priceMoreThan5;

  void setFilter(FilterOptionEnum filter) {
    state = filter;
  }
}

By default, the filter is set to priceMoreThan5 by the build() method. To update it, there is the setFilter() method that updates the state property.

Presentation

Now, here is some UI code. There are 4 buttons for the different filter options. Pressing the button calls the setFilter() method of the filterOptionProvider. The controller watches this provider and returns an updated list of books after calling the filter service.

Dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_example/filter_data_multiple_providers_example/application/filter_option_provider.dart';
import 'package:riverpod_example/filter_data_multiple_providers_example/domain/filter_option_enum.dart';
import 'package:riverpod_example/filter_data_multiple_providers_example/presentation/book_filter_controller.dart';

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

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

    return Scaffold(
        body: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
      Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
        ElevatedButton(
            onPressed: () => _setFilter(ref, FilterOptionEnum.priceMoreThan5),
            child: const Text("Show books over 5€")),
        ElevatedButton(
            onPressed: () => _setFilter(ref, FilterOptionEnum.priceMoreThan15),
            child: const Text("Show books over 15€")),
        ElevatedButton(
            onPressed: () => _setFilter(ref, FilterOptionEnum.priceMoreThan25),
            child: const Text("Show books over 25€")),
        ElevatedButton(
            onPressed: () => _setFilter(ref, FilterOptionEnum.priceMoreThan35),
            child: const Text("Show books over 35€")),
      ]),

      filterState.when(
          data: (books) => ListView(
              shrinkWrap: true,
              children: books
                  .map((item) =>
                      Text("${item.title}, ${item.author}, ${item.price}"))
                  .toList()),
          loading: () => const CircularProgressIndicator(),
          error: (error, stack) => Text("Error: $error"))
    ]));
  }

  Future<void> _setFilter(WidgetRef ref, FilterOptionEnum filter) async {
    ref.read(filterOptionProvider.notifier).setFilter(filter);
  }
}
Dart
import 'dart:async';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_example/filter_data_multiple_providers_example/application/filter_option_provider.dart';
import 'package:riverpod_example/filter_data_multiple_providers_example/application/filter_service.dart';
import 'package:riverpod_example/filter_data_multiple_providers_example/domain/book.dart';
import 'package:riverpod_example/filter_data_multiple_providers_example/data/book_repository.dart';

final bookFilterControllerProvider =
    AutoDisposeAsyncNotifierProvider<BookFilterController, List<Book>>(
        () => BookFilterController());

class BookFilterController extends AutoDisposeAsyncNotifier<List<Book>> {
  @override
  FutureOr<List<Book>> build() async {
    final books = await ref.read(bookRepositoryProvider).fetchBooks();
    final filter = ref.watch(filterOptionProvider);
    return await ref.read(filterServiceProvider).filterBooks(books, filter);
  }
}

Since the controller only uses the build() method, you’ll get results as soon as the widget is created initially. And you could replace the AsyncNotifier with a FutureProvider to make the code shorter as we have already learned before.

Short recap

  • Notifier holds current filter value
  • Controller watches filter notifier, grabs data from the repository, and returns the filtered results
  • Optional application layer is used to keep logic away from the controller
  • All we do is set the filter and Riverpod does the rest
Example video of filtering data with multiple providers
Example video of filtering data with multiple providers

Learn all details with the corresponding source code.

Load more items in a list with AsyncNotifierProvider

Another common task is load more data in a list. Many social networks use inifinite scrolling, a technique to load more data once when user scrolls near the end of a list. Here is how you can do that with Riverpod:

Data

Here is another repository that provides data for this example:

Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_example/load_more_list_items_with_async_notifier_provider_example/domain/car.dart';

final carRepositoryProvider = Provider((ref) => CarRepository());

class CarRepository {
  Future<List<Car>> fetchCars(int howMany) async {
    await Future.delayed(const Duration(seconds: 2));
    return List.generate(
        howMany,
        (index) => Car(
              horsepower: (index + 1) * 50,
              manufacturer: 'Brand ${index + 1}',
              model: 'Model ${index + 1}',
              year: 2022 - index,
            ));
  }
}

Domain

And this is the corresponding model class:

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

class Car extends Equatable {
  final int horsepower;
  final String manufacturer;
  final String model;
  final int year;

  const Car({
    required this.horsepower,
    required this.manufacturer,
    required this.model,
    required this.year,
  });

  @override
  List<Object?> get props => [horsepower, manufacturer, model, year];

  factory Car.fromJson(Map<String, dynamic> json) {
    return Car(
      horsepower: json['horsepower'] as int,
      manufacturer: json['manufacturer'] as String,
      model: json['model'] as String,
      year: json['year'] as int,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'horsepower': horsepower,
      'manufacturer': manufacturer,
      'model': model,
      'year': year,
    };
  }
}

Application

In this layer, we define a fetch count provider as a NotifierProvider. The notifier holds a number that indicates how many results the repository should return. By default, it’s 5 and there is a method to increase the number by 5.

Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

final fetchCountProvider = NotifierProvider<FetchCount, int>(() {
  return FetchCount();
});

class FetchCount extends Notifier<int> {
  @override
  int build() {
    return 5;
  }

  void incBy5() {
    state = state + 5;
  }
}

Presentation

The widget contains a button that increases the fetch count by calling the notifier method we defined earlier.

Dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_example/load_more_list_items_with_async_notifier_provider_example/application/fetch_count_provider.dart';
import 'package:riverpod_example/load_more_list_items_with_async_notifier_provider_example/presentation/car_list_controller.dart';

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

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

    return Scaffold(
        body:
            carState.when(
                data: (data) => Column(children: [
                      ListView(
                          shrinkWrap: true,
                          children: data
                              .map((item) => Text(
                                  "${item.manufacturer}, ${item.horsepower}, ${item.model}, ${item.year}"))
                              .toList()),
                      const SizedBox(height: 10),
                      ElevatedButton(
                          onPressed: () => _loadMoreItems(ref),
                          child: const Text("Load more items")),
                    ]),
                loading: () => const CircularProgressIndicator(),
                error: (error, stack) => Text("Error: $error")));
  }

  void _loadMoreItems(WidgetRef ref) {
    ref.read(fetchCountProvider.notifier).incBy5();
  }
}

The controller works similar to the one in the previous example. It watches the fetch count provider and passes the value to the repository which returns the exact amount of requested items.

Dart
import 'dart:async';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_example/load_more_list_items_with_async_notifier_provider_example/application/fetch_count_provider.dart';
import 'package:riverpod_example/load_more_list_items_with_async_notifier_provider_example/domain/car.dart';
import 'package:riverpod_example/load_more_list_items_with_async_notifier_provider_example/data/car_repository.dart';

final carListControllerProvider =
    AutoDisposeAsyncNotifierProvider<CarListController, List<Car>>(
        () => CarListController());

class CarListController extends AutoDisposeAsyncNotifier<List<Car>> {
  @override
  FutureOr<List<Car>> build() async {
    final itemsCount = ref.watch(fetchCountProvider);
    final repository = ref.read(carRepositoryProvider);
    return await repository.fetchCars(itemsCount);
  }
}

If you followed the previous examples, you should see the similarities and differences by now.

Example video of how to load more list items with a FutureProvider
Example video of how to load more list items with a FutureProvider

Check out the example source code.

Refresh a list with FutureProvider

Pull to refresh is a common pattern in mobile apps and websites to reload a list. With Riverpods FutureProvider, this is implemented in minutes. Get ready for the details:

Data

Another example, another dummy repository 🙂

Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_example/refresh_list_items_with_future_provider_example/domain/animal.dart';

final animalRepositoryProvider = Provider((ref) => AnimalRepository());

class AnimalRepository {
  Future<List<Animal>> fetch() async {
    await Future.delayed(const Duration(seconds: 2));
    return [
      Animal(name: 'Lion', age: DateTime.now().second, species: 'Panthera leo'),
      Animal(
          name: 'Elephant',
          age: DateTime.now().microsecond,
          species: 'Loxodonta africana'),
      Animal(
          name: 'Giraffe',
          age: DateTime.now().millisecond,
          species: 'Giraffa camelopardalis')
    ];
  }
}

Domain

This is the model class for the repository:

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

class Animal extends Equatable {
  final String name;
  final int age;
  final String species;

  const Animal({
    required this.name,
    required this.age,
    required this.species,
  });

  @override
  List<Object?> get props => [name, age, species];

  factory Animal.fromJson(Map<String, dynamic> json) {
    return Animal(
      name: json['name'] as String,
      age: json['age'] as int,
      species: json['species'] as String,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'name': name,
      'age': age,
      'species': species,
    };
  }
}

Presentation

The widget contains a button that calls ref.invalidate() on a provider. Since we watch this provider, it will immediately recompute the return value and trigger a rebuild of the widget.

Dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_example/refresh_list_items_with_future_provider_example/presentation/animal_list_controller.dart';

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

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

    return Scaffold(
        body:
            animalState.when(
                skipLoadingOnRefresh: false,
                data: (data) => Column(children: [
                      ListView(
                          shrinkWrap: true,
                          children: data
                              .map((item) => Text(
                                  "${item.name}, ${item.age}, ${item.species}"))
                              .toList()),
                      const SizedBox(height: 10),
                      ElevatedButton(
                          onPressed: () => _refresh(ref),
                          child: const Text("Refresh")),
                    ]),
                loading: () => const CircularProgressIndicator(),
                error: (error, stack) => Text("Error: $error")));
  }

  void _refresh(WidgetRef ref) {
    ref.invalidate(animalListControllerProvider);
  }
}

The controller is a FutureProvider that calls the repository for data.

Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'package:riverpod_example/refresh_list_items_with_future_provider_example/domain/animal.dart';
import 'package:riverpod_example/refresh_list_items_with_future_provider_example/data/animal_repository.dart';

final animalListControllerProvider = FutureProvider.autoDispose<List<Animal>>(
    (ref) async => await ref.read(animalRepositoryProvider).fetch());

With ref.invalidate() or ref.refresh() you can force a provider to recalculate its value.

Example video of how to refresh a FutureProvider
Example video of how to refresh a FutureProvider

Read more about the entire source code here.

Sort data with AsyncNotifierProvider

The last example shows how you can sort data. Easy, but effective. Let’s jump right in:

Data

Here is the data provider:

Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_example/sort_data_with_async_notifier_provider_example/domain/person.dart';

final personRepositoryProvider = Provider((ref) => PersonRepository());

class PersonRepository {
  Future<List<Person>> fetchPersons() async {
    await Future.delayed(const Duration(seconds: 1));
    return [
      const Person(id: 1, name: 'John Doe', age: 30),
      const Person(id: 2, name: 'Jane Doe', age: 25),
      const Person(id: 3, name: 'Alice', age: 35),
      const Person(id: 4, name: 'Bob', age: 40),
    ];
  }
}

Domain

And this is the domain model for the repository:

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

class Person extends Equatable {
  final int id;
  final String name;
  final int age;

  const Person({
    required this.id,
    required this.name,
    required this.age,
  });

  @override
  List<Object?> get props => [id, name, age];

  factory Person.fromJson(Map<String, dynamic> json) {
    return Person(
      id: json['id'] as int,
      name: json['name'] as String,
      age: json['age'] as int,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'age': age,
    };
  }
}

Additionally, we have an enum of sorting options:

Dart
enum Sort { byName, byAge, byId }

Application

This is the service for sorting the persons based on the sorting option:

Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_example/sort_data_with_async_notifier_provider_example/domain/person.dart';
import 'package:riverpod_example/sort_data_with_async_notifier_provider_example/domain/sort.dart';
import 'package:riverpod_example/sort_data_with_async_notifier_provider_example/data/person_repository.dart';

final sortServiceProvider = Provider((ref) => SortService(ref));

class SortService {
  final ProviderRef ref;
  
  SortService(this.ref);
  
  Future<List<Person>> sortPersons(Sort sort) async {
    final allPersons = await ref.read(personRepositoryProvider).fetchPersons();
      switch (sort) {
        case Sort.byName:
          allPersons.sort((a, b) => a.name.compareTo(b.name));
        case Sort.byAge:
          allPersons.sort((a, b) => a.age.compareTo(b.age));
        case Sort.byId:
          allPersons.sort((a, b) => a.id.compareTo(b.id));
    }
    
    return allPersons;
  }
}

Presentation

In the UI, all the pieces come together. There are 3 buttons for the different sort options. They all call the controller which handles the sorting.

Dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_example/sort_data_with_async_notifier_provider_example/domain/sort.dart';
import 'package:riverpod_example/sort_data_with_async_notifier_provider_example/presentation/person_sort_controller.dart';

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

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

    return Scaffold(
        body: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
      Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
        ElevatedButton(
            onPressed: () => _sortById(ref), child: const Text("Sort by id")),
        ElevatedButton(
            onPressed: () => _sortByName(ref),
            child: const Text("Sort by name")),
        ElevatedButton(
            onPressed: () => _sortByAge(ref), child: const Text("Sort by age")),
      ]),
      sortState.when(
          data: (data) => ListView(
              shrinkWrap: true,
              children: data
                  .map((item) => Text("${item.id}, ${item.name}, ${item.age}"))
                  .toList()),
          loading: () => const CircularProgressIndicator(),
          error: (error, stack) => Text("Error: $error"))
    ]));
  }

  Future<void> _sortById(WidgetRef ref) async {
    ref.read(personSortControllerProvider.notifier).sortPersons(Sort.byId);
  }

  Future<void> _sortByName(WidgetRef ref) async {
    ref.read(personSortControllerProvider.notifier).sortPersons(Sort.byName);
  }

  Future<void> _sortByAge(WidgetRef ref) async {
    ref.read(personSortControllerProvider.notifier).sortPersons(Sort.byAge);
  }
}

Here is the controller. Note that we don’t use an additional Notifier as in previous examples. Instead, the controller has a method sortPersons that expects the sorting option as an argument.

Dart
import 'dart:async';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_example/sort_data_with_async_notifier_provider_example/application/sort_service.dart';
import 'package:riverpod_example/sort_data_with_async_notifier_provider_example/domain/sort.dart';
import 'package:riverpod_example/sort_data_with_async_notifier_provider_example/domain/person.dart';

final personSortControllerProvider =
    AutoDisposeAsyncNotifierProvider<PersonSortController, List<Person>>(
        () => PersonSortController());

class PersonSortController extends AutoDisposeAsyncNotifier<List<Person>> {
  @override
  FutureOr<List<Person>> build() => [];

  Future<void> sortPersons(Sort sort) async {
    state = const AsyncValue.loading();

    state = await AsyncValue.guard(
        () async => await ref.read(sortServiceProvider).sortPersons(sort));
  }
}

This example shows a strict layer separation. The controller from the UI layer calls the SortService class in the application which calls the person repository in the data layer to retrieve data.

Example video of how to sort data with AsyncProviderNotifier
Example video of how to sort data with AsyncProviderNotifier

Follow this link for the full code.

Conclusion

In this article, you learned how to use Riverpod in Flutter apps with real-world scenarios. For beginners, Riverpod can be a bit challenging. But once you get the idea behind it, you can build proper apps with less code than before.

With those examples, you should get the idea behind Riverpod. And remember: There are often multiple ways to achieve the desired results!



Want More Flutter Content?

Join my bi-weekly newsletter that delivers small Flutter portions right in your inbox. A title, an abstract, a link, and you decide if you want to dive in!

Become A Firebase Expert!

Take a deep dive into Firebase and learn how to really handle their services. This ebook makes you an instant Firebase professional!

Flutter โค๏ธ Firebase

Get started with Firebase and learn how to use it in your Flutter apps. My detailed ebook delivers you everything you need to know! Flutter and Firebase are a perfect match!