Riverpod as an StateManagement: A Complete Guide

Definition of State

  1. The State is the information that can be read synchronously when the widget is built and might be change during the lifetime of widget.
  2. It defined the property of the widget
  3. State are mutable.
  4. The state is a behavior of the App at a given moment.
  5. As an example, think about an application login state, after the user logs in to the application, the application content change as to the logged user rather than a guest/new user. In the same way at every time like, when we click a button, change a text, change a list etc, we have to change the app state.
  6. An app is not a static it is a dynamic and interactive to each other by the user to get some information from it.
  7. Each application has its state that hold some data.
  8. State is the current data or current UI or current state of the application
  9. Whenever user interact with the application it state will be change.

Also read: Flutter Bloc: A Complete Guide

Example: we have a login application and login screen is presented to the user. So this is the current state of the application so that there is a login screen. Once the user enter the username and password and tap the sign in button what we are supposed to do that we want verify the username and password that is provided by the user. And login to the user mean they are authenticated to the application and move to the different page of the screen.

Our application state change from one state where user is in the login page to the next stage where the state when the user is already login. So this is the state of the application

Why do we need State Management?

An application are generally huge and they have lot of state. For example we have a login screen At first it show the login screen when the user enter the data and pressed to the login button it show the some processing so that it show processing state. Sometime when the password is incorrect it show the error state. If the authentication is correct they are move to the next screen which is another state. There is lot of state are lot of data are being shared in the application.

When your application grows from one page to multiple pages we need some kind of solution to manage the state because our state or data of the application will be somewhere in the one place so that we need to share it to the overall application in multiple pages multiple places so that we need some kind of state management technique or architecture in order to handle the proper data changes. Whenever use change one data in one screen and they move to the another screen if they are consuming the same data in the next screen

Also read: Flutter State Management: GetX As An StateManagement

Limitations of these State Management packages

Most of the state management solutions (like BLoC and Redux) require a lot of boilerplate code to start using them. This boilerplate might be good for apps with huge codebases, but in most apps, it’s not that necessary and might lead to some confusion (if you don’t have much knowledge about the architecture).

Though Provider is well known and widely used by the community, it has some limitations as well. Some of them are:

  1. The code becomes nested, and widgets are used to provide dependencies, which should be avoided (ideally, the UI code should be decoupled from the logic).
  2. In a nested provider situation, we cannot get an object of the same type that is far away. (Only an object that is closer to the call site can be fetched.)
  3. Causes runtime failure. Calling a provider with a different type that isn’t present in the code doesn’t give any compile-time error but instead results in a crash during runtime.
  4. Depends on Flutter. It requires access to BuildContext for listening to providers.

In order to solve these issues, Riverpod was introduced (by the same person who created Provider, Rémi Rousselet). Riverpod not only improves the dependency injection technique, but it also comes tightly integrated with StateNotifier, which is a state management class.

There are three variants of Riverpod packages available:

  1. riverpod: doesn’t depend on Flutter
  2. flutter_riverpod: the basic way to use Riverpod with Flutter
  3. hooks_riverpod: a way to use flutter hooks with Riverpod

According to the official documentation:

Riverpod Logo

Riverpod is a complete rewrite of the Provider package to make improvements that would be otherwise impossible.

Riverpod lets you manage your application state in a compile-safe way, while sharing many of the advantages of Provider. It also brings many additional benefits, making it a great solution for Flutter state management.

It has main three main advantage over the provider

  1. It does not depend on flutter and its build context which means we can listen to the provider and our state without the need of using build context.
  2. It ensures compile safety and with It, It supports multiple provider instances of the same time
  3. It provides a new feature which improves the performance of our application

The starter code that we will be using is as follows:


import 'package:flutter/material.dart';
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Riverpod Tutorial',
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Riverpod as an StateManagement'),
        centerTitle: true,
      ),
      body: Center(
        child: Text('Hello World'),
      ),
    );
  }
}

The output of the above code is given below:

We need the Flutter_riverpod package to start with our first implementation. Add the package to your pubspec.yaml file:

dependencies:
  flutter_riverpod:^0.12.4

Let’s start with a very simple provider, which provides only a String.First of all, you have to wrap the MyApp widget with ProviderScope:

import 'package:flutter/material.dart';

import 'Riverpod/riverpod.dart';

void main() {
  runApp( ProviderScope(
    child: ProviderScope(
    child: MyApp())));
}

The ProviderScope consists of ProviderContainer, which is responsible for storing the state of individual providers. Next, we define the provider:

final provider = Provider<String>((ref) => 'Welcome to Riverpod');

I want you to analyze the syntax a bit.

  1. We declare them as a final variable.
  2. The Provider is created with the class Provider, which contains a callback function by which we return the class. Note: this function will serve us much later.
  3. The callback returns a variable of type ProviderReference, which we call ref, for our use in creating the Provider.

The ref variable is what would be a BuildContext for Providers in Riverpod. With it we can access other Providers in the tree and the good thing is that we can access it in the creation of any Provider.

We also have to define it globally. The first question that should come to your mind: Aren’t globals bad? Yes, they are. 😉

In this case, only the provider definition is global, but it can be scoped locally where it would be used. This is usually within a widget, so the value gets disposed of as soon as the widget is disposed . The Riverpod’s provider is Flutter independent. Now, you can use the provider within your app in two ways. The simplest way is by changing the StatelessWidget to a ConsumerWidget:



import 'package:flutter/material.dart';
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Riverpod Tutorial',
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends ConsumerWidget{
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context,ScopedReader watch) {
   final initialprovider = watch(provider)
    return Scaffold(
      appBar: AppBar(
        title: Text('Riverpod as an StateManagement'),
        centerTitle: true,
      ),
      body: Center(
        child: Text(initialprovider, style: TextStyle( 
            fontWeight: FontWeight.bold,
            fontSize: 30,
            color: Colors.red,)),
      ),
    );
  }
}


An output to the above program

Different type of provider that are used in the riverpod are given below

  • StateProvider — Exposes a value that can be modified from outside. As we can access the state through the state variable and we can change it. Very similar to ValueNotifier.
  • FutureProvider — Constructs an asynchronous value or AsyncValue and has the operation of an ordinaryFuture. It’s useful to load data from a service whose method to obtain this data is asynchronous, as we saw in the example with a value that comes from an asynchronous call and then saves it in a StateProvider.
  • StreamProvider — Create and expose the last value in a Stream. Ideal for communication in real time with Firebase or some other API whose events can be interpreted in Stream.
  • StateNotifierProvider — Create a StateNotifier and expose its state. The most used for logic components, it allows easy access to state, which is usually a private property that’s modified by public methods.
  • ScopedProvider — Defines a Provider<T> (of any type) that behaves differently somewhere in the project. It’s used in conjunction with the overrides property of ProviderScope and defines with the latter the scope where this ScopedProvider is going to be modified.
  • ProviderFamily — This type of Provider is a modifier, that is, all of the above can use it. In itself, it defines an additional parameter that’s involved in the creation of our Provider, such as a String.

Change Notifer Provider :  It expose the data to the outside world and everyone from the outside can change the state and also access it

Create a new class called CounterNotifier, which extends the ChangeNotifier class. Here, we will declare a variable and an increment() and subtract() method.

class CounterNotifer extends ChangeNotifier {
  int count;
  CounterNotifer({this.count = 0});

  void increment() {
    count++;
    notifyListeners();
  }

  void subtract() {
    count--;
    notifyListeners();
  }
}

The notifyListeners() method is used to notify the listeners when the value gets updated.Define a global variable for ChangeNotifierProvider, which calls the CounterNotifier class:

final counterProvider = ChangeNotifierProvider<CounterNotifer>((ref) => CounterNotifer());

We need to update the Text widget that is displaying the counter value, so we extend it with the Consumer widget, and use the value from the counterProvider:

class ChangesNotifierScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final count = watch(counterProvider).count;
    return Scaffold(
      appBar: buildAppBar(),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          FloatingActionButton(
            heroTag: "btm1",
            child: Icon(Icons.add),
            onPressed: () => context.read(counterProvider).increment(),
          ),
          SizedBox(
            width: 30,
          ),
          FloatingActionButton(
              heroTag: "btn2",
              child: Icon(Icons.remove),
              onPressed: () => context.read(counterProvider).subtract()),
        ],
      ),
      body: Center(
        child: Text(
          '${count.toString()}',
          style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
        ),
      ),
    );
  }

  AppBar buildAppBar() {
    return AppBar(
      title: Text("Change Notifier Provider"),
      centerTitle: true,
      backgroundColor: Colors.teal,
      automaticallyImplyLeading: false,
    );
  }
}
when plus button is pressed
when – button is pressed

State Provider:  These provider are generally modified from outside. the class . In the below example we will discuss about the state provider how we can change the color from one screen to another i.e from green to red color

final colorPickerProvider = StateProvider<String>((ref) => '');
final colorProvider = Provider<String>((ref) {
  final color = ref.read(colorPickerProvider);

  return color.state = ' Red';
});

In the above code we declare the global variable as colorPickerProvider and use stateProvider as an empty String.Later these provider was read by other variable whose name is colorprovider. we generally use read() method to read the other provider.Initially we declare the color as a Red one.

class StateProviderScreen extends ConsumerWidget {
  final String title = "State Provider";
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final colors = watch(colorPickerProvider).state;
    return Scaffold(
      backgroundColor: colors == 'Red' ? Colors.red : Colors.green,
      appBar: AppBar(
        title: Text(title),
        centerTitle: true,
        backgroundColor: Colors.teal,
      ),
      floatingActionButton: FloatingActionButton(
          child: Icon(
            Icons.legend_toggle,
            color: Colors.redAccent,
          ),
          onPressed: () => context.read(colorPickerProvider).state =
              colors == 'Red' ? 'green' : 'Red'),
      body: Container(
        child: Center(
          child: Text(
            "Hello World",
            style: TextStyle(
                color: Colors.white, fontSize: 40, fontWeight: FontWeight.bold),
          ),
        ),
      ),
    );
  }
}
Initial Screen when app open
After clicking the floating action button

FutureProvider: It’s useful to load data from a service whose method to obtain this data is asynchronous . for example

We need to define provider (a FutureProvider) that returns the value 5 as the response after 2 second and stores it in the futureproviderglobal variable:

final futureprovider = FutureProvider<int>((ref) async {
  await Future.delayed(Duration(seconds: 2));
  return 5;
});

Now, we use the Consumer widget inside the UI to watch for the provider

class FutureProviderScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, watch) {
    final number = watch(futureprovider);
    return Scaffold(
        appBar: AppBar(
          title: Text("Future Provider"),
          centerTitle: true,
          backgroundColor: Colors.teal,
        ),
        body: Center(
            child: number.when(
                data: (data) {
                  return Text(
                    data.toString(),
                    style: TextStyle(fontSize: 35, color: Colors.red),
                  );
                },
                loading: () => CircularProgressIndicator(),
                error: (object, stack) => Text("An Error as been occur"))));
  }
}
After 2 second It display a value 5

When we watch a FutureProvider, the return type is an AsyncValue (which is a union). You can use the when() method on the numberto get access to the three states of a Future and show an appropriate widget in each of them.

State Notifier Provider : The state is generally immutable and doesnot change from outside the class. so we can change only the state with in the class

Create a new class called CounterStateNotifier, which extends the ChangeNotifier class. Here, we will declare a variable and an add() and subtract() method.

class CounterStateNotifier extends StateNotifier<int> {
  CounterStateNotifier() : super(0);
  void add() {
    state++;
  }

  void subtract() {
    state--;
  }
}

Define a global variable for StateNotifierProvider, which calls the CounterStateNotifierclass:

final stateProvider = StateNotifierProvider((ref) => CounterStateNotifier());
class StateNotifierScreen extends ConsumerWidget {
  @override
  Widget build(
    BuildContext context, watch
  ) {
    // final count = watch(stateProvider.state);
    final state = watch(stateProvider);
    return Scaffold(
      appBar: buildAppBar(),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          FloatingActionButton(
            heroTag: "btm1",
            child: Icon(Icons.add),
            onPressed: () => context.read(stateProvider.notifier).add(),
          ),
          SizedBox(
            width: 30,
          ),
          FloatingActionButton(
              heroTag: "btn2",
              child: Icon(Icons.remove),
              onPressed: () => context.read(stateProvider.notifier).subtract()),
        ],
      ),
      body: Center(
        child: Text(
          '${state.toString()}',
          style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
        ),
      ),
    );
  }

  AppBar buildAppBar() {
    return AppBar(
      title: Text("State Notifier Provider"),
      centerTitle: true,
      backgroundColor: Colors.teal,
    );
  }
}
+ button when pressed
– button when pressed