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:
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.yamlflutter_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
We also need to wrap our app with
ProviderScope
in order to be able to access the provider from our widgetsvoid 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
. Usingwatch
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
Let's take a look at the different types of providers and how to use them.
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); } }
- 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 - 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 - Because we extend
ConsumerWidget
our build method would require aScopedReader
object. This is what allows us to listen to a provider usingwatch
- 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') ], ); } }
- Similar to a normal
Provider
we create a global stringStateProvider
that has an empty string as its default value - We create a boolean
Provider
that uses theref
object to watch theselectedButtonProvider
and return whether it's red or not. This refreshes the UI whenever our state provider value changes because it is dependant on it. - We declare selectedButton that watches the
selectedButtonProvider
's state - We declare a boolean that watches the basic
isRedProvider
- 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'sonPressed
(or similar) calls. - 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(); } }
- We create a constructor that takes in a count or defaults to 0 if no value was provided
- 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('+'), ) ], ); } }
- We declare a
ChangeNotifierProvider
of typeCounter
and return a newCounter
object on creation. - We watch (listen) to the count in our counterProvider
- When pressing the button we use the appropriate way to access a provider within a button
onPressed
call and increment. Which will trigger thenotifyListeners
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(); } }
- We create a simple todo class with an id, title and a completed flag
- We create a class that extends
StateNotifier
and is of typeList<Todo>
. This will define the state type of this TodoList object. - 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 extendingStateNotifier
- 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 likestate[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'), ), ], ); } }
- We declare a
StateNotifierProvider
that returns a newTodoList
object on creation - When we use a
StateNotifierProvider
we watchtodoListProvider.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. - 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 theTodoList
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'), ); } }
- 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 theonDispose
call onref
. An example of when this might be needed is by closing a connection to firebase to avoid unnecessary calls. - We use the
onDispose
to close the stream. - We listen to values that are emitted from
loadingProcessor.stream
in a for loop and then perform the actions needed. - We then yield the value that the stream provides emits. (yield is like return in a stream async function)
- We listen to the provider the
loadingProvider
that returns anAsyncValue
. - 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. - Data is provided when everything succeeds and we have no errors
- Loading is returned when the provider does not have any initial data and is waiting for the stream to yield something
- 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 endpointimport '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.
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'), ); } }
- 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. - 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