State management in Flutter is an ongoing topic, and there are frameworks such as Provider, GET, and fish_redux on the market. Since I came into contact with flutter development, I have generally experienced stateless management, simple state abstraction, and now I am using an internal provider-like solution in my company. In addition, I recently saw Zhang Fengjietele’s views and understanding of state management and Flutter’s cognition and thinking of state management. I will also share with you my views on state management and analysis of mainstream frameworks (such as Provider and GET) based on my past experience, as well as some pitfalls I have stepped on in practice.
Why do YOU need state management: To solve the problems of responsive development
First, why is state management needed in FLUTTER development? In my opinion, the essence is a series of problems caused by the construction of Flutter responsiveness. Traditional native development uses controlled builds, which are two completely different ideas, so we don’t hear about state management in native development.
1. “Responsive” vs. “controlled” analysis
So how do you understand “responsive” and “controlling”? Here we use the simplest counter example:
As shown in the picture, click the button in the lower right corner to add one to the displayed text number. This very simple feature should be thought of in a controlled build mode.
When the button in the lower right corner is clicked, take the object in the middle TextView and manually set the displayed text. The code is as follows:
/// The number of displays
private int mCount = 0;
/// A TextView with numbers in the middle
private TextView mTvContent;
/// The scheme called by the lower right button
private void increase(a) {
mCount++;
mTvContent.setText(mCount);
}
Copy the code
With flutter, we just need _counter++ and then call setState((){}). SetState refreshes the entire page so that the values displayed in the middle of the page constantly change.
These are two completely different development approaches. In the control approach, the developer needs to get an instance of each View to handle the display. In a reactive approach, we just need to deal with the state (data) and the display of the state (widgets), leaving the rest to setState(). So there’s a saying
UI = f(state)
Copy the code
In the example above, where state is the value of _counter, call setState driver f (build method) to generate a new UI.
So what are the benefits and problems of “responsive” development?
2. Benefits of responsive development: It frees developers from the tedious control of components to focus on state handling
The biggest advantage of responsive development, I think, is that it frees developers from the tedious control of components to focus on state handling. After getting used to flutter development, I cut back to the original feeling that controlling the View is too cumbersome, especially if multiple components are related to each other, and the things you need to deal with explode. In Flutter we just need to deal with the state (complexity is the mapping of the state -> UI, i.e. Widget building).
For example, let’s say that you are the CEO of a company and you create a work plan for your employees. With controlled development, you need to push every employee (View) to complete their tasks.
If you have more and more employees, or their tasks are related to each other, you can imagine how much work you can do.
In this case, you’re not the boss, you’re working for the View.
One picture is controlled development
You wonder why you need to deal with such small details now that you’re CEO, so responsive development comes along.
With responsive development, you only need to handle each employee’s plan (state), and when you say “setState”, each employee (Widget) will build according to the plan. This makes you really enjoy being a CEO.
In one picture, responsive development is
The latest developments in technologies like Jetpack Compose and Swift are also moving in a “responsive” direction, as de Guo has also talked about. Learning to flutter is not far from compose.
Responsive development is so good, what are the problems with it?
3. Problems of responsive development: the goal of state management
When I first got involved with Flutter, I did not get involved with state management, but with the original “responsive” development. There are many problems encountered in the process, and there are three main ones summarized
Logic and page UI coupling, resulting in unreuse/unit testing, modification confusion, etc
Initially all the code is written directly to the widget, which results in widget file bloat and common logic such as network requests and page states, paging, and repeated writes (CV) across different pages. This problem also exists in the native, so there are some ideas to solve it, such as MVP.
It is difficult to access data across components (across pages)
Cross-component communication can be divided into two types, “1, parent to child” and “2, child to parent”. The first can be implemented with the Notification mechanism, and the second with no contact to the Element tree, I use callback. If you come across a widget with two or so layers nested, you’ll see how cool it can be.
This problem also applies to accessing data. For example, if there are two pages where the filter data is shared, there is no elegant mechanism to handle such cross-page data access.
Can’t easily control the refresh range (changes in page setState can cause changes in the global page)
The last problem is also the advantage mentioned above. In many scenes, we only modify part of the state, such as the color of the button. But the setState of the entire page will cause other parts of the page that don’t need to be changed to be rebuilt as well. I’ve already concluded that I’ve been using setState() incorrectly? .
In my opinion, the core of the state management framework in Flutter lies in the solution of these three problems. Let’s take a look at some mainstream frameworks such as provider and GET.
Second, provider, get state management framework design analysis: how to solve the above three problems?
1. Logic and page UI coupling
Traditional native development also has this problem, and activities can explode, so the MVP framework decoupled them. In a nutshell, the logic of the View is removed to the Presenter layer, where the View is responsible for building the View.
This is also the solution for almost every state management framework in Flutter. The Presenter above is the GetxController in Get, the ChangeNotifier in Provider, and the Bloc in Bloc. It is worth mentioning that the approach to Flutter differs from that of the original MVP framework.
We know that in the traditional MVP model, the logic is convergent to Presenter, and the View focuses on THE UI construction. Generally, the View and Presenter define their actions by interface, and hold interfaces for each other to call (and sometimes hold objects directly for less).
This is not a good idea for a Flutter. A View corresponds to a Widget in a Flutter from the Presenter → View relationship. A Widget’s lifecycle is non-perceptive, so it is not a good idea to grab a Widget instance directly. There are view.setbackground methods in the native flutter, but you don’t define and call widget.xxx in flutter. In Flutte we usually combine this with a local refresh component for Presenter (such as ValueListenable) -> View (ValueListenableBuilder) control.
In the View → Presenter relationship, the Widget can actually hold the Presenter directly, but this can cause difficulties in data communication. Different state management frameworks have different solutions to this point. In terms of implementation, they can be divided into two categories: Provider, Bloc, based on the Flutter tree mechanism, and GET, which is implemented through dependency injection. Let’s see:
A, Provider, Bloc the idea of dependent tree mechanism
First, a brief understanding of the Flutter tree mechanism is required.
We create a widget tree in Flutter by nesting widgets. What if a node WidgetB wants to get the name attribute defined in WidgetA?
Flutter gives us methods to look up and down in the BuildContext class
abstract class BuildContext {
///Find type T State in the parent node
T findAncestorStateOfType<T extends State>();
///Iterates through the Element object of the child element
void visitChildElements(ElementVisitor visitor);
///Find T type inheritedWidgets such as MediaQuery in the parent node
T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object aspect })
...... }
Copy the code
This BuildContext corresponds to the context in our build(Context) method for each Widget. You can think of the context as a physical node in the tree. With the findAncestorStateOfType method, we can access WidgetA layer by layer and get the Name property.
The findAncestorStateOfType() method is called, which looks up the parent node level by level. Obviously, the search speed depends on the depth of the tree, and the time complexity is O(logn). And data sharing scenario is very common in the Flutter, such as the theme, such as user information, etc, in order to faster access speed, Flutter in the provided dependOnInheritedWidgetOfExactType () method, It stores the InheritedWidget into the Map so that the time complexity of the node’s lookup becomes O(1). But both of these methods are essentially implemented through a tree mechanism, and they both require a “context”.
Bloc and Provider use this tree mechanism to retrieve View -> Presenter. So every time you use Provider you call provider.of
(context).
Is there any benefit to this? Obviously, all Widget nodes below the Provider can access presenters in the Provider via their own context. This is a good solution for cross-component communication. However, we have encountered some problems in the practice of relying on context. I’ll cover that in my next article. For more formal design of the interaction between View and Presenter, I highly recommend Flutter’s awareness and thinking about state management.
By the way, does this.of(context) look familiar to you? Yes, the routing of a Flutter is also based on this mechanism. For the routing of a Flutter, you can read my previous article on how to understand the source design of a Flutter routing. This series.
B. Get through dependency injection
The tree mechanic is great, but it relies on context, which is sometimes maddening. Get provides access to the Presenter layer through dependency injection. Simply put, Presenter is stored in a singleton Map that can be accessed anywhere at any time.
Global singleton storage must take Presenter’s collection into account, otherwise it may cause memory leaks. To use GET, you either dispose the page manually and dispose it, or you dispose the page using GetBuilder.
@override
void dispose() {
super.dispose();
if (widget.autoRemove && GetInstance().isRegistered<T>(tag: widget.tag)) {
// Remove Presenter instanceGetInstance().delete<T>(tag: widget.tag); }}Copy the code
You may be wondering why you don’t need to consider this when using a Provider.
This is because normal PageRoute providers always follow PageRoute. As the page exits, all nodes in the tree are reclaimed, so it can be understood that the system mechanism solves this problem for us.
Of course, if you are at a very high Provider level, such as the MaterialApp level, the Presenter you store will also tend to be global logic, and their life cycle will follow the entire App.
2. Difficulty accessing data across components (across pages)
Both types of state management solutions support data access across components. In provider, we use context.
As shown in the figure above, the storage nodes of the Provider generally follow the page. To achieve cross-page access, the storage nodes of the Provider need to be placed in a higher position, but also need to pay attention to recycling. Because GET is a global singleton, it has no dependencies, either across pages or across components.
3. You can’t easily control the refresh range
There are many solutions to this problem, such as Stream, ChangeNotifier, ValueListenable, etc. They essentially set up a binding mechanism between the View and the data so that when the data changes, the responding components change with it, avoiding additional builds.
/// Declare data that may change
ValueNotifier<int> _statusNotifier;
ValueListenableBuilder<int> (// Establish a binding relationship with _statusNotifier
valueListenable: _statusNotifier,
builder: (c, data, _) {
return Text('$data');
})
/// Data changes drive ValueListenableBuilder partial refresh
_statusNotifier.value += 1;
Copy the code
For one thing, seeing the Obx component of GET in use really blew me away at first.
class Home extends StatelessWidget {
var count = 0.obs;
@override
Widget build(context) => Scaffold(
body: Center(
child: Obx(() => Text("$count")),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () => count ++,
));
}
Copy the code
There are only three lines of critical code
/// That variable
var count = 0.obs;
/// In response to the component
Obx(() => Text("$count"))
/// Variable modification, synchronization Obx changes
count ++
Copy the code
What? That simple? How does Obx know which variable he should look at?
The realization of this mechanism can be summarized as follows: 1. Variable is set to observable state; 2. Let’s take a quick look:
1. Observable state of variables
If you look at the way it declares variables, you can see that count is not a normal int, but 0. Obs. The obs extension returns an object of type RxInt, the core of which is its get and set methods.
T get value {
if(RxInterface.proxy ! =null) { RxInterface.proxy! .addListener(subject); }return _value;
}
set value(T val) {
// ** omit non-critical code **
_value = val;
subject.add(_value);
}
Copy the code
We can imagine this RxInt type object as a big lady, the subject inside is a big lady servant girl, each big lady only one servant girl, RxInterface. Proxy is a static variable, has not appeared, let’s take it as a small black line.
We can see that every time we call RxInt’s get method, hei will pay attention to our servant girl dynamic.
When a set value is set, the girl will inform the servant girl.
So who is Little Black?
2. The response component is associated with the variable
There is only one truth, the little black is our Obx component, check Obx internal code can see:
@override
Widget build(BuildContext context) => notifyChilds;
Widget get notifyChilds {
// Select rxinterface. proxy from rxinterface. proxy
final observer = RxInterface.proxy;
// Every Obx has a _observer object
RxInterface.proxy = _observer;
final result = widget.build();
RxInterface.proxy = observer;
return result;
}
Copy the code
When Obx calls the build method, it returns notifyChilds. The get method assigns _observer to rxInterface.proxy, _observer, and Obx, which we assume is a cheating man.
So with that in mind, let’s go through the process
body: Center(
child: Obx(() => Text("$count"))),Copy the code
First, the Obx component is returned in the build method of the page, and this is where our cheating and philandering man enters the scene, who is now black.
The Obx component returns Text(“$count”), where $count actually translates to count.tostring (). This method is overridden by RxInt, which calls value.tostring ().
@override
String toString() => value.toString();
Copy the code
So $count is equivalent to count.value.tostring (). Remember we said above that when the get method is called, black will pay attention to the servant girl, so now it is
On this day, miss big mood good, direct count++, a careful look, the original count++ has also been rewritten, called value =
RxInt operator +(int other) {
value = value + other;
return this;
}
Copy the code
When I mention miss Big’s set method above, she will inform the servant girl. And the man who deceives and dallied with the female’s feelings pays attention to the servant girl all the time, once saw the servant girl had changed, the man who deceives and allied with the female’s feelings cannot respond quickly, immediately came to flatter.
The whole process can be understood in the way above. Ok, why do we say Obx is a man who cheats on women’s feelings. As long as it is in his build phase, he can observe all Rx variables that call get. That is, any call to set value from any of the variables triggers his reconstruction.
Canon version can see the Flutter GetX depth profiling | we will out of my way (word graphic).
All in all, the design is really quite clever. Obx indirectly observes all Rx variables that call get Value during the build phase. One problem with this, however, is that the get Value must be explicitly called during the build phase, otherwise the binding cannot be established.
For components like LsitView, the child build is done during layout, and an error occurs if you don’t call get Value beforehand. For example, the following code
Center(
child: Obx(() => ListView.builder(
itemBuilder: (i, c) => Text('${count}'),
itemCount: 10,),),Copy the code
Of course, GetBuilder is also provided in GET to handle partial flushes, but we’ll leave the rest for the next installment.
Third, summary
Everyone may have some thoughts on state management, and those are my thoughts. Feel free to leave your thoughts in the comments. In general, the essence is to solve the three problems mentioned, although different frameworks may have different approaches. You can also use this idea to analyze the design of the framework you are currently using so that you can get in touch with the essence of the framework, not just the use of the framework. In the next installment, I will combine the design and daily use of the framework and share my problems with providers and GET in the development process, as well as what I think is not reasonable design.
If you have any questions, please contact me through the official account. If the article inspires you, I hope to get your thumbs up, attention and collection, which is the biggest motivation for me to continue writing. Thanks~
The most detailed guide to the advancement and optimization of Flutter has been collected in Advance of Flutter or RunFlutter.
Past highlights:
Advance optimization of Flutter
The Flutter core rendering mechanism
Flutter routing design and source code analysis