visit
Maximizing the performance of your Flutter app is crucial for delivering a seamless user experience. As software developers, we constantly seek out tools that can enhance our coding experience while improving the efficiency and quality of our code. This beginner's guide focuses on utilizing the power of AsyncNotifierProvider and NotifierProvider from Riverpod, together with the Riverpod code generator, for efficient state management. By incorporating these tools, you can generate providers faster, simplify the process of passing ref property around, and streamline debugging. The guide includes simple examples that demonstrate how to use these providers in your project and leverage the benefits of the Freezed and Riverpod code generators.
AsyncValue
to handle loading statecopyWith
when working with an immutable class.
First, you will need to get to your pubspec.yaml
and add the following packages
dependencies:
flutter_riverpod: ^2.1.3
riverpod_annotation: ^1.1.1
freezed_annotation: ^2.2.0
freezed: ^2.3.2
dev_dependencies:
build_runner:
riverpod_generator: ^1.1.1
Then run flutter pub get
All you need to do is follow the syntax for defining your Riverpod code generator and annotate your code, then with build_runner
you can generate all your providers.
For Providers:
@riverpod
int foo(FooRef ref) => 0;
For FutureProviders:
@riverpod
Future<int> foo(FooRef ref) async {
return 0;
}
For StateProviders:
@riverpod
class Foo extends _$Foo {
@override
int build() => 0;
}
First, using Riverpod annotation and the code syntax below, we will create a NotifierProvider. We will also add the functions for adding a random string and clearing the list of strings. We will add the name of the file to be generated by specifying with part
as seen in the code.
Note: The name of the file to be generated is the same as the name of the current file you are working on. When specifying it with part
you will need to add .g.dart
as that is how Riverpod-generated files are named.
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'notifier_list_provider.g.dart';
@riverpod
class RandomStrNotifier extends _$RandomStrNotifier{
@override
List<String> build() {
return [];
}
//using Dart's spread operator we create a new copy of the list
void addString(String randomStr){
state = [...state, randomStr];
}
void removeStrings(){
state = [];
}
}
From our code, we can see that the RandomStrNotifier
returns an empty List, then we added the two functions to add to the list and clear the list. NotifierProvider and AsyncNotifierProvider support immutable state, because our state is immutable, we can not say state.add
or state.remove
. So we create a new copy of the list. state
is used for updating the UI state.
flutter pub run build_runner watch --delete-conflicting-outputs
Note: If you get the error Could not find a file named "pubspec.yaml" in "C:\Users\…
then run the dart pub get
command on your terminal.
Widget build(BuildContext context, ref) {
// rebuid the widget when there is a change
List<String> randomStrList = ref.watch(randomStrNotifierProvider);
final random = Random();
return Scaffold(
appBar: AppBar(
title: const Text("RiverPod Notifier Example App"),
backgroundColor: Colors.brown,
),
body: SingleChildScrollView(
child: Column(
children: [
Column(
children: [
//map to a list
...randomStrList.map((string) =>
Container(
alignment: Alignment.center,
margin: const EdgeInsets.only(bottom: 10,top: 5),
height: 30,
width: 300,
color: Colors.brown,
child: Text(string.toString(),
style: const TextStyle(
color: Colors.white
),
)))
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton.icon(
icon: const Icon(Icons.add),
label: const Text('Generate'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.brown, // Background color
),
onPressed: () {
//add string to list function
ref.read(randomStrNotifierProvider.notifier).addString("This is the "
"random String ${5 + random.nextInt( 1000 + 1 - 5)}");
},
),
ElevatedButton.icon(
icon: const Icon(Icons.clear),
label: const Text('Clear'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.brown, // Background color
),
onPressed: () {
//clear list function
ref.read(randomStrNotifierProvider.notifier).removeString();
},
)
],
)
],
),
),
);
As you can see the Riverpod code generator, generated a matching randomStrNotifierProvider
Let’s dive into the example:
First, we will use the Freezed code generator tool to create our product class. Then using the copyWith
method we will create the product objects that will be going into our list. We will add the name of the file to be generated by specifying with part
as seen in the code. The name of the generated file is the current name of your file and .freezed.dart
This is how the freezed files are named.
import 'package:freezed_annotation/freezed_annotation.dart';
//replace with part 'name_of_your_file.freezed.dart';
part 'async_notifier_list_provider.freezed.dart';
@freezed
class Product with _$Product{
const Product._();
const factory Product({
String? name,
String? description,
}) = _Product;
}
const Product _product1 = Product(name: "Dart course for beginners",
description: "This is course will make you a dart star");
final Product _product2 = _product1.copyWith(description: "This course will make you a pro");
final Product _product3 = _product1.copyWith(name: "Ultimate Dart course for beginners");
final products = [
_product1,
_product2,
_product3,
];
flutter pub run build_runner watch --delete-conflicting-outputs
We used the copyWith
method to create new objects of product that we added to the list. The copyWith
method is used for returning a new object with the same properties as the original but with the values you have specified, it is used when working with immutable structures like Freezed.
//replace with part 'name_of_your_file.g.dart';
part 'async_notifier_list_provider.g.dart';
@riverpod
class AsyncProducts extends _$AsyncProducts {
Future<List<Product>> _fetchProducts() async {
await Future.delayed(const Duration(seconds: 3));
return products;
}
@override
FutureOr<List<Product>> build() async {
return _fetchProducts();
}
Future<void>clearProducts()async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async{
await Future.delayed(const Duration(seconds: 3));
return [];
});
}
}
The asyncNotifierProvider returns a future list of products. To modify the UI we will now create the clearProducts
function, using AsyncValue class
we can easily manage the loading, error state, and data state.
Looking at the AsyncValue
class, AsyncValue.guard
is used to transform a Future that can fail into something safe to read, it is recommended to use this instead of try and catch blocks. it will handle both the data and error states.
flutter pub run build_runner watch --delete-conflicting-outputs
Widget build(BuildContext context, WidgetRef ref) {
final productProvider = ref.watch(asyncProductsProvider);
return Scaffold(
appBar: AppBar(
title: const Text("AsyncNotifier"),
actions: [
IconButton(
icon: const Icon(
Icons.clear,
color: Colors.white,
),
onPressed: () {
ref.read(asyncProductsProvider.notifier).clearProducts();
},
)
]
),
body: Container(
child: productProvider.when(
data: (products)=> ListView.builder(
itemCount: products.length,
itemBuilder: (context, index){
return Padding(
padding: const EdgeInsets.only(left: 10,right: 10,top: 10),
child: Card(
color: Colors.blueAccent,
elevation: 3,
child: ListTile(
title: Text("${products[index].name}",style: const TextStyle(
color: Colors.white, fontSize: 15)),
subtitle: Text("${products[index].description}",style: const TextStyle(
color: Colors.white, fontSize: 15)),
),
),
);
}),
error: (err, stack) => Text("Error: $err",style: const TextStyle(
color: Colors.white, fontSize: 15)),
loading: ()=> const Center(child: CircularProgressIndicator(color: Colors.blue,)),
),
),
);
}
Here we can see that by calling ref.watch
we can access our provider, then we add the clear product function to the onPressed
by calling ref.read
. Now we can handle the different states of the response, using productProvider.when