Exploring flutter riverpod for state management and api integration

Pravin Palkonda
8 min readFeb 26, 2023

--

A simple way to access the state from anywhere in the application.

In flutter, we can manage the state of the application by using various packages such as Bloc, GetX, MobX, Provider, etc. Flutter riverpod is also one of them which is most discussed and supported by large communities. It is liked by most developers and the popularity of the riverpod is growing day by day. It overcomes the drawbacks of other packages which can be understood by working on the application by using flutter_riverpod. This package is stable and actively maintained by the team.

Learn riverpod from the documentation which explains about it in detail with lots of examples.

flutter_riverpod

The flutter_riverpod is a state management library that helps us to manage the state of an application by accessing the state from anywhere in the application. We can use multiple types of providers for accessing the data or managing the state. The providers which are declared using flutter riverpod can be accessed globally in the application.

Providers

Providers are the most important part of the riverpod application. We can easily create the providers according to the requirement. Here we need to create the provider so we can manage the state and access the data anywhere in the application.

Types of providers

  1. Provider → It is the most basic of all providers. It is mainly used to create value.

This is how we can create a simple provider

final photosApiProvider = Provider<PhotosApi>((ref) => PhotosApi());

2. StateProvider → StateProvider is a provider which exposes the way to modify its state. We can easily modify the state of any variable from the UI in the application. The state of the StateProvider is mostly for String, enum, boolean, and a number.

// Declaring a state provider 
final counterProvider = StateProvider((ref) => 0);

// This is how we can modify the state
ref.read(counterProvider.notifier).state++

3. FutureProvider → FutureProvider is mostly used for performing any network request. It easily handles the loading and error state of asynchronous operations.

We can easily create FutureProvider for asynchronous operations.

final photoStateFuture = FutureProvider<List<PhotosModel>>((ref) async {
return ref.read(photosApiProvider).getPhotos();
});

Easily listen to FutureProvider from UI. In the UI we can easily manage to build the widget as the future gets completed. Listening to FutureProvider we get an AsyncValue which allows us to handle loaders and errors easily.

Widget build(BuildContext context, WidgetRef ref) {
final photosData = ref.watch(photoStateFuture);
return Scaffold(
appBar: AppBar(
title: const Text("Riverpod api demo "),
),
body: photosData.when(
data: (photosData) {
return ListView.builder(
itemCount: photosData.length,
itemBuilder: (context, index) => Container(
child: ListTile(
title: Text(photosData[index].title),
subtitle: Text(photosData[index].id.toString()),
trailing: CircleAvatar(
radius: 25,
backgroundImage:
NetworkImage(photosData[index].thumbnailUrl),
),
),
),
);
},
error: (err, s) => Text(err.toString()),
loading: () => const Center(child: CircularProgressIndicator())),

4. StreamProvider → StreamProivder is similar to FutureProvider but it is for streams instead of future. It is mostly used to rebuild another provider every few seconds or listen to firebase continuously. It is very helpful when we have to show data that is continuously changing.

5.StateNotifierProvider → StateNotifierProvider is the provider which is used to change or listen to state. StateNotifierProvider along with StateNotifier is Riverpod’s recommended solution for managing state which may change in reaction to user interaction.

6. ChangeNotifierProvider → ChangeNotifierProvider is a provider which returns a subclass of ChangeNotifier. It is mostly used to listen the change in the state or any data. As per Riverpod’s documentation, ChangeNotifierProvider is not recommended for scalable applications because of the issues with mutable states.

Advantages of creating providers and using it.

  1. We can easily access the state anywhere in the application.
  2. The performance of an application is optimized. The build method of the widget doesn't get called again and again even if there is a small change in the application. Also, we completely ignore the statefull widgets.
  3. Easy integration for advanced features like pull to refresh and logging.
  4. We can easily dispose the state of the provider if it is no longer needed.

How to read provider that have been created?

All providers that have been created have a ref as a parameter. We can easily listen to any provider by using this ref parameter.

// Creating a basic provider using ref as a parameter for class PhotosApi
final photosApiProvider = Provider<PhotosApi>((ref) => PhotosApi());

// Read the provider using ref through which we can access
// the methods defined in it.
ref.read(photosApiProvider).getPhotos()

Also, we can read the providers using ConsumerWidget. It is the same as StatelessWidget but with the parameters BuildContext and WidgetRef. WidgetRef is an object which allows widgets to interact with providers.

// Consumer widget 
class HomePage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// Listen to the providers using ref
// We can listen to multiple providers using ref
final photosData = ref.watch(photoStateFuture);
return Scaffold(
body: photosData.when(
data: (photosData) {
return ListView.builder(
itemCount: photosData.length,
itemBuilder: (context, index) => Container(
child: ListTile(
title: Text(photosData[index].title),
subtitle: Text(photosData[index].id.toString()),
trailing: CircleAvatar(
radius: 25,
backgroundImage:
NetworkImage(photosData[index].thumbnailUrl),
),
),
),
);
},
error: (err, s) => Text(err.toString()),
loading: () => const Center(child: CircularProgressIndicator())),
);
}
}

Now we know how to create and use providers.

Let's create a demo application where we fetch the list of data from api and display it on the screen. If the user clicks on any list that will be added to the favorites list and that data will be displayed on the favorites page.

Packages we need to install.

  1. http → This package is required for making an http request for fetching the data from the given url.
  2. flutter_riverpod → To manage the state of the application.

Install these packages from pub.dev and install them in the pubspec.yaml file.

pubspec.yaml file

Enabled riverpod for the entire application.

To use the feature provided by the Riverpod we must first enable it. In the main.file we need to wrap the MyApp widget with the ProviderScope widget. It is a widget that stores the state of the providers. All Flutter applications using Riverpod must contain an [ProviderScope] at the root of their widget tree.

Folder structure

We will create separate folders for each file.

  • api → This folder contains the dart file for making an api request.
  • constants → App-related constants go in this folder.
  • models → Here we will create files for parsing the JSON data to models.
  • pages → This folder contains all the pages.
  • state → Here we will create files to maintain the state and create providers according to the need.

Create a separate dart file in the api directory and name it as photos_api.dart.

In this file, we will fetch the data from the photos api url by making a get http request. From this api, it will return the list of photos model.

photos_api.dart file

Create a dart file in the constants directory and name it constants.dart file.

In this file, we will create all the constants we required in the application.

constants.dart file

Now it will be easy to get the photosUrl by simply calling Constants.photosUrl.

Now we will create a model file in the model's directory.

By creating a model we can easily parse the JSON data to the model during api call.

quicktype.io is the best tool to generate models for the response data easily and fastly.

Copy the response getting from the photos api URL and paste it into quicktype.io. It will generate all the JSON to dart code according to the response data.

photos_model.dart file

Now we need to create a providers to access the data or manage the state

Let's create a dart file in a state directory and name it as providers.dart file. In this file, we will create a basic provider for PhotosApi class and call the api method by using FutureProvider.

Now it's time to display the data coming from the photos api.

Create a separate dart file in the pages directory and name it home_page.dart.

Here we will display the data in a list by using ConsumerWidget instead of StatelessWidget. The consumer widget has an additional parameter (WidgetRef ) in the build method. Through this ref parameter, we can listen to any provider we have created as providers are global. Using this ref parameter we can also listen to multiple providers.

As we are getting an AsynValue from the FutureProvider, we can easily use the .when property to render the widget based on the data we get. It has a property

  • data → Where the actual data is displayed.
  • error → To display the error widget if there is any error.
  • loading → To display the loader widget while fetching the api data.
class HomePage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// Listening to provider using ref parameter provided by the consumer widget
final photosData = ref.watch(photoStateFuture);
return Scaffold(
appBar: AppBar(
title: const Text("Riverpod api demo "),
),
body: photosData.when(
// Here the actual api data is displayed
data: (photosData) {
return ListView.builder(
itemCount: photosData.length,
itemBuilder: (context, index) => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border:
Border.all(color: Colors.black, width: 0.2)),
margin: const EdgeInsets.all(4),
child: ListTile(
title: Text(photosData[index].title),
subtitle: Text(photosData[index].id.toString()),
trailing: CircleAvatar(
radius: 25,
backgroundImage:
NetworkImage(photosData[index].thumbnailUrl),
),
),
),
);
},
// Error is displayed if any
error: (err, s) => Text(err.toString()),
// Loader in displayed for initial api call
loading: () => const Center(child: CircularProgressIndicator())),
);
}
}
screenshot of the home page

Here we conclude that we can create a provider easily and access them in the application from anywhere using ref property as providers have global scope. We can easily display the api data using ConsumerWidget.

I have also added the feature for adding the data to the favorites list. Full code is available on git.

Let me know in the comments if I need to correct anything. I will try to improve it.

Clap if you like the article. 👏

Also, go through my article to learn state management using MobX.

--

--