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.
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 🙂
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.
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.
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](https://quickcoder.org/wp-content/uploads/2024/06/fetch_async.gif)
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.
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:
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.
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.
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](https://quickcoder.org/wp-content/uploads/2024/06/fetch_future.gif)
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.
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:
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:
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.
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:
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.
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);
}
}
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](https://quickcoder.org/wp-content/uploads/2024/06/filter_multiple.gif)
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:
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:
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.
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.
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.
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](https://quickcoder.org/wp-content/uploads/2024/06/load_more.gif)
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 🙂
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:
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.
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.
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](https://quickcoder.org/wp-content/uploads/2024/06/refresh_future.gif)
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:
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:
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:
enum Sort { byName, byAge, byId }
Application
This is the service for sorting the persons based on the sorting option:
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.
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.
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](https://quickcoder.org/wp-content/uploads/2024/06/sort_async.gif)
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!
Related articles
![Flutter category image](https://quickcoder.org/wp-content/uploads/2023/01/fluttercat-150x150.png)
![Flutter category image](https://quickcoder.org/wp-content/uploads/2023/01/fluttercat-150x150.png)