With the development of Flutter, state management frameworks on Flutter have sprung up one after another in recent years. In recent years, the most officially recommended state management framework on Flutter is Undoubtedly Riverpod, even surpassing Provider. In fact, Riverpod officially calls itself “Provider, but different.”
The Provider itself is, in its own words, “an encapsulation of the InheritedWidget, but simpler and more reusable.” Riverpod reconstructs new possibilities on the basis of Provider.
For a comparison of the state management frameworks of the past year, see Flutter State Management 2021: How to Choose? This article takes you through the guts of how RiverPod is implemented, understanding how it works, and how to do it with fewer templates than Provider and without reliance on BuildContext.
preface
If there’s one thing that’s most obvious about Riverpod, it’s that it doesn’t rely on BuildContext externally. Since it doesn’t rely on BuildContext, it can do things like this relatively easily:
Providers in Riverpod can be written globally and do not rely on BuildContext to write the business logic we need.
⚠️ For the record, this has nothing to do with subsequent providers or third-party library providers.
How does Riverpod work inside? Let’s explore the implementation of Riverpod.
The implementation of Riverpod is relatively complex, so I am still patient to read down, because this is a step by step analysis, so if you are confused during the process of reading, you can not care about it first, and then go back to read the whole article may be more clear.
Since ProviderScope
With Flutter, you cannot avoid using an InheritedWidget as long as you use state management. The same is true with Riverpod, where there is a ProviderScope. Typically, you only need to register a top-level ProviderScope.
If you’re still in doubt about inheritedWidgets, see What I’m Digging for: Understanding State and Providers
We’ll start with an example, as shown below, of the official simple example, which can be seen here:
- Nested a top-level
ProviderScope
; - Created a global
StateProvider
; - use
ConsumerWidget
的ref
To create acounterProvider
forread
So I read State, and I getint
Value is increased; - Use another
Consumer
的ref
To create acounterProvider
forwatch
To read after each changeint
Value;
A very simple example would be to see that there is no of(context), and the data in the global counterProvider would be read/watch through the ref, and read and update correctly.
So how does that work? How is the counterProvider injected into the ProviderScope? Why don’t you see the context? With these questions we continue to explore.
First we look at ProviderScope, which is the only top-level InheritedWidget, so the counterProvider must be stored here:
In RiverPod, the greatest use of the ProviderScope is to provide a ProviderContainer.
More specifically, it is provided by internal nested UncontrolledProviderScope, so here we can know that: ProviderScope provides state sharing down because it has an InheritedWidget inside it, and the primary shared down is the ProviderContainer class.
So the first guess is that the various Providers we define, such as the counterProvider above, are stored in ProviderContainer and shared down.
In fact, the official definition of ProviderContainer is to hold the State of various Providers and allow override of specific Providers.
ProviderContainer
There is a new class called ProviderContainer, which you don’t normally need to know about to use RiverPod because you won’t use it directly. However, every action you do with RiverPod will involve its implementation, for example:
ref.read
You’re going to need itResult read<Result>
;ref.watch
You’re going to need itProviderSubscription<State> listen<State>
;ref.refresh
You’re going to need itCreated refresh<Created>
Even the saving and reading of various providers are basically related to it, so as a class for internal management of various providers, it implements some key logic in RiverPod.
“Provider” and “Element”
How does the Provider work when we know that ProviderScope shares ProviderContainer? Why can ref.watch/ ref.read read the value of its Provider?
To continue with the previous code, we defined the StateProvider and used ref.watch. Why can we read the state value?
First of all, StateProvider is a special Provider, and there is an internal one called _NotifierProvider to help it implement a layer of transformation, so we use the most basic Provider class as the analysis object.
Basically, all kinds of similar providers are subclasses of ProviderBase, so we’ll parse ProviderBase first.
Within RiverPod, each ProviderBase subclass will have its corresponding ProviderElementBase subclass implementation, such as the StateProvider used in the previous code being ProviderBase. It also has a corresponding StateProviderElement which is a subclass of ProviderElementBase;
So basically every “Provider” in RiverPod has its own “Element”.
⚠️ The “Element” here is not one of the three elements of the Flutter concept, but a subclass of the Ref object in RiverPod. Ref mainly provides the interface between “providers” in RiverPod and provides some abstract lifecycle methods, so it is a unique “Element” unit in RiverPod.
What do “Provider” and “Element” do?
First, in the example above, we pass in the StateProvider (ref) => 0, which is actually the Create
> function. We use this Create function as the entry point to explore.
Create<T, R extends Ref> = T Function(R ref)
When constructing “Provider” in RiverPod, we will pass a Create function, which will write some necessary business logic. For example, counterProvider ()=> 0 returns a value of int 0 when initialized. More importantly, it determines the type of State.
This is especially true if we add
to the code above. In fact, the State we’ve been talking about is a generic, and we define “Provider” by defining the type of the generic State, such as int.
Going back to normal Provider calls, the Create function we pass in is actually being invoked in ProviderElementBase.
As shown in the figure above, simply put, when ProviderElementBase performs “setState,” the Create function is called to perform the generic State we defined, get Result and notify and update the UI.
⚠️ the “setState” here is not the setState of the Flutter Framework, but the first “setState” function in RiverPod, independent of the State in the Flutter Framework.
So each “Provider” will have its own “Element”, and the Create function passed in when the “Provider” is built will be executed inside the “Element” with a setState call.
In the “Element”setState
The main thing is to get a RiverPod from the new newStateResult
Object, and pass through_notifyListeners
To getResult
Update to thewatch
Place.
The main function of Result is to provide execution results through result. data, result. error, map and requireState. In general, the state is obtained through requireState, which is embodied in RiverPod as follows:
When we call read(), we end up calling element.readself (); , which returns requireState (which is generally our generic State).
Isn’t it a bit messy?
When “Provider” is built, setState(_provider.create(this)) is executed in Element. Call the Create function we passed in and pass in “Element” itself as a ref, so the ref we use is actually ProviderElementBase.
So there is a reason for the name of RiverPod. The relationship between “Provider” and “Element” in RiverPod is similar to the visual sense of the Widget and Element in Flutter.
Step by step:
- We passed one in when we built the Provider
Create
Functions; Create
Function will beProviderElementBase
The inside of thesetState
Called, gets aReuslt
;Reuslt
Within therequireState
We can use it againread()
Get the generic type we definedState
The value of the.
WidgetRef
So far, we haven’t talked about how StateProvider is associated with ProviderScope, or “Provider” with ProviderContainer. Why should ref. Read read State?
So in the previous code, the ConsumerWidget and Consumer are the same thing, and the ref is the “Element” or ProviderElementBase we’ve been talking about.
In the source code, you can see that the logic of the ConsumerWidget is mainly in the ConsumerStatefulElement, which inherits the StatefulElement. And implement WidgetRef interface.
The code above shows you the familiar figures: ProviderScope, ProviderContainer, WidgetRef.
. First we see ProviderScope containerOf (this), finally saw the familiar BuildContext ever, this method is used to our common of (context), But it is put into the ConsumerStatefulElement to get the ProviderContainer shared below the ProviderScope.
So we see that the ConsumerStatefulElement in the ConsumerWidget gets the ProviderContainer, So ConsumerStatefulElement can call read/ Watch on ProviderContainer.
Then looking back, the ConsumerStatefulElement implements the WidgetRef interface, so the WidgetRef we use is the ConsumerStatefulElement itself
That isref.read
Is to performConsumerStatefulElement
的 read
, and then execute toProviderContainer
的 read
.
So we can conclude: BuildContext is Element, and Element implements WidgetRef, so WidgetRef is the replacement for BuildContext.
The Element of Flutter is not to be confused here with the “ProviderElementBase” in RiverPod.
So the WidgetRef interface becomes an abstraction of Element instead of BuildContext, so this is one of the “magic” parts of Riverpod.
read
So we’ve sorted out ProviderScope, Provider, ProviderElementBase, ProviderContainer, ConsumerWidget (ConsumerStatefulElement) And WidgetRef et al., and finally we can start to figure out the whole chain of work for Read.
After we clear and know the concepts and functions, we combine ref.read to do a process analysis, and the overall is:
ConsumerWidget
It will pass through the interiorConsumerStatefulElement
Get to the top levelProviderScope
The SharedProviderContainer
;- When we go through
ref
callread
/watch
When, in fact, is passingConsumerStatefulElement
To callProviderContainer
Within theread
Functions;
And then finally, how does the read function inside ProviderContainer read State?
This is combined with ProviderElementBase, which we also covered earlier, and the fact that ProviderContainer calls readProviderElement when it executes the read function.
ReadProviderElement, as its name implies, is used to obtain the corresponding Element through the Provider, for example:
ref.read(counterProvider),
Copy the code
In general, read/watch simply means retrieving the “Element” ProviderElementBase from ProviderContainer using proivder as the key. This process has a new object that needs to be briefly introduced. _StateReader.
One of the keys to the readProviderElement is to get _StateReader. Inside the ProviderContainer is an internal variable for _stateReaders, which is the Map used to cache _StateReader.
So inside ProviderContainer:
- 1, first will be based on
read
When the incomingprovider
Construct a_StateReader
; - 2,
provider
As the key,_StateReader
For the value stored in_stateReaders
This Map, and return_StateReader
; - 3, through
_StateReader
的getElement()
Gets or creates toProviderElementBase
;
ProviderBase is the Key, _StateReader is the value, _stateReaders is the value, and “provider” is stored in the ProviderContainer. That is, associated with ProviderScope, where “provider” and ProviderScope are bound together.
Instead of using overt BuildContext and redundant nesting, let the Provider and ProviderScope be associated. In addition, you can see how ProviderElementBase can be built or retrieved through the provider when ref.read is used.
Get ProviderElementBase Remember earlier where we introduced “Provider” and “Element”? ProviderElementBase calls setState to execute the Create function we passed in, and Result returns State.
As you can see, returning Element.readSelf () after ProviderElementBase returns requireState.
The simplest ref.read process in RiverPod is now complete:
-
ProviderScope shares ProviderContainer down;
-
The ConsumerStatefulElement inside the ConsumerWidget reads ProviderContainer via BuildContext and implements the WidgetRef interface.
-
Call read in ProviderContainer via WidgetRef’s read(provider);
-
ProviderContainer creates or retrieves the ProviderElementBase through the provider of the read method
-
ProviderElementBase executes the Create function in the provider to get the Result return State;
Other Watch, refresh processes are similar, but some internal implementation logic is more complex, such as refresh:
The refresh of ProviderContainer is triggered by the ref.refresh method, and setState(_provider.create(this)) is eventually triggered by the _buildState method.
From this flow analysis, you can also see how RiverPod does not expose the logic for implementing full correlation using BuildContext.
Additional analysis
The whole call process is introduced in the previous section. Here we will introduce how to implement some common calls. For example, you will see many “elements” in Riverpod. Such as ProviderElement, StreamProviderElement, FutureProviderElement and other ProviderElementBase subclasses.
We have found that they are not elements in a Flutter, but units of State in a Riverpod that handle the State of the Provider. For example, FutureProviderElement provides an AsyncValue
based on ProviderElementBase, which is mainly used in FutureProvider.
AsyncValue
The normal create method definition in RiverPod looks like this:
On FutureProvider, listenFuture is added, and the value of the Function is AsyncValue
of State type.
Listenfuture () is executed internally. After AsyncValue
.loading(), Return AsyncValue
. Data or AsyncValue
. Error depending on the Future result.
So, for example, in the case of read/watch, the returned generic requireState becomes AsyncValue
.
In addition to data \ asData and T value of AsyncData, it mainly provides the construction methods of different states mentioned above. For example the when method:
autoDispose & family
There are static variables called autoDispose and family in Riverpod, which are used by almost every Provider.
For example, in the previous code we have a FutureProvider, which we use as autoDispose:
Main is actually FutureProvider. AutoDispose AutoDisposeFutureProvider, and so on each Provider has its own basic autoDispose implementation, family is also in the same way.
If a normal Provider inherits from AlwaysAliveProviderBase, an AutoDisposeProvider inherits from AutoDisposeProviderBase:
As can be seen from the name:
AlwaysAliveProviderBase
Is an active one;AutoDisposeProviderBase
Nature just doesn’tlistened
When destroyed;
Internal _listeners, _subscribers, and _dependents are empty, but there is also a maintainState control state which is false by default.
Simple understanding is to use “burn immediately”.
MayNeedDispose is called to attempt to dispose:
Dispose (), remove from the _stateReaders map, etc.
The same family corresponds to ProviderFamily, which builds the provider with an additional parameter, that is, an additional parameter.
For example, the default is:
final tagThemeProvider = Provider<TagTheme>
Copy the code
Can be changed into
final tagThemeProvider2 = Provider.family<TagTheme, Color>
Copy the code
Then you can use additional arguments for read/watch:
final questionsCountProvider = Provider.autoDispose((ref) {
return ref
.watch(tagThemeProvider2(Colors.red));
});
Copy the code
ProviderFamily: ProviderFamily: ProviderFamily: ProviderFamily: ProviderFamily: ProviderFamily: ProviderFamily
You can see that create is a new Provider, which is a nested Provider under family.
Watch (tagThemeProvider); That’s fine, because our tagThemeProvider is directly ProviderBase.
Watch (tagThemeProvider2); You’ll see an error
The argument type 'ProviderFamily<TagTheme, Color>' can't be assigned to the parameter type 'ProviderListenable<dynamic>'.
Copy the code
ProviderFamily
ProviderFamily
Watch (tagThemeProvider2(colors.red)); .
A single execution via tagThemeProvider2(colors.red) becomes the ProviderBase we need.
Why is tagThemeProvider2 ProviderFamily executed this way? ProviderFamily has no such constructor.
This covers the features of the Dart language, if you’re interested: juejin.cn/post/696836…
The first thing we get here is a ProviderFamily
. In Dart all Function types are subtypes of Function, so functions have call methods inherent.
tagThemeProvider2(colors.red); FamilyProvider is a subclass of ProviderBase.
⚠️ Notice what’s a little bit wrong here, one is ProviderFamily, one is FamilyProvider, we get FamilyProvider from ProviderFamily, As ProviderBase for Ref.watch.
The last
I haven’t written such a long source code analysis for a long time, and I didn’t know I was writing it until midnight. Actually, the whole Riverpod is more complex, so it is more difficult to read, but it is relatively easier to use, especially without the BuildContext constraint. But it also comes with the dependency of consumerWidgets. All the pros and cons are up to your needs, but overall Riverpod is definitely a good framework to try.