Flutter Riverpod State Management Explained-img

Flutter Riverpod State Management Explained

This guide will introduce you to the Riverpod package and how to use its providers to manage a Flutter application's state
In this guide, we will use Riverpod to add state to different Flutter scenarios using the providers that the package has. There are a lot of state management packages out there and this guide will not compare or state that Riverpod is the best. I used to use Provider and then moved to Riverpod. This guide will cover how to use Riverpod with Flutter (without hooks), assuming you understand why it is important to manage state in an application.

Riverpod uses the providers to manage state. If you have never used the original Provider package then this concept might be new to you. You can think of a provider as a place to access shared data. Providers solve many of state management problems that we'll see as we go through the guide.

A Provider can be declared as a global variable without having the downsides of global variables, this is because they are immutable and act as if we declared a function or a class. This allows us to access the data a provider holds, modify it, and react to changes from anywhere within our app.

Here is a list of the providers and the examples we'll look at in this guide:
  • Hello world (Provider)
  • Selected button (State Provider)
  • Counter (Change Notifier)
  • Todo (State Notifier Provider)
  • Loading (Stream Provider)
  • HTTP call (Future Provider)

Before we start

The structure of this project will be a parent app that holds smaller widgets. Each widget will be an example of a provider type. Let's add flutter_riverpod to our pubspec.yaml
flutter_riverpod: ^0.12.1
At the time of writing this is the latest version of riverpod but please go here to get the latest version. Now let's start with the first example that will introduce providers in general and then we take it from there.

We also need to wrap our app with ProviderScope in order to be able to access the provider from our widgets
void main() {
  runApp(ProviderScope(child: MyApp()));
}

Reading Providers

There are multiple ways to listen to providers. In this guide, we will go through either using Consumer or ConsumerWidget.

  • Consumer: is a widget that can be placed anywhere within the widget tree. It has a builder property that exposes a function called watch. Using watch we can listen to and access providers. This is best used if you want to render a certain part of a widget without rerendering all of it.
  • ConsumerWidget: is a base-class for Widgets similar to StatelessWidget. This class also exposes the watch function but a change on a watched provider will cause the entire widget to rebuild. (Might be less efficient if used incorrectly)

If you need a widget that is completely dependant on a provider then extending ConsumerWidget is the way to go. If you have a widget that has a small part that is dependant on a provider then you can either use a Consumer or extract that part into its own widget that extends ConsumerWidget.

Let's take a look at the different types of providers and how to use them.

Provider

A Provider is a place to access and listen to shared data. Let's create a simple hello world example to present how a provider works and how we can read the value of a provider.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 1
final hellowWorldProvider = Provider<String>((ref) => "Hello world");

// 2
class HelloWorld extends ConsumerWidget {
  const HelloWorld({Key key}) : super(key: key);

  @override
  // 3
  Widget build(BuildContext context, ScopedReader watch) {
    // 4
    String text = watch(hellowWorldProvider);
    return Text(text);
  }
}
  1. We declare a global string provider (they are immutable, it's like declaring a function). A provider takes a function that creates the shared state and receives the ref object. This ref object can be used to watch and read other providers within the one being created. We will take a look at this in a bit
  2. Riverpod has ConsumerWidget which is basically a stateless widget that allows us to listen to a provider, so the UI can automatically update the component when needed
  3. Because we extend ConsumerWidget our build method would require a ScopedReader object. This is what allows us to listen to a provider using watch
  4. We declare a string that takes the value of our hellowWorldProvider.

Provider cannot be used to listen to changes and update the UI, because it does not have a state. If a Provider is dependant on another provider that changes then the UI would rebuild when we listen to this type of Provider. We will see that in the next section 

State Provider

State provider and all the following providers in this guide can be listened to and update the UI when their data changes. This type of provider is ideal for simple data types such as flags or filters that a component needs.

This example will have 2 buttons labeled red and blue and will use a string state provider and a boolean basic provider. When a button is clicked the state provider changes to match the selection and that triggers a UI rebuild. The basic provider uses the state provider to generate an is red boolean and that gets refreshed with the UI rebuild.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 1
final selectedButtonProvider = StateProvider<String>((ref) => '');
// 2
final isRedProvider = Provider<bool>((ref) {
  final color = ref.watch(selectedButtonProvider);
  return color.state == 'red';
});

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

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    // 3
    final selectedButton = watch(selectedButtonProvider).state;
    // 4
    final isRed = watch(isRedProvider);

    return Column(
      children: [
        Text(selectedButton),
        RaisedButton(
          // 5
          onPressed: () => context.read(selectedButtonProvider).state = 'red',
          child: Text('Red'),
        ),
        RaisedButton(
          // 5
          onPressed: () => context.read(selectedButtonProvider).state = 'blue',
          child: Text('Blue'),
        ),
        // 6
        isRed ? Text('Color is red') : Text('Color is not red')
      ],
    );
  }
}
  1. Similar to a normal Provider we create a global string StateProvider that has an empty string as its default value
  2. We create a boolean Provider that uses the ref object to watch the selectedButtonProvider and return whether it's red or not. This refreshes the UI whenever our state provider value changes because it is dependant on it.
  3. We declare selectedButton that watches the selectedButtonProvider's state
  4. We declare a boolean that watches the basic isRedProvider
  5. We use context.read(selectedButtonProvider) to access the state of the provider without listening to it and change its value. We do this because we are not supposed to listen inside a button's onPressed (or similar) calls.
  6. We use the value from the basic provider to show the right text.

Change Notifier Provider

If you have used the original provider package then this should be familiar. This provider creates a Change Notifier and listens to it. There are some downsides to using this and I personally prefer using StateNotifier whenever it's possible. The downsides are that ChangeNotifierProvider's data is mutable and can be accessed and overwritten from different places. It is also heavily dependant on notifying listeners as we will see in a second. Forgetting to notify listeners in small examples is hard to show but as a project grows, having a mutable state and manually notifying listeners makes this provider.

This example is a simple counter app to show how this provider works. Let's start by creating our Counter object that extends ChangeNotifier.
import 'package:flutter/material.dart';

class Counter extends ChangeNotifier {
  int count;
  // 1
  Counter([this.count = 0]);

  // 2
  void increment() {
    count++;
    notifyListeners();
  }
}
  1. We create a constructor that takes in a count or defaults to 0 if no value was provided
  2. This increment() method increments the count and notifies the listeners. If we just change the count without notifying then any widget that listens to this provider does not rebuild.

Let's create a provider that uses this class and the corresponding widget. Do not forget to import the class.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// import counter class here

// 1
final counterProvider = ChangeNotifierProvider<Counter>((ref) => new Counter());

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

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    // 2
    final count = watch(counterProvider).count;
    return Column(
      children: [
        Text(count.toString()),
        RaisedButton(
          // 3
          onPressed: () {
            context.read(counterProvider).increment();
          },
          child: Text('+'),
        )
      ],
    );
  }
}
  1. We declare a ChangeNotifierProvider of type Counter and return a new Counter object on creation.
  2. We watch (listen) to the count in our counterProvider
  3. When pressing the button we use the appropriate way to access a provider within a button onPressed call and increment. Which will trigger the notifyListeners and causes the UI to rebuild this widget.

To explain the downsides of this approach better let's add a manual increment to count inside on pressed without using increment
onPressed: () {
  context.read(counterProvider).count = count + 1; // New Line
  context.read(counterProvider).increment();
},
We are able to change the count like this because our Counter object is mutable. This will change the count without notifying our listeners and hence the UI won't rebuild until the next line is hit and displays an increment by 2.

State Notifier Provider

Creates a StateNotifier and exposes its current state. This is my go to alternative to ChangeNotifierProvider. It allows us to manipulate advanced states without exposing a state to the outside like we did with count earlier. We still need to create a class that extends StateNotifier to manage the state.

This example is a simple todo widget. Clicking on add creates a new todo, editing changes the title, and deleting removes it. This widget will not have form fields or data entry. The point is to present the functionality of the StateNotifierProvider
import 'package:flutter_riverpod/all.dart';
// 1
class Todo {
  num id;
  String title;
  bool completed;
  Todo({this.id, this.title, this.completed = false});
}
// 2
class TodoList extends StateNotifier<List<Todo>> {
  // 3
  TodoList([List<Todo> todos]) : super(todos ?? []);

  // 4
  void add(String title) {
    state = [...state, new Todo(id: state.length + 1, title: title)];
  }
  // 4
  void toggle(num id) {
    state = [
      for (final todo in state)
        if (todo.id == id)
          Todo(
            id: todo.id,
            completed: !todo.completed,
            title: todo.title,
          )
        else
          todo,
    ];
  }
  // 4
  void edit(Todo updatedTodo) {
    state = [
      for (final todo in state)
        if (todo.id == updatedTodo.id)
          Todo(
            id: todo.id,
            completed: todo.completed,
            title: updatedTodo.title,
          )
        else
          todo,
    ];
  }
  // 4
  void remove(num id) {
    state = state.where((todo) => todo.id != id).toList();
  }
}
  1. We create a simple todo class with an id, title and a completed flag
  2. We create a class that extends StateNotifier and is of type List<Todo>. This will define the state type of this TodoList object.
  3. We define the constructor that takes in a list of todos as an argument but if it is not provided then we set our state to an empty list. To access the todos in our class we will use state which can be accessed due to us extending StateNotifier
  4. In our functions we assign different values to our state but we always override its value, state = that is because it is immutable. If we change the add function to have something like state[state.length] = new Todo(id: state.length + 1, title: title); This would not trigger a state change and UI does not rebuild.

Now let's present how we use these classes to manipulate data in our widgets.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Import the todo and todolist classes here

// 1
final todoListProvider = StateNotifierProvider((ref) => new TodoList());

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

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    // 2
    final todoList = watch(todoListProvider.state);
    return Column(
      children: [
        Container(
          height: 100,
          child: ListView.builder(
            itemCount: todoList.length,
            itemBuilder: (BuildContext context, int index) {
              Todo todo = todoList[index];
              return ListTile(
                title: Text(todoList[index].title),
                leading: IconButton(
                  icon: Icon(Icons.edit),
                  // 3
                  onPressed: () {
                    todo.title = 'Updated Title';
                    context.read(todoListProvider).edit(todo);
                  },
                ),
                trailing: IconButton(
                  icon: Icon(Icons.delete),
                  // 3
                  onPressed: () =>
                      context.read(todoListProvider).remove(todo.id),
                ),
              );
            },
          ),
        ),
        RaisedButton(
          // 3
          onPressed: () => context.read(todoListProvider).add('New Item'),
          child: Text('Add'),
        ),
      ],
    );
  }
}
  1. We declare a StateNotifierProvider that returns a new TodoList object on creation
  2. When we use a StateNotifierProvider we watch todoListProvider.state. Trying to access the immutable state from the outside will result in a warning. This is to make sure we do not modify the state from anywhere but the class itself.
  3. When triggering actions we use the appropriate way to access a provider within a button onPressed call and perform our state changes that will change the state of the TodoList class and trigger a UI rebuild.

Stream Provider

Stream provider is a provider that is used to listen to a stream and change whenever that stream emits a new value. Stream can be anything from a firebase connection to a socket to a backend. In this example we will create a stream called loadingProcessor that basically just emits a number every second going up by 10 we will then listen to that and update our widget with every new emit. This proivder has very similar functionality to StreamBuilder
import 'dart:async';

class LoadingProcessor {
  LoadingProcessor() {
    Timer.periodic(Duration(seconds: 1), (timer) {
      if (controller.isClosed) {
        timer.cancel();
      } else {
        controller.sink.add(loading);
        loading += 10;
      }
    });
  }
  final controller = StreamController<int>();
  var loading = 0;
  Stream<int> get stream => controller.stream;
}
This guide is already getting long and for the sake of staying on topic. This guide will not explain how to create streams. Let us now create a widget that will listen to this stream.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// import the loading processor here

// 1
final loadingProvider = StreamProvider.autoDispose<int>((ref) async* {
  final loadingProcessor = LoadingProcessor();
  // 2
  ref.onDispose(() => loadingProcessor.controller.sink.close());
  // 3
  await for (final value in loadingProcessor.stream) {
    if (value == 100) {
      loadingProcessor.controller.sink.close();
    }
    // 4
    yield value;
  }
});

class LoadingWidget extends ConsumerWidget {
  LoadingWidget({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    // 5
    AsyncValue<int> loading = watch(loadingProvider);
    // 6
    return loading.when(
      // 7
      data: (percent) {
        return Text("loading is $percent");
      },
      // 8
      loading: () => const CircularProgressIndicator(),
      // 9
      error: (err, stack) => Text('Error: $err'),
    );
  }
}
  1. We create a StreamProvider with an autoDispose modifier. This modifier allows us to destroy the state of a provider when it is no longer used and gives us access to the onDispose call on ref. An example of when this might be needed is by closing a connection to firebase to avoid unnecessary calls.
  2. We use the onDispose to close the stream.
  3. We listen to values that are emitted from loadingProcessor.stream in a for loop and then perform the actions needed.
  4. We then yield the value that the stream provides emits. (yield is like return in a stream async function)
  5. We listen to the provider the loadingProvider that returns an AsyncValue.
  6. By returning loading.when() we allow the UI to automatically rebuild when the data changes or new value is emitted from the stream to the provider.
  7. Data is provided when everything succeeds and we have no errors
  8. Loading is returned when the provider does not have any initial data and is waiting for the stream to yield something
  9. Error is what is returned when an errors occurs

Future Provider

A Future Provider has identical behavior to StreamProvider but instead of creating a Stream the provider creates a Future. It also has very similar functionality to FutureBuilder. In this widget we will create a http request and display the json response in text in order to explain the functionality without going off topic.

Let's create an Address class that will have a function that returns address from an http endpoint
import 'dart:convert';

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

class Address {
  Address();

  Future<String> getAddress() async {
    final resp = await http.get(
        "https://my-json-server.typicode.com/refactord/deep-dive-db/addresses");
    if (resp.statusCode == 200) {
      return jsonDecode(resp.body).toString();
    } else {
      throw Exception('Failed to load album');
    }
  }
}
In order for this to work, we would need to add the HTTP package as a dependency to our app. Let's add this to our pubspec.yaml
 http: ^0.12.2
You can find the latest version of this package here

Similar to last example we will not go through explaining how HTTP calls are made. We only have this to display the functionality of the provider. Let's create our widget now.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// import the address class here

// 1
final addressProvider = FutureProvider<String>((ref) async {
  return Address().getAddress();
});

class HttpRequestWidget extends ConsumerWidget {
  HttpRequestWidget({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    AsyncValue<String> address = watch(addressProvider);
    // 2
    return address.when(
      data: (data) {
        return Text(data.toString());
      },
      loading: () => const CircularProgressIndicator(),
      error: (err, stack) => Text('Error: $err'),
    );
  }
}
  1. We create our address provider FutureProvider. autoDispose is not used here but it can be used to stop any ongoing HTTP calls if a user leaves the page that made the call.
  2. Just like stream provider we listen to the changes on the FutureProvider by calling .when on our async value and return what's appropriate based on the data. 

Conclusion

This long guide gave an intro to riverpod then explained the main concepts, functions, and providers that this package offers. This guide did not cover some other topics we touched so it does not go off-topic. The information in this guide is good enough to allow you to start to add state management to your applications using riverpod.

Like the post?

feel free to share it anywhere

Related Guides

Flutter Riverpod Filters-img

Flutter Riverpod Filters

Link Riverpod providers to create reactive filters