
Riverpod is the response to all the insufficiencies of other dependency injection and state management packages for Dart & Flutter apps. It's quick to get familiar with, maintainable, testable and it's much less error-prone than the other solutions out there. Let's take a look at the core principles of Riverpod.
How is Riverpod any different?
There are multiple ways to provide dependencies around your Flutter app. Service locators, such as get_it, are great but they aren't directional, for lack of a better word. This means you can access objects in whatever order you want and you're not necessarily guaranteed that all of them are properly initialized unless you're careful. This can result in major headaches especially on large projects with huge amounts of dependencies.

Another solution is to use the Provider package which is a well-known and widely accepted package for providing objects around your Flutter apps. In this tutorial I assume that you've had a chance to use it before. It's great, it's directional but it also has many drawbacks.
Provider is not perfect

For one, Provider is dependent on Flutter since you're using widgets to provide objects down the widget tree. I've never been very comfortable with mixing the UI code with dependency injection. Provider's usage of widgets also goes hand in hand with all the nesting it creates. Imagine we have MySecondClass
that depends on MyFirstClass
.
main.dart
class MySecondClass {
final MyFirstClass myFirstClass;
MySecondClass(this.myFirstClass);
}
Although I'm not going to explain the Provider package in this tutorial, take a look at the horribly nested UI code needed to properly construct an instance of MySecondClass
.
main.dart
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider(
create: (context) => MyFirstClass(),
child: ProxyProvider<MyFirstClass, MySecondClass>(
update: (context, firstClass, previous) => MySecondClass(firstClass),
child: MyVisibleWidget(),
),
);
}
}
Secondly, Provider relies only on types to resolve the requested object. If you provide two objects of the same type, you can get only the one closer to the call site.
main.dart
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider(
create: (context) => 'A String far away.',
child: Provider(
create: (context) => 'A String that is close.',
builder: (context, child) {
// Displays 'A String that is close.'
// There's no way to obtain 'A String far away.'
return Text(Provider.of<String>(context));
},
),
);
}
}
Lastly, if you try to access a type which is not provided at all, you're going to get an error only at run-time. This is not ideal since we should always strive to catch as many errors as possible at compile-time.
The basics of Riverpod

Riverpod combines the best things from service locators and Provider which results in a package that is just pleasurable to use . It's compile-time safe, doesn't depend on Flutter, it's still directional and the boilerplate is minimal. Sounds too good to be true? Well, this old adage just doesn't apply in some cases and Riverpod is thankfully one of them.
The first step is to add the package as a dependency to your Flutter project. In this tutorial, we're going to use flutter_riverpod which is the way to go if you want to use pure Flutter.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^0.12.1
If you're using flutter_hooks in your project, you will probably want to also use hooks_riverpod. Lastly, there's the pure Dart riverpod package in case you're developing for something else than the Flutter framework.
Riverpod's Providers aren't placed into the widget tree. They're instead put into global variables which can be inside any file you want. As long they're accessible, you're fine.
main.dart
final greetingProvider = Provider((ref) => 'Hello Riverpod!');
This simplest Provider
can expose a read-only value. There are many more types of Providers for working with Futures, Streams, ChangeNotifiers, StateNotifiers and more.
ref
parameter is of type ProviderReference
. As you'll see later on, it's mostly used to resolve dependencies between providers.While the Provider
object is globally accessible, this does not mean that the provided object (in this case a string "Hello Riverpod!") is itself global. Much like with a global function, you can call it from anywhere but the returned value may also be scoped locally. Consider the following code:
function_analogy.dart
String globalFunction() {
return 'some value';
}
class MyClass {
void _classMethod() {
final valueLocalToThisMethod = globalFunction();
}
}
Where are the widgets!?
Although it's great news that Riverpod's Providers are Flutter-independent, we still need to use the value provided by a Provider
object from the widget tree - this is Flutter, after all.
The Riverpod package comes with just a single InheritedWidget
that needs to be placed above the whole widget tree called ProviderScope
. It's responsible for holding something called a ProviderContainer
which is in turn responsible for storing the state of individual Provider
objects.
main.dart
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
ProviderScope
wrapping the whole widget tree is enough. You can have multiple ones if you want to override certain providers for a part of the widget tree.Watching a provider

How do we then obtain the string from the greetingProvider
so that we can display it using a Text
widget? There are actually two ways to do it. Let's showcase both based on the same piece of standard Flutter code. We want to get the provided string into the highlighted widget.
main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Riverpod Tutorial',
home: Scaffold(
appBar: AppBar(
title: Text('Riverpod Tutorial'),
),
body: Center(
child: Text('greeting goes here'),
),
),
);
}
}
The first way is to change the superclass of our widget to ConsumerWidget
which comes from the flutter_riverpod package. This adds a ScopedReader
function into the widget's build
method. Since we use the ScopedReader
to watch the provider and rebuild the widget if any change occurs, the convention is to give this parameter a name "watch".
main.dart
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
// Gets the string from the provider and causes
// the widget to rebuild when the value changes.
final greeting = watch(greetingProvider);
return MaterialApp(
title: 'Riverpod Tutorial',
home: Scaffold(
appBar: AppBar(
title: Text('Riverpod Tutorial'),
),
body: Center(
child: Text(greeting),
),
),
);
}
}
The other way of getting the value out of a provider is useful if you want to quickly optimize your widget rebuilds. It's not the end of the world in our case, but we're still rebuilding the whole MaterialApp
while, in fact, we only need to rebuild the affected Text
widget all the way down the widget tree. This is what the Consumer
widget is for.
main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Riverpod Tutorial',
home: Scaffold(
appBar: AppBar(
title: Text('Riverpod Tutorial'),
),
body: Center(
child: Consumer(
builder: (context, watch, child) {
final greeting = watch(greetingProvider);
return Text(greeting);
},
),
),
),
);
}
}
Reading a provider
Sometimes, it may be impossible to call watch
because you're not inside of the build
method. For example, you may want to perform an action when a button is pressed. That's when you can call context.read()
. Let's showcase it on another type of a provider - ChangeNotifierProvider
. Consider this code:
main.dart
class IncrementNotifier extends ChangeNotifier {
int _value = 0;
int get value => _value;
void increment() {
_value += 1;
notifyListeners();
}
}
final incrementProvider = ChangeNotifierProvider((ref) => IncrementNotifier());
As you've probably seen a thousand times already, we're going to create the classic counter app. A Text
widget will watch the provider and get automatically rebuilt if a change occurs and the FloatingActionButton
will only read the provider to call the increment()
method on it.
main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Riverpod Tutorial',
home: Scaffold(
body: Center(
child: Consumer(
builder: (context, watch, child) {
final incrementNotifier = watch(incrementProvider);
return Text(incrementNotifier.value.toString());
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read(incrementProvider).increment();
},
child: Icon(Icons.add),
),
),
);
}
}
Stuff you can't do with the "original" Provider
As you could've seen by now, Riverpod's providers objects don't rely on types for resolving their values. This is in contrast with the "original" Provider package. This is awesome because you can have multiple providers of the same type without any issue.
example.dart
final firstStringProvider = Provider((ref) => 'First');
final secondStringProvider = Provider((ref) => 'Second');
// Somewhere inside a ConsumerWidget
final first = watch(firstStringProvider);
final second = watch(secondStringProvider);
Another conclusion that logically flows out of Riverpod not relying on types is that you cannot possibly try to get a value which is unavailable. You can only watch
or read
a provider variable that you can access and as long as you can access a provider, its value has to be available, right?
Dependency between providers
Any real-world app has dependencies between classes. For example, you can have a ChangeNotifier
that depends on a Repository
which in turn depends on a HttpClient
. Handling such dependencies with Riverpod is simple and readable.
To keep our example simple, let's just have a FutureProvider
depend directly on a simple FakeHttpClient
. Getting hold of another provider inside a provider's function is done by calling read
on the ProviderReference
parameter which is always passed in. If you depend on a provider whose value can change, you can also call watch
.
FutureProvider
but I don't think its creation requires much explanation. It looks almost exactly like the most basic Provider
but the function that you pass in returns a Future
. Just like with aProvider
the creation function of a FutureProvider
runs as soon as we obtain it from a widget for the first time.main.dart
class FakeHttpClient {
Future<String> get(String url) async {
await Future.delayed(const Duration(seconds: 1));
return 'Response from $url';
}
}
final fakeHttpClientProvider = Provider((ref) => FakeHttpClient());
final responseProvider = FutureProvider<String>((ref) async {
final httpClient = ref.read(fakeHttpClientProvider);
return httpClient.get('https://resocoder.com');
});
Using values from a FutureProvider
from the UI is absolutely delightful. Gone are the days of clumsy FutureBuilder
s! Riverpod makes building widgets based on a Future
easy. It uses freezed unions behind the scenes to break down a Future
object into an AsyncValue
union which we can easily map to different widgets.
main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Riverpod Tutorial',
home: Scaffold(
body: Center(
child: Consumer(
builder: (context, watch, child) {
final responseAsyncValue = watch(responseProvider);
return responseAsyncValue.map(
data: (_) => Text(_.value),
loading: (_) => CircularProgressIndicator(),
error: (_) => Text(
_.error.toString(),
style: TextStyle(color: Colors.red),
),
);
},
),
),
),
);
}
}
Passing arguments to providers

What if we wanted to pass a user-defined URL to the responseProvider
? That's what provider families are for! By changing the responseProvider
to the following...
main.dart
final responseProvider =
FutureProvider.family<String, String>((ref, url) async {
final httpClient = ref.read(fakeHttpClientProvider);
return httpClient.get(url);
});
...we can now pass the request URL from the UI. Sure, there's no actual user input to keep things simple but you can try changing the hard-coded URL string and you'll see that the FutureProvider
reruns its creation function each time you change the passed in string.
main.dart
final responseAsyncValue = watch(responseProvider('https://resocoder.com'));
Automatically disposing of state
Here's an interesting observation - try changing the inputted URL to A then to B and lastly back to A. What happened? When you changed the value back to A, there was no CircularProgressIndicator
displayed which means that the value was readily present in the FutureProvider
and there was no need to perform yet another fake get request.
This caching of a provider's state is great but sometimes you may want to perform requests even to the same URL over and over again. That's what the autoDispose
modifier is for.
main.dart
final responseProvider =
FutureProvider.autoDispose.family<String, String>((ref, url) async {
final httpClient = ref.read(fakeHttpClientProvider);
return httpClient.get(url);
});
This modifier will dispose of the provider's state as soon as the provider is not used. In our case, this happens if we change the argument passed into the provider family. However, autoDispose
is useful even if you're not using a family
modifier. In such case, disposal will be the started when the ConsumerWidget
that watches a provider is disposed.
Conclusion
You've just learned the essence of the Riverpod package. We've gone through the very basics all the way to provider modifiers. Of course, we can go more in-depth because this package has a lot more to offer. If you don't want to miss the upcoming tutorials about building more advanced apps with Riverpod, sign up to the weekly Flutter newsletter below.
When I access this page, YouTube throws an error: Youtube refuses to connect.
I heard this can replace get_it, however I still don’t get clearly how to use it to inject/locate repository and other things.
So if I have these:
– Repository Class which implements IRepository
– FakeHttpClient Class which implement IHttpClient
– FirebaseLogger which implement ILogger
– Repository that use IHttpClient and ILogger
– Provider Class that extend ChangeNotifier and use IRepository
then I have to create:
final httpClientProvider = Provider((ref) => FakeHttpClient());
final loggerProvider = Provider((ref) => FirebaseLogger());
final repositoryProvider = Provider((ref) => Repository(ref.read(httpClientProvider)));
final provider = ChangeNotifierProvider(
(ref) => Provider(
ref.read(loggerProvider),
ref.read(approvalRepository),
));
Is this right?
Yes, that’s right. As someone pointed out on YouTube, you should only use `ref.watch` inside providers.
I have an abstract class LectureRepository implemented by LectureRepositoryImpl. LectureRepositoryImpl initialises SharedPreferences sharedPreferences with
LectureLocalDataSourceImpl({@required this.sharedPreferences})
Now I want to use Riverpod to access the repository so I do:
FutureProvider(
(ref) async {
return LectureLocalDataSourceImpl(sharedPreferences: await SharedPreferences.getInstance());
},
);
but now in lectureRepositoryProvider, wenn I read the lectureLocalDataRepositoryProvider I get an AsyncValue in the value localDataSource, which I cannot assign here:
final lectureRepositoryProvider = FutureProvider((ref) async {
final localDataSource = ref.read(lectureLocalDataRepositoryProvider);
return LectureRepositoryImpl(localDataSource: localDataSource);
});
How should I handle the AsyncValue?
Great video and very instructive. Please take a look at my SO question here: https://stackoverflow.com/questions/66233832/statenotifierprovider-not-keeping-state-between-flutter-hot-restarts. What am I doing wrong?
Excellent article! I’d like to see it one day as a part of the official Riverpod documentation in Getting Started section.
This is one of the best tutorial on Riverpod out there.