I think it's not an exaggeration to say that most of the content on Flutter is about state management. Being a reactive framework for the client-side, Flutter has integrated the concept of state since its birth. But if you do a complex project, setState
won't solve your problems, that's why there are state managers.
Flutter's most popular state management tool is package:provider
. Ok, maybe there's another one I'm not recommending nor mentioning, but provider
it's probably the one that most state management packages use or take inspiration from. At the time I'm writing this, 396 packages depend on provider
according to pub.dev.
I'm not going to talk much about provider
because I already made a clarification that I consider important in my article on state_notifier
. That article is a must-read since it clarifies quite what provider
is and it is one of the packages that riverpod
incorporates.
But... what is riverpod
and why do I like it so much?
Cause
In the state_notifier
article, I explained that Provider was a tool that allows us to easily use InheritedWidget
, which is the class in Flutter that gives access to its properties to its children in the widget tree.
provider
itself has some problems because of this relationship with InheritedWidget
.
First, doesn't allow us to create two Providers of the same type unless they are on a different subtree. If we have a class from which we want to create two separate objects to handle similar logic in different modules of the app, it's impossible for us because an InheritedWidget
is found in the tree looking for a specific data type --or class--.
This also causes Providers
not to be found and leaves us runtime errors that could be avoided in the compilation. Strike one.
Then combining Providers can be cumbersome, more complex than it should, and throw unexpected errors from the same unusual InheritedWidget
manipulation. The common way to make a Provider
aware of changes in another to react in its logic is by using a ProxyProvider
or ChangeNotifierProxyProvider
and yet these aren't the best alternatives in synchronization or syntax (it gets very nested). You can make hacky workarounds for this but I'm not into that.
They are a dependency injection and not exactly a listener for events. Strike two.
Finally: depending on InheritedWidget, it depends on Flutter. Which isn't the best if you like to encapsulate your business logic in a separate package and maintain a separation of concerns with your logic independent of the SDK of the user interface.
Strike three... well, not necessarily, but do you see that there are many opportunities for improvement?
Effect
All these problems made the creator of provider
, Rémi Rousselet, have the initiative to re-engineer InheritedWidget
from scratch, thus creating package:riverpod
.
In simple words, riverpod
doesn't depend on Flutter so it doesn't depend on InheritedWidget
, it allows us to combine Providers
to listen to and react to its changes and it guarantees us reliable code from the compilation stage.
Yes, it solves provider
's problems, and very well if you ask me.
Let's see it in action and its equivalents with respect to Provider.
Which package do I use
If you do a search in pub.dev, you will find 3 versions of riverpod
, let me explain what each one does.
package:riverpod
: Contains the core functionality to use it without depending on Flutter. This is the one to use if you are creating a package for a business logic layer other than the graphical interface.package:flutter_riverpod
: Contains the core, plus all the classes and widgets used to link the logic with the user interface, depending on Flutter. It's the one that you must use if your project already needs Flutter and you are not separating the logic in another package outside the project. It's the most common because of how projects are usually organized.package:hooks_riverpod
: Contains the core and adds the necessary functionality to be used in conjunction withpackage:flutter_hooks
, which must also be added as a dependency. It's the one you should use if your project uses hooks to simplify your code in the user interface and Riverpod to access the state. I'm not going to go too deep into this package in this article.
And if it wasn't clear, this image is in the documentation to make it more clear:
Migrating from Provider to Riverpod
From MultiProvider to ProviderScope
Initially, when we wanted to insert a Provider
in the widget tree, we did something like this:
void main() {
runApp(
MultiProvider(
providers: [
Provider(create: (_) => MyProvider()),
],
child: MyApp(),
),
);
}
Here we could have a list of our Providers
and it depended on the MultiProvider
, a single Provider
or ChangeNotifierProvider
. This way gave us a scope that determined wherein our widget tree we would be able to use our logic component.
In riverpod
, our Providers
can be used as global variables that we can also consume in a global scope called ProviderScope
and this scope can be overwritten as necessary in the tree.
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
New way to create Providers
Once our ProviderScope
is declared, we can use any Provider
below it. Now let's see how we make a Provider
.
In the case of provider
, we create a class that inherits from ChangeNotifier
, StateNotifier
, a repository or service class of its own. riverpod
comes with StateNotifier
by default, so it's easier to implement this alternative, same way there are more types of Providers than we will talk about in a moment.
class NamesNotifier extends StateNotifier<List<String>> {
NamesNotifier([List<String>? initialNames]) : super(initialNames ?? []);
void add(String newName) {
state = [...state, newName];
}
}
We can use this NamesNotifier
both in provider
and riverpod
because the logic components are not affected by the package, which already takes care of incorporating this component into the widget tree for use.
In provider
, we need to include the packages state_notifier
and flutter_state_notifier
. In riverpod
, these are not needed since flutter_riverpod
contains the ConsumerWidget
(equivalent to the Consumer
widget in provider
but used more similar to StatelessWidget
).
💡 I quickly clarify that the core of the packages is in
riverpod
andstate_notifier
, and the other packages that I mentioned that are called the same with the difference of the flutter prefix contain the widgets to communicate the logic of these packages with the user interface.
To create a Provider
on riverpod
, use the following:
final myProvider = Provider((ref) => MyProvider());
And to create a StateNotifierProvider
in riverpod
we do this:
final namesNotifierProvider = StateNotifierProvider<NamesNotifier, List<String>>((_) => NamesNotifier());
Once declared, no matter what file it's in, the Provider
is in the ProviderScope
and is available wherever it is needed via ProviderRef
.
I want you to analyze the syntax a bit.
We declare them as a
final
variable.We instance the class
Provider
, which contains a callback function by which we return the class.Note: this function will serve us much later.
The callback returns a variable of type
ProviderRef
, which we callref
, for our use in creating theProvider
.
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
.
See why it was good to separate him from Flutter? We no longer need BuildContext
.
Use of Providers with Widgets
In provider
, we use the BuildContext
of the build
method of any widget to access our Provider
since it is anInheritedWidget
.
We can do it in two ways...
- Use
ConsumerWidget
.
class WidgetToShowNames extends ConsumerWidget {
const WidgetToShowNames({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final names = ref.watch(namesNotifierProvider);
return ListView.builder(
shrinkWrap: true,
itemCount: names.length,
itemBuilder: (_, index) {
final name = names[index];
return Text(name);
},
);
}
}
- Use
Consumer
widget.
class WidgetToShowNames extends StatelessWidget {
const WidgetToShowNames({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Consumer(
builder: (context, ref, child) {
final names = ref.watch(namesNotifierProvider);
return ListView.builder(
shrinkWrap: true,
itemCount: names.length,
itemBuilder: (_, index) {
final name = names[index];
return Text(name);
},
);
},
);
}
}
Yes, there's a ConsumerWidget
that is very similar to aStatelessWidget
, but adds a WidgetRef
as a parameter to the build
method. This new parameter allows us to access any Provider
that we create and be listening (or observing) the changes it may have.
Likewise, we can use a StatelessWidget
in conjunction with the Consumer
widget that works in the same way as in Provider
, only that it gives us our WidgetRef
instead of the type that we define since there can be multiple Providers with the same type and that no longer guarantees that we will get the value we want when sending the type.
💡 In this section of What Marcos Recommends...
I use
ConsumerWidget
since I don't like to nest more widgets withConsumer
, the code seems much more readable to me and it is easier to find where your Providers are used having classified your widgets inStatelessWidget
,StatefulWidget
andConsumerWidget
.
Combining Providers
One of the solutions that riverpod
brings to Provider
problems is easier communication between Providers
. We achieve this thanks to the ProviderRef
.
The creation of a Provider
gives us by default its possible combination with another that is in the reference. This is how we can inject dependencies of Providers
and establish behaviors based on those changes.
final authenticateUser = FutureProvider<String>(
(ref) async {
await ref.read(authProvider.notifier).login();
return ref.read(authProvider);
},
);
final authProvider = StateNotifierProvider<AuthNotifier, String>(
(_) => AuthNotifier(),
);
class AuthNotifier extends StateNotifier<String> {
AuthNotifier() : super('no-token');
Future<String> login() async {
return await Future.delayed(
const Duration(seconds: 3),
() => state = 'token',
);
}
}
Suppose we have a Provider
with authentication functionality, which we call authProvider
, we use its login
method that sets an access token as its state. Having the token in the state, we can use that state to call an authentication function and save the value for other queries.
What? What's that FutureProvider
thing? Glad you ask...
Everything is a Provider
As you can see, there are many types of Providers
, and it's a word that's going to come up everywhere if you work with riverpod
.
Just like Flutter tells you to consider everything a widget in the framework, at riverpod
I suggest you consider everything a Provider. A Provider
can be a function (they are built with functions themselves), a state variable, a global variable (literally), and so on.
Provider types
I'm going to present to you the most common and used types of Providers
so far.
StateProvider
: Exposes a value that can be modified from outside. We access the state through thestate
variable and we can change it. Very similar toValueNotifier
.FutureProvider
: Constructs an asynchronous value orAsyncValue
and has the operation of an ordinaryFuture
. It's useful to load data from a service whose method to obtain this data is asynchronous.StreamProvider
: Create and expose the last value in aStream
. Ideal for communication in real-time with Firebase or some other API whose events can be interpreted inStream
.StateNotifierProvider
: Create aStateNotifier
and expose its state. The most used for logic components, it allows easy access tostate
, which is usually a private property that's modified by public methods.ScopedProvider
: Defines aProvider<T>
(of a generic type) that behaves differently somewhere in the project. It's used in conjunction with theoverrides
property ofProviderScope
and defines with the latter the scope where thisScopedProvider
is going to be modified.ProviderFamily
: This type ofProvider
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 ourProvider
, such as aString
.
final usersFamily = FutureProvider.family<User, String>(
(ref, id) async => dio.get('https://my_api.dev/users/$id'),
);
AutoDisposeProvider
: Another modifier that allows us to automaticallydispose
of ourProviders
, allowing us to delete the state when the widgets where they are used are destroyed.
final numbersProvider = StreamProvider.autoDispose(
(ref) {
final streamController = StreamController<int>();
ref.onDispose(() {
streamController.close();
});
return streamController.stream;
},
);
Best practices
How to structure Providers
riverpod
has the peculiarity that, as we saw previously, it covers many use cases with different types of Providers
, from app state to ephemeral state.
The problem is that there is no guide on how to structure Providers
, so I always recommend separating your logic in a good way.
I summarize it in my personal rules:
Separate project folders by functionalities (features).
In a specific functionality I try to have a
/provider
folder where I have all myProviders
.I don't have more than one logic component (
StateNotifier
) for functionality.In the provider folder, I separate my state, logic component, and
Provider
in separate files. You can also have a generatedfreezed
file if you use that tool for state union classes.
With the rules that I mentioned before, we have a folder structure like this...
Some things to consider
I was thinking for a long time if there are any downsides or something to say as a warning about riverpod
. Being honest and objective, I don't find many things and the ones I do find aren't critical.
riverpod
is already in a stable phase, as you can see. You can implement it in your projects from now on and I dare to say that it may be a better option than provider
. It's a package that can take your time to master, but once you do, it becomes a very capable tool.
I also clarify that it isn't a replacement for provider
yet, since provider
still solves many needs. For me, riverpod
is becoming a real state management option since more developers are mastering it and the problems it solves are valued. Many have yet to find scenarios where provider
shows its flaws.