Flutter Riverpod

Abubakar Saddique
26 min readJan 27, 2024

--

What is riverpod :

River pod is the state management technique which is the advanced or the updated form of the provider .

For detailed information about the river pod kindly read it from the official documentation .

Riverpod (anagram of Provider) is a reactive caching framework for Flutter/Dart.

It uses the declarative and the reactive programing technique .

The provider’s are the most important part of the river pod . So if you don’t know about the provider i recommend you to read it from the official documentation or you may also get some of the knowledge from my article on provider state management technique .

A provider is an object that can encapsulate the piece of the state and allow listening to that state .

There are some of the following flaw’s or issues in the provider . So that’s why we need riverpod .

i ) Type System :

The provider state management technique is the advanced form of the inherited widget that is used to pass the data from the parent to child by using the widget of the exact type .

If will search the data from the bottom to top of the exact type . If you have two value’s of the type string father name and the user name . Then when you call it by specifying the type to string it will always get or fetch the value of the first one whose value will be found first . It will never fetch the second one .

If the first one is user name and the second one is father name then when you call it using the template type of the string then it will always fetch the user name and never get the data of the user name .

Now you must know one thing that the provider is the type based and the river pod is the object based . You can search the value using the name of the object which mean’s that you can also able to create multiple object of the same type like ( String , int , double ) .

Null Value :

The provider is based on the type system when we call the provider may be it is null or store’s the null value in this case the provider state management technique will throw and error or the exception and as we know that riverpod is object base and the object can never be null you must have to assign some value before using it so you will never get this type of exception in case of riverpod .

Now that’s all for the difference between the river pod and the provider and why we use riverpod . For detailed information kindly visit the official documentation .

Now let’s start coding of the river pod .

To use the riverpod inside your project you must have to add the dependencies of the given below package .

The river pod is based on the provider’s and there a lot of types of the provider’s that are used for the different purposes .

Provider :

According to the official documentation .

Provider is the most basic of all providers .

Provider is a powerful tool for caching synchronous operations when combined with ref.watch.

Let’s start from the simple provider that is used as the advanced form of the inherited widget .

For that purpose i make file of the name my custom providers . dart and initialize my provider’s globally inside it .

final nameProvider = Provider<String>((ref) {
return "Abubakar";
});

final cnicNumberProvider = Provider<String>((ref) {
return "312XXXXXXX715";
});

You must have to given the template type of the provider which is the type of whatever you are doing .

If you want’s a string then it will be string and if it is numerical value then you may use num , int , double or bool for boolean values .

For displaying the provider in my project i make a consumer state full widget of the name Simple Provider Page Design .

If you don’t know about the above widget ref and the consumer or the consumer state full widget i recommend you once again to read it from the official documentation or you may also get some of the knowledge from my article on provider state management technique .

In the consumer state full widget you have to call the provider that we have made in the separate file and assign their value to the variable which is used as the text .

    String personName = ref.read(nameProvider);
String personCNICNumber = ref.read(cnicNumberProvider);

ref . read is used to only read the value from the given provider .

T read<T>(ProviderListenable<T> provider)

Reads a provider without listening to it.

According to the official documentation .

Avoid calling [read] inside build if the value is used only for events :

Widget build(BuildContext context) {
// counter is used only for the onPressed of RaisedButton
final counter = ref.read(counterProvider);

return RaisedButton(
onPressed: () => counter.increment(),
);
}

Consider calling [read] inside event handlers :

Widget build(BuildContext context) {
return RaisedButton(
onPressed: () {
// as performant as the previous solution, but resilient to refactoring
ref.read(counterProvider).increment(),
},
);
}

Avoid using [read] for creating widgets with a value that never changes :

Widget build(BuildContext context) {
// using read because we only use a value that never changes.
final model = ref.read(modelProvider);

return Text('${model.valueThatNeverChanges}');
}

Consider using [Provider] or select for filtering unwanted rebuilds :

Widget build(BuildContext context) {
// Using select to listen only to the value that used
final valueThatNeverChanges = ref.watch(modelProvider.select((model) {
return model.valueThatNeverChanges;
}));
return Text('$valueThatNeverChanges');
}

So use the read method when you want’s to only listen to the value who doesn’t change and use the watch method when it may be changed in the future like the counter value that can be updated each time o the click event of the button .

Watch :

Returns the value exposed by a provider and rebuild the widget when that value changes.

T watch<T>(ProviderListenable<T> provider)

Now you have the value of the provider’s that you have created above . As you know that we have declare these provider’s globally now you are able to access these provider’s any when you want’s either inside or outside the state .

The complete code of the consumer state full widget is given below .

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_practice/initial_riverpod_simple_provider/providers.dart';

class SimpleProviderPageDesign extends ConsumerStatefulWidget {
const SimpleProviderPageDesign({super.key, required this.title});

final String title;

@override
ConsumerState<SimpleProviderPageDesign> createState() => _MyHomePageState();
}

class _MyHomePageState extends ConsumerState<SimpleProviderPageDesign> {



@override
Widget build(BuildContext context) {
// In the consumer stateful widget the ref comes as the property inside the build method of the consumer state .

// According to the official documentation .
// ref is used to reads a provider without listening to it.
String personName = ref.read(nameProvider);
String personCNICNumber = ref.read(cnicNumberProvider);

return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Name : $personName"),
Text("CINIC Number : $personCNICNumber"),

],
),
),

);
}
}

The result of the above code is given below .

That’s all for the simple provider .

For more information about the simple provider Kindly read it from the official docuentation .

State Provider :

According to the official documentation .

A provider that exposes a value that can be modified from outside.

StateProvider is a provider that exposes a way to modify its state. It is a simplification of NotifierProvider, designed to avoid having to write a Notifier class for very simple use-cases.

StateProvider<int> StateProvider(
int Function(StateProviderRef<int>) _createFn, {
String? name,
Iterable<ProviderOrFamily>? dependencies,
Family<Object?>? from,
Object? argument,
String Function()? debugGetCreateSourceHash,
})

Let’s deep dive into the state provider using the example of the counter app .

final counterProvider = StateProvider<int>((ref) {
// Provide it's initial value here .
return 0;
});

Add the template type of the state provider and return the initial value in it like counter start from the zero so we give the initial value of the counter to 0 and that’s why we return zero .

If your value’s need’s to be changed in the future like counter whose values will be change then use the state provider .

For displaying the data of the counter i will make a consumer state full widget of the name State Provider Counter Page Design .

Using the ref . watch method i will fetch the value of the state provider every time when it changes .

    int counterValue = ref.watch(counterProvider);

Returns the value exposed by a provider and rebuild the widget when that value changes .

Now we have to use the above value as the text inside the page .

Now i created the two button’s on which the i update the value of the counter .

  onPressed: () {
ref.read(counterProvider.notifier).state++;
},

We mostly don’t used the state provider because it will expose the value outside the class anywhere you want’s .

The total code of the State Provider Counter Page Design is given below .

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_practice/counter_page_riverpod/providers.dart';

class StateProviderCounterPageDesign extends ConsumerStatefulWidget {
const StateProviderCounterPageDesign({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _CounterPageState();
}

class _CounterPageState extends ConsumerState<StateProviderCounterPageDesign> {
@override
Widget build(BuildContext context) {
// Returns the value exposed by a provider and rebuild the widget when that value changes.
int counterValue = ref.watch(counterProvider);

return Scaffold(
appBar: AppBar(
title: const Text("Counter Page Riverpod Design"),
centerTitle: true,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [Text(counterValue.toString(),
style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
)],
),
),
floatingActionButton: ButtonBar(
children: [
FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
// We mostly don't used the state provider because it will expose the value outside the class anywhere you want's .
ref.read(counterProvider.notifier).state++;
},
),
const SizedBox(
width: 50,
),
FloatingActionButton(
child: const Icon(Icons.remove),
onPressed: () {
// We mostly don't used the state provider because it will expose the value outside the class anywhere you want's .
ref.read(counterProvider.notifier).state--;
},
),
],
));
}
}

I only again recommend you to not to the state notifier provider because they will expose the state inside the ui or outside the class or anywhere you want’s .

That’s all for the state notifier provider . If you want’s more information about the state provider kindly read it from the official documentation .

Future Provider :

Future Provider is the equivalent of Provider but for asynchronous code.

FutureProvider<List<AlbumModel>> FutureProvider(
FutureOr<List<AlbumModel>> Function(FutureProviderRef<List<AlbumModel>>) _createFn, {
String? name,
Iterable<ProviderOrFamily>? dependencies,
Family<Object?>? from,
Object? argument,
String Function()? debugGetCreateSourceHash,
})

FutureProvider is typically used for:

  • performing and caching asynchronous operations (such as network requests)
  • nicely handling error/loading states of asynchronous operations
  • combining multiple asynchronous values into another value

FutureProvider does not offer a way of directly modifying the computation after a user interaction. It is designed to solve simple use-cases.

Let’s deep dive into the future provider using the example of the album api’s .

If you want a detailed information about the api’s kindly read it from my article on api’s .

We will work on the following api’s .

https://jsonplaceholder.typicode.com/albums

Now we should start to work first on the api .

You must have to add the dependencies of the below package to interact with the api’s of the json place holder .

Import the above package in your file and make it’s allias of the name http .

import 'package:http/http.dart' as http;

Now first of all we have to make a model class .


class AlbumModel {
int userId;
int id;
String title;
AlbumModel({
required this.userId,
required this.id,
required this.title,
});

AlbumModel copyWith({
int? userId,
int? id,
String? title,
}) {
return AlbumModel(
userId: userId ?? this.userId,
id: id ?? this.id,
title: title ?? this.title,
);
}

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

factory AlbumModel.fromMap(Map<String, dynamic> map) {
return AlbumModel(
userId: map['userId'] as int,
id: map['id'] as int,
title: map['title'] as String,
);
}

String toJson() => json.encode(toMap());

factory AlbumModel.fromJson(String source) =>
AlbumModel.fromMap(json.decode(source) as Map<String, dynamic>);

@override
String toString() => 'AlbumModel(userId: $userId, id: $id, title: $title)';

@override
bool operator ==(covariant AlbumModel other) {
if (identical(this, other)) return true;

return other.userId == userId && other.id == id && other.title == title;
}

@override
int get hashCode => userId.hashCode ^ id.hashCode ^ title.hashCode;
}

Now it’s time to make the extension method of the http response .

extension ResponseCode on http.Response {
bool get isSuccess => statusCode == 200;
bool get isInsertSuccess => statusCode == 201;
bool get isNotFound => statusCode == 404;
}

Now we have to make a abstract class of the name api service and add the crud operation and the url inside it .


abstract class APIService {
String get baseURL => 'https://jsonplaceholder.typicode.com';
String get apiURL;
String get url => baseURL + apiURL;

dynamic fetch({String endPoint = '', Map<String, String>? headers}) async {
var response = await http.get(Uri.parse(url + endPoint), headers: headers);
if (response.isSuccess) {
return jsonDecode(response.body);
}
return null;
}

Future<bool> insert(Map<String, dynamic> map,
{Map<String, String>? headers}) async {
var response = await http.post(Uri.parse(url),
body: jsonEncode(map), headers: headers);
return response.isSuccess;
}

Future<bool> update(Map<String, dynamic> map,
{Map<String, String>? headers}) async {
var response =
await http.put(Uri.parse(url), body: jsonEncode(map), headers: headers);
return response.isSuccess;
}

Future<bool> delete(
{required String endPoint, Map<String, String>? headers}) async {
var response =
await http.delete(Uri.parse(url + endPoint), headers: headers);
return response.isSuccess;
}
}

The above api service is the general api service class it will work’s on all of the api’s you only need to change the url or the crud method for the specialized like we done below in the case of album api service class .


class AlbumService extends APIService {
static AlbumService? _albumService;
AlbumService._internal();
factory AlbumService() {
return _albumService ??= AlbumService._internal();
}

Future<List<AlbumModel>> fetchAlbums() async {
List albumList = await fetch();
return albumList.map((map) => AlbumModel.fromMap(map)).toList();
}

Future<AlbumModel> fetchAlbum(int albumId) async {
var map = await fetch(endPoint: '/$albumId');
return AlbumModel.fromMap(map);
}

Future<bool> insertAlbum(AlbumModel albumModel) async {
return await insert(albumModel.toMap());
}

@override
String get apiURL => '/albums';
}

Now that’s all for the interaction with the api’s now we only need to use them .

No it’s time to make the future provider of the template type of List<Model>

final AlbumAPIServiceProvider = Provider<AlbumService>((ref) {
return AlbumService();
});

FutureProvider<List<AlbumModel>> fetchAlbumsProvider =
FutureProvider<List<AlbumModel>>((ref) async {
return ref.watch(AlbumAPIServiceProvider).fetchAlbums();
});

First you have to use the simple provider of the template type of the album service and return the album service it work’s here as the inherited widget to access the album api service provider anywhere we want’s down the tree

Now i use the state full widget and the and consumer widget to display the change that build every time on the change . You may either use the consumer state full widget here .

 var asyncValue = ref.watch(fetchAlbumsProvider);

ref . watch is used to returns the value exposed by a provider and rebuild the widget when that value changes.

If you need to change one thing in the whole page then you may use the consumer widget other wise you must use the consumer state full widget .

Now i use the async value . when and perform the operation based on the api’s .

 return asyncValue.when(
data: (albums) => ListView.builder(
itemBuilder: (context, index) => ListTile(
leading: CircleAvatar(
child: Text(albums[index].id.toString()),
),
title: Text(albums[index].title),
)),
error: (object, stackTrace) => const Text('Some thing went wrong'),
loading: () => const CircularProgressIndicator());

According to the official documentation .

A utility for safely manipulating asynchronous data.

By using AsyncValue, you are guaranteed that you cannot forget to handle the loading/error state of an asynchronous operation.

On the click event of the floating action button i will fetch the api’s of the album’s .

 onPressed: () {
ref.read(
fetchAlbumsProvider,
);
},

That’s all for the future provider the complete code of the future provider album api design page is given below .


class FutureProviderAlbumApiPageDesign extends StatefulWidget {
const FutureProviderAlbumApiPageDesign({super.key});

@override
State<FutureProviderAlbumApiPageDesign> createState() => _FutureProviderAlbumApiPageDesignState();
}

class _FutureProviderAlbumApiPageDesignState extends State<FutureProviderAlbumApiPageDesign> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Albums'),
actions: [
IconButton(
onPressed: () {
Consumer(
builder: (context, ref, child) {
ref.invalidate(fetchAlbumsProvider);
return const SizedBox.shrink();
},
);
},
icon: const Icon(Icons.refresh))
],
),
body: Consumer(builder: (context, ref, child) {
var asyncValue = ref.watch(fetchAlbumsProvider);
return asyncValue.when(
data: (albums) => ListView.builder(
itemBuilder: (context, index) => ListTile(
leading: CircleAvatar(
child: Text(albums[index].id.toString()),
),
title: Text(albums[index].title),
)),
error: (object, stackTrace) => const Text('Some thing went wrong'),
loading: () => const CircularProgressIndicator());
}),
floatingActionButton: Consumer(
builder: (context, ref, child) => FloatingActionButton(
onPressed: () {
ref.read(
fetchAlbumsProvider,
);
},
child: const Text('Click To Load'),
),
),
);
}
}

That’s all for the future provider . This is also not the recommended because this will automatically rebuild the UI when the Future completes.

As you can see, listening to a FutureProvider inside a widget returns an AsyncValue – which allows only handling the error/loading states.

Stream Provider :

Stream is the continuous flow of the data . In simple word’s stream is the data that comes again and again in the future .

A source of asynchronous data events.

A Stream provides a way to receive a sequence of events. Each event is either a data event, also called an element of the stream, or an error event, which is a notification that something has failed. When a stream has emitted all its events, a single “done” event notifies the listener that the end has been reached .

StreamProvider is similar to FutureProvider but for Streams instead of Futures.

StreamProvider is usually used for:

  • listening to Firebase or web-sockets
  • rebuilding another provider every few seconds

You produce a stream by calling an async* function, which then returns a stream. Consuming that stream will lead the function to emit events until it ends, and the stream closes. You consume a stream either using an await for loop, which is available inside an async or async* function, or by forwarding its events directly using yield* inside an async* function .

Now we will make a stream that will generate the number each time and start’s from the given number .

First of all we will have to make a generative stream that will generate the number’s after every second .

For that purpose i make a function of the name generate inside the number class named as number generator which will generate the number .

class NumberGenerator {
Stream<int> generate({int startValue=0})async*{
for (var i = startValue; i < 5000; i++) {
await Future.delayed(const Duration(seconds: 1));
yield i;
}
}
}

The return data type of the above function is int which mean’s that the it will return the stream of number’s of the int data type in which we have to wait for the one minutes and using the loop it will increment in the value every time and using the yield method it will automatically return the value but do not exit from the function .

final numberGeneratorProvider = Provider<NumberGenerator>((ref) {
return NumberGenerator();
});
final numberStreamProvider = StreamProvider.family<int,int>((ref,value) => ref.watch(numberGeneratorProvider).generate(startValue: value));

Now i make simple provider of the for the number generator which will return the instance of the number generator and used as the inherited widget .

Now we have to make the stream provider in which we have to call generate method of the number generator class using the number generator provider .

class StreamProvider<T> extends _StreamProviderBase<T> with AlwaysAliveProviderBase<AsyncValue<T>>, AlwaysAliveAsyncSelector<T>

Creates a stream and exposes its latest event.

We have to give the two template type’s of the stream provider in which have to add the input and the return data type of the stream provider .

That’s all for the stream provider . Now we have to consume it inside the consumer state full widget .

    var asyncValue = ref.watch(numberStreamProvider(200));

Use the ref . watch method to returns the value exposed by a provider and rebuild the widget when that value changes and provider the number stream provider that we made above in it and give the initial value inside it .

The complete code of the Number Page / Screen is given below .

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_practice/stream_provider.dart/providers.dart';

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

@override
ConsumerState<NumberPage> createState() => _NumberPageState();
}

class _NumberPageState extends ConsumerState<NumberPage> {
@override
Widget build(BuildContext context) {
var asyncValue = ref.watch(numberStreamProvider(200));
return Scaffold(
appBar: AppBar(
title: const Text('Number Page'),
actions: [
IconButton(onPressed: (){
ref.invalidate(numberStreamProvider);
}, icon:const Icon(Icons.refresh_outlined))
],
),
body: asyncValue.when(
skipLoadingOnRefresh: false,
skipLoadingOnReload: false,
data: (value) => Center(
child: Text(value.toString()),
),
error: (obj,stackTrace)=>const Center(child: Text('Error'),),
loading:()=>const Center(child: CircularProgressIndicator(),)),
);
}
}

Now it will generate the number of the type int after every one second and display it on the screen as displayed in the below video .

The stream provider is used when you want’s an operation to be performed continuously in the future .

That’s all for the stream provider .

Change Notifier Provider :

Creates a ChangeNotifier and exposes its current state .

Combined with ChangeNotifier, ChangeNotifierProvider can be used to manipulate advanced states, that would otherwise be difficult to represent with simpler providers such as Provider or FutureProvider .

We use the change notifier provider in Provider state management technique and there it was discussed in detailed . So here i am not going to write on it again . So i recommend you to read it from the official documentation or you can also get some information from my article on provider state managemt in whuch i use the change notifier provider .

State Notifier Provider :

StateNotifierProvider is a provider that is used to listen to and expose a StateNotifier (from the package state_notifier, which Riverpod re-exports).

It is typically used for:

  • exposing an immutable state which can change over time after reacting to custom events.
  • centralizing the logic for modifying some state (aka “business logic”) in a single place, improving maintainability over time.

The state notifier is the best and the recommended technique for the river pod for the state management technique .

Now i will do both of the example’s of the counter app and the api with the state notifier provider for detailed understanding of the state notifier provider .

Counter App :

Make a custom notifier that extend’s the state notifier class .


class CounterNotifier extends StateNotifier<int> {

}

An observable class that stores a single immutable [state].

It can be used as a drop-in replacement to ChangeNotifier or other equivalent objects like Bloc.

It particularity is that it tries to be simple, yet promote immutable data.

Must give the template type of the state notifier . In case of counter app it is int data type .

First of make a final variable (for immutability) of the variable whose value will going to be changed in the future

final int count;

[StateNotifier] is designed to be subclassed. We first need to pass an initial value to the super constructor, to define the initial state of our object.

CounterNotifier({this.count = 0}) : super(count);

Now make the constructor of the custom notifier class and gives the initial value in it and pass the variable in the parent using the constructor of super .

Now we have to make all possible event’s that will change the value or the state of the counter . In our case of counter app there are the following two cases .

void increment() {
state = ++state;
}

void decrement() {
state = state - 1;
}

Increment method is used for the increment of one in the state of the counter variable and decrement is method is used to decrement of one in the state of the counter variable respectively .

The complete code of the custom notifier class is given below .

import 'package:flutter_riverpod/flutter_riverpod.dart';

class CounterNotifier extends StateNotifier<int> {
final int count;
CounterNotifier({this.count = 0}) : super(count);

void increment() {
state = ++state;
}

void decrement() {
state = state - 1;
}
}

That’s all for the custom notifier class .

Now it’s time to make the state notifier provider globally .


final CounterStateNotifierProvider =
StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});

Creates a [StateNotifier] and exposes its current state.

In the template type of the state notifier provider you have to add two thing’s one is the custom notifier provider that we created above with the name of counter notifier provider and the return data type of it and it will return the object of the counter notifier provider .

StateNotifierProvider<CounterNotifier, int> StateNotifierProvider(
CounterNotifier Function(StateNotifierProviderRef<CounterNotifier, int>) _createFn, {
String? name,
Iterable<ProviderOrFamily>? dependencies,
Family<Object?>? from,
Object? argument,
String Function()? debugGetCreateSourceHash,
})

This provider is used in combination with package:state_notifier.

Combined with [StateNotifier], [StateNotifierProvider] can be used to manipulate advanced states, that would otherwise be difficult to represent with simpler providers such as [Provider] or [FutureProvider] .

Now you can consume it using the ref . watch and ref . read method respectively .

var counterValue = ref.watch(CounterStateNotifierProvider);

For the increment and the decrement we have to use the read method that will gives us the counter state .

var counterState = ref.read(CounterStateNotifierProvider.notifier);

We can do increment or decrement by

onTap: () {
print("incr");
counterState.increment();
},

calling the increment and the decrement method of the counter notifier class .

The complete code of the counter page is given below .

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_practice/counter_best_practice.dart/counter_provider.dart';

class CounterPageDesign extends StatefulWidget {
const CounterPageDesign({super.key});
@override
State<CounterPageDesign> createState() => _CounterPageDesignState();
}

class _CounterPageDesignState extends State<CounterPageDesign> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Counter Page Riverpod Best Practices"),
centerTitle: true,
),
body: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [CounterText(), IncrementButton(), DecrementButton()],
),
),
);
}
}

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

@override
Widget build(BuildContext context, WidgetRef ref) {
var counterValue = ref.watch(CounterStateNotifierProvider);

print("count value : ${counterValue.toString()}");

return Text(counterValue.toString());
}
}

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

@override
Widget build(BuildContext context, WidgetRef ref) {
var counterState = ref.read(CounterStateNotifierProvider.notifier);

return InkWell(
onTap: () {
print("incr");
counterState.increment();
},
child: Container(
width: 300,
height: 60,
alignment: Alignment.center,
color: Colors.amber,
child: const Text("Increment"),
),
);
}
}

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

@override
Widget build(BuildContext context, WidgetRef ref) {
var counterState = ref.read(CounterStateNotifierProvider.notifier);

return InkWell(
onTap: () {
counterState.decrement();
},
child: Container(
width: 300,
height: 60,
alignment: Alignment.center,
color: Colors.amber,
child: const Text("Decrement"),
),
);
}
}

That’s all for the counter app using the state notifier provider of the riverpod .

Now it’s time to interact with the api’s using the state notifier provider .

First of all we should all of the work for the api’s .

If you want a detailed information about the api’s kindly read it from my article on api’s .

We will work on the following api’s .

https://jsonplaceholder.typicode.com/albums

Now we should start to work first on the api .

You must have to add the dependencies of the below package to interact with the api’s of the json place holder .

Import the above package in your file and make it’s allias of the name http .

import 'package:http/http.dart' as http;

Now first of all we have to make a model class .

class AlbumModel {
int userId;
int id;
String title;
AlbumModel({
required this.userId,
required this.id,
required this.title,
});
AlbumModel copyWith({
int? userId,
int? id,
String? title,
}) {
return AlbumModel(
userId: userId ?? this.userId,
id: id ?? this.id,
title: title ?? this.title,
);
}
Map<String, dynamic> toMap() {
return <String, dynamic>{
'userId': userId,
'id': id,
'title': title,
};
}
factory AlbumModel.fromMap(Map<String, dynamic> map) {
return AlbumModel(
userId: map['userId'] as int,
id: map['id'] as int,
title: map['title'] as String,
);
}
String toJson() => json.encode(toMap());
factory AlbumModel.fromJson(String source) =>
AlbumModel.fromMap(json.decode(source) as Map<String, dynamic>);
@override
String toString() => 'AlbumModel(userId: $userId, id: $id, title: $title)';
@override
bool operator ==(covariant AlbumModel other) {
if (identical(this, other)) return true;
return other.userId == userId && other.id == id && other.title == title;
}
@override
int get hashCode => userId.hashCode ^ id.hashCode ^ title.hashCode;
}

Now it’s time to make the extension method of the http response .

extension ResponseCode on http.Response {
bool get isSuccess => statusCode == 200;
bool get isInsertSuccess => statusCode == 201;
bool get isNotFound => statusCode == 404;
}

Now we have to make a abstract class of the name api service and add the crud operation and the url inside it .

abstract class APIService {
String get baseURL => 'https://jsonplaceholder.typicode.com';
String get apiURL;
String get url => baseURL + apiURL;
dynamic fetch({String endPoint = '', Map<String, String>? headers}) async {
var response = await http.get(Uri.parse(url + endPoint), headers: headers);
if (response.isSuccess) {
return jsonDecode(response.body);
}
return null;
}
Future<bool> insert(Map<String, dynamic> map,
{Map<String, String>? headers}) async {
var response = await http.post(Uri.parse(url),
body: jsonEncode(map), headers: headers);
return response.isSuccess;
}
Future<bool> update(Map<String, dynamic> map,
{Map<String, String>? headers}) async {
var response =
await http.put(Uri.parse(url), body: jsonEncode(map), headers: headers);
return response.isSuccess;
}
Future<bool> delete(
{required String endPoint, Map<String, String>? headers}) async {
var response =
await http.delete(Uri.parse(url + endPoint), headers: headers);
return response.isSuccess;
}
}

The above api service is the general api service class it will work’s on all of the api’s you only need to change the url or the crud method for the specialized like we done below in the case of album api service class .

class AlbumService extends APIService {
static AlbumService? _albumService;
AlbumService._internal();
factory AlbumService() {
return _albumService ??= AlbumService._internal();
}
Future<List<AlbumModel>> fetchAlbums() async {
List albumList = await fetch();
return albumList.map((map) => AlbumModel.fromMap(map)).toList();
}
Future<AlbumModel> fetchAlbum(int albumId) async {
var map = await fetch(endPoint: '/$albumId');
return AlbumModel.fromMap(map);
}
Future<bool> insertAlbum(AlbumModel albumModel) async {
return await insert(albumModel.toMap());
}
@override
String get apiURL => '/albums';
}

Now that’s all for the interaction with the api’s now we only need to use them .

State Class :

Make an abstract class of the state (It will becomes the parent of the all possible child classes) .

Must include the immutable annotation with all of the classes because here we want’s our states immutable .

In our case of the api’s we have only four possible state’s .

i ) Initial State :

The initial state is the state in which no operation will be performed .

In this state we will show a button on which click event we will start fetching the data from the cloud database using the api’s .

ii ) Loading State :

The loading state is the second state in which the we will show the circular progress indicator to represent that the loading start and data is fetching .

iii ) Loaded State :

The loaded state is the third state in which the data is fetched successfully and we have to show the data in our project .

iv ) Error State :

The error state is the either third or the last state when the data is not fetched succesfully either beacuse of the network problem or any other issue .


@immutable
abstract class AlbumState {
const AlbumState();
}

// Make all of the possible states here .
@immutable
class AlbumInitialState extends AlbumState {}

@immutable
class AlbumLoadingState extends AlbumState {}

@immutable
class AlbumLoadedState extends AlbumState {
final List<AlbumModel> albums;
const AlbumLoadedState({required this.albums});
}

@immutable
class AlbumErrorState extends AlbumState {
final String message;
const AlbumErrorState({required this.message});
}

State Notifier :

Now it’s time to make the custom state notifier class named as the album state notifier class that extend the state notifier class and as the template type of the state notifier we have to pass the album state ( parent ) class .

Now inside the constructor of the super we have to pass the initial state of the album .

class ALbumStateNotifier extends StateNotifier<AlbumState> {
ALbumStateNotifier() : super(AlbumInitialState());

}

Now we have to make the function of the name fetch albums and inside it we have to change the state and perform the code for fetching the data from the api’s .

 Future<List<AlbumModel>> fetchAlbums() async {

}

Now we have to make the object of the album service class and create a list of the album’s and assign it empty list .

var albumService = AlbumService();
List<AlbumModel> albums = [];

Now it’s time to change the state from the initial state to the loading state .

state = AlbumLoadingState();

Now it’s time to fetch the album’s or the data from the api’s using the album api service class and assign it to the variable of list that we have created above .

 albums = await albumService.fetchAlbums();

Now we have to change the state from the loading state to the loaded state and pass the above fetched album’s in it’s constructor .

state = AlbumLoadedState(albums: albums);

You must have to write this code inside the try catch block to handle the exception’s and show the error state in case of the error or the exception .

 try {
albums = await albumService.fetchAlbums();
state = AlbumLoadedState(albums: albums);
} catch (e) {
print(e);
state = AlbumErrorState(message: e.toString());
}

Now we have to return’s the fetched album’s from that method that we have created of the fetch albums

return Future.value(albums);

The complete code of the state notifier is given below .

class ALbumStateNotifier extends StateNotifier<AlbumState> {
ALbumStateNotifier() : super(AlbumInitialState());

Future<List<AlbumModel>> fetchAlbums() async {
var albumService = AlbumService();
List<AlbumModel> albums = [];
state = AlbumLoadingState();
try {
albums = await albumService.fetchAlbums();
state = AlbumLoadedState(albums: albums);
} catch (e) {
print(e);
state = AlbumErrorState(message: e.toString());
}
return Future.value(albums);
}
}

That’s all for the state notifier . Now it’s time for making the state notifier provider and it must me made global .

final ALbumStateNotifierProvider =
StateNotifierProvider<ALbumStateNotifier, AlbumState>((ref) {
return ALbumStateNotifier();
});

We have to provide the state notifier provider that we have created above and also the state of the album and it will return the album state notifier provider .

I have created some of the widget that will be displayed on the screen based on the state of the album api’s .

Album Initial Widget :

The album initial widget consist of the button on whom click event we should fetch the data from the database using the api’s and it will be displayed when the state is album initial state .


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

@override
Widget build(BuildContext context, WidgetRef ref) {
return InkWell(
onTap: () {
ref.read(ALbumStateNotifierProvider.notifier).fetchAlbums();
},
child: Container(
width: 300,
height: 60,
color: Colors.amber,
alignment: Alignment.center,
child: const Text("Click To Load Albums"),
),
);
}
}

We use the consumer widget here it is same as the state less widget but it has an extra parameter of widget ref which we can use here to call the method of the fetch album’s to fetch the data that we have created inside the album state notifier class .

Album Loading Widget :

Album loading widget will be displayed on the screen when the state is loading state and it will show the circular progress indicator in the center of the screen .

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

@override
Widget build(BuildContext context) {
return const Center(child: CircularProgressIndicator());
}
}

Album Loaded Widget :

Album loaded widget is the widget that is displayed on the screen when the state is loaded state and it will displayed the list of the album that we have fetched using the api’s .

class AlbumLoadedWidget extends StatelessWidget {
final List<AlbumModel> albums;
const AlbumLoadedWidget({super.key, required this.albums});

@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) => ListTile(
leading: CircleAvatar(
child: Text(albums[index].id.toString()),
),
title: Text(albums[index].title),
trailing: Text(albums[index].userId.toString()),
));
}
}

Album Error Widget :

The error widget will be displayed on the screen when the state is error state and it will show the error message on the screen or it will show the exception .

class AlbumErrorWidget extends StatelessWidget {
final String message;
const AlbumErrorWidget({super.key, required this.message});

@override
Widget build(BuildContext context) {
return Container(
color: Colors.red,
alignment: Alignment.center,
child: const Text(
"message",
style: TextStyle(color: Colors.white, fontSize: 40),
),
);
}
}

Now we have created all of the possible widget and the states . Now should create a page or the state full widget where we would consume them .

For that purpose i create a state full widget of the album main page design and use them according to the given requirement’s .


class AlbumMainPageDesign extends StatefulWidget {
const AlbumMainPageDesign({super.key});

@override
State<AlbumMainPageDesign> createState() => _AlbumMainPageDesignState();
}

class _AlbumMainPageDesignState extends State<AlbumMainPageDesign> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Albums Best Practice Api Riverpod"),
centerTitle: true,
),
body: Center(
child: Consumer(
builder: (context, ref, child) {

},
),
),
);
}
}

In the consumer widget we should check the state and return the appropriate widget regarding to the state .

var albumState = ref.watch(ALbumStateNotifierProvider);

ref . watch is used to fetch the album state from the album state notifier provider .

Now using either the switch or the if else you should check the current state of the album state notifier provider and return the appropriate widget according to them .

  if (albumState is AlbumInitialState) {
return const AlbumInitialWidget();
} else if (albumState is AlbumLoadingState) {
return const AlbumLoadingWidget();
} else if (albumState is AlbumLoadedState) {
return AlbumLoadedWidget(albums: albumState.albums);
} else {
return AlbumErrorWidget(
message: (albumState as AlbumErrorState).message);
}

That’s all for the album page design the complete code for this page is given below .

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_practice/api_best_practice_riverpod/album_state.dart';
import 'package:flutter_riverpod_practice/api_best_practice_riverpod/album_widgets/album_error_widget.dart';
import 'package:flutter_riverpod_practice/api_best_practice_riverpod/album_widgets/album_initial_widget.dart';
import 'package:flutter_riverpod_practice/api_best_practice_riverpod/album_widgets/album_loaded_widget.dart';
import 'package:flutter_riverpod_practice/api_best_practice_riverpod/album_widgets/album_loading_widget.dart';
import 'package:flutter_riverpod_practice/api_best_practice_riverpod/providers.dart';

class AlbumMainPageDesign extends StatefulWidget {
const AlbumMainPageDesign({super.key});

@override
State<AlbumMainPageDesign> createState() => _AlbumMainPageDesignState();
}

class _AlbumMainPageDesignState extends State<AlbumMainPageDesign> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Albums Best Practice Api Riverpod"),
centerTitle: true,
),
body: Center(
child: Consumer(
builder: (context, ref, child) {
var albumState = ref.watch(ALbumStateNotifierProvider);
if (albumState is AlbumInitialState) {
return const AlbumInitialWidget();
} else if (albumState is AlbumLoadingState) {
return const AlbumLoadingWidget();
} else if (albumState is AlbumLoadedState) {
return AlbumLoadedWidget(albums: albumState.albums);
} else {
return AlbumErrorWidget(
message: (albumState as AlbumErrorState).message);
}
},
),
),
);
}
}

That’s all for the api’s using the state notifier provider of the flutter riverpod .

The complete code for the riverpod is given at my github link is given below .

That’s all for the flutter riverpod

I hope you have learned a lot of new things 😊

Thanks for reading 📚

--

--