Posted by Nayuta, CFUG Community
State management has always been a hot topic in Flutter development. When it comes to state management frameworks, the community also has
Get , Provider
There are various schemes represented, and they have their own advantages and disadvantages.
With so many options, you might be thinking, "Do I need to use state management? Which framework is right for me?"
This article will start from the author's actual development experience, analyze the problems and ideas solved by state management, and hope to help you make a choice.
Why do you need state management?
First, why do you need state management?
In my experience, this is because Flutter is based on
Declaratively build UI,
One of the purposes of using state management is to solve the problems posed by "declarative" development.
"Declarative" development is a way that is different from native, so we haven't heard of state management in native development, so how do we understand "declarative" development?
"Declarative" VS "imperative" analysis
Take the most classic counter example to analyze:
As shown in the figure above: Click the button in the lower right corner, and the displayed text number increases by one.
This can be achieved in Android: when the button in the lower right corner is clicked,
Get the object of TextView
and manually set the displayed text.
The implementation code is as follows:
// 一、定义展示的内容
private int mCount =0;
// 二、中间展示数字的控件 TextView
private TextView mTvCount;
// 三、关联 TextView 与 xml 中的组件
mTvCount = findViewById(R.id.tv_count)
// 四、点击按钮控制组件更新
private void increase( ){
mCount++;
mTvCounter.setText(mCount.toString());
}
In Flutter, we only need to call setState((){})
after increasing the variable. setState
will refresh the entire page so that the value displayed in the middle changes.
// 一、声明变量
int _counter =0;
// 二、展示变量
Text('$_counter')
// 三、变量增加,更新界面
setState(() {
_counter++;
});
It can be found that only the _counter
property has been modified in Flutter, and no operation has been performed on the Text component. The entire interface changes with the state.
So there is such a saying in Flutter: UI = f(state) :
In the above example, the state is the value of _counter
, call the setState
driver f
build method to generate a new UI.
So, what are the advantages of declarative, and what problems does it bring?
Advantages: Let developers get rid of the cumbersome control of components and focus on state processing
After getting used to Flutter development and returning to native platform development, you will find that it is very troublesome to control View when multiple components are related to each other.
In Flutter, we only need to deal with the state (complexity is transferred to the mapping of state -> UI, that is, the construction of Widget). The latest developments in technologies such as Jetpack Compose and Swift are also evolving in a "declarative" direction.
Problems with declarative development
When state management is not used and direct "declarative" development, there are three problems encountered:
- Logic and page UI are coupled, resulting in inability to reuse/unit test, modify confusion, etc.
- Difficulty accessing data across components (cross pages)
- Unable to easily control the refresh scope (changes in page setState will cause changes to the global page)
Next, I will lead you to understand these problems one by one, and the next chapter will describe in detail how the state management framework solves these problems.
1) Logic and page UI are coupled, resulting in inability to reuse/unit test, modify confusion, etc.
When the business was not complicated at the beginning, all the code was written directly into the widget. With the business iteration,
The file is getting bigger and bigger, and it is difficult for other developers to intuitively understand the business logic inside.
And some common logic, such as the processing of network request status, paging, etc., are pasted back and forth on different pages.
This problem also exists natively, and ideas such as the MVP design pattern have also been derived to solve it.
2) Difficulty accessing data across components (cross pages)
The second point is cross-component interaction, such as in the Widget structure,
A child component wants to display the name
field in the parent component,
It may be necessary to pass layer by layer.
Or to share filter data between two pages,
There is not a very elegant mechanism to solve this kind of cross-page data access.
3) The refresh range cannot be easily controlled (changes in page setState will cause changes in the global page)
The last question is also the advantage mentioned above. In many scenarios, we only modify part of the state, such as the color of the button.
But the entire page setState
will cause other places that don't need to be rebuilt,
bring unnecessary overhead.
Provider, Get state management framework design analysis
The core of the state management framework in Flutter lies in the solutions to these three problems,
Let's take a look at how Provider and Get are solved:
Solve the problem of coupling logic and page UI
This problem also exists in traditional native development. Activity files may also become difficult to maintain with iteration.
This problem can be decoupled through the MVP pattern.
Simply put, it is to extract the logic code in the View to the Presenter layer,
View is only responsible for the construction of the view.
This is also the solution to almost all state management frameworks in Flutter.
You can think of the Presenter in the above picture as GetController
in Get,
ChangeNotifier
Bloc
Bloc.
It is worth mentioning that Flutter and native MVP frameworks are different in specific ways.
We know that in the classic MVP model,
Generally View and Presenter define their own behavior (action) by interface,
Hold the interface to each other to call .
But this is not suitable for Flutter,
From the Presenter → View relationship, View corresponds to Widget in Flutter,
But in Flutter, Widgets are just user-declared UI configurations.
It is not good practice to directly control the Widget instance.
And on the relationship from View → Presenter,
Widget can indeed hold Presenter directly,
But this will bring about the problem of difficult data communication.
The solutions to this point are different for different state management frameworks. From the perspective of implementation, they can be divided into two categories:
- Solved by the Flutter tree mechanism , such as Provider;
- Via Dependency Injection , such as Get.
1) Handle the acquisition of V → P through the Flutter tree mechanism
abstract class Element implements BuildContext {
/// 当前 Element 的父节点
Element? _parent;
}
abstract class BuildContext {
/// 查找父节点中的T类型的State
T findAncestorState0fType<T extends State>( );
/// 遍历子元素的element对象
void visitChildElements(ElementVisitor visitor);
/// 查找父节点中的T类型的 InheritedWidget 例如 MediaQuery 等
T dependOnInheritedWidget0fExactType<T extends InheritedWidget>({
Object aspect });
……
}
<center> Element implements the method of manipulating the tree structure in the parent class BuildContext</center>
We know that there are three trees in Flutter, Widget, Element and RenderObject.
The so-called Widget tree is actually just a way of describing the nesting relationship of components, which is a virtual structure .
But Element and RenderObject actually exist at runtime,
You can see that the Element component contains the _parent
attribute, which stores its parent node.
And it implements the BuildContext
interface, including many methods for tree structure operations,
For example findAncestorStateOfType
, look up the parent node;
visitChildElements
Traverse child nodes.
In the starting example, we can pass context.findAncestorStateOfType
Find the required Element object layer by layer,
After obtaining the Widget or State, you can take out the required variables.
The provider also uses this mechanism to complete the acquisition of View -> Presenter.
Obtain the Present object in the top-level Provider component through Provider.of
.
Obviously, all the Widget nodes below the Provider,
can access the Presenter in the Provider through its own context,
A good solution to the problem of communication across components.
2) Solve V → P by means of dependency injection
The tree mechanism is nice, but it depends on the context, which can be maddening at times.
We know that Dart is a single-threaded model,
Therefore, there is no race condition for object access under multi-threading.
Based on this Get stores objects with the help of a global singleton Map.
By means of dependency injection, the access to the Presenter layer is achieved.
In this way, the Presenter can be obtained in any class.
The key corresponding to this Map is runtimeType
+ tag
,
Where tag is an optional parameter, and value corresponds to Object
,
That is to say, we can store any type of object and get it anywhere.
Solve the problem of difficult to access data across components (cross pages)
This question is basically similar to the thinking in the previous part, so we can summarize the characteristics of the two schemes:
Provider
- Dependency tree mechanism, must be based on context
- Provides the ability for subcomponents to access the upper layer
Get
- Global singleton, can be accessed anywhere
- There are types of duplication, memory recovery problems
Solve the problem of unnecessary refresh caused by high-level setState
Finally, the high-level we mentioned setState
caused the problem of unnecessary refresh,
Flutter solves the problem by adopting the observer pattern, and the key lies in two steps:
- The observer subscribes to the observed object;
- The observed object notifies the observer.
The system also provides the implementation of components such as ValueNotifier
:
/// 声明可能变化的数据
ValueNotifier<int> _statusNotifier = ValueNotifier(0);
ValueListenableBuilder<int>(
// 建立与 _statusNotifier 的绑定关系
valueListenable: _statusNotifier,
builder: (c, data, _) {
return Text('$data');
})
///数据变化驱动 ValueListenableBuilder 局部刷新
_statusNotifier.value += 1;
After understanding the most basic observer pattern, take a look at the components provided in the different frameworks:
For example, Provider provides ChangeNotifierProvider
:
class Counter extend ChangeNotifier {
int count = 0;
/// 调用此方法更新所有观察节点
void increment() {
count++;
notifyListeners();
}
}
void main() {
runApp(
ChangeNotifierProvider(
/// 返回一个实现 ChangeNotifier 接口的对象
create: (_) => Counter(),
child: const MyApp( ),
),
);
}
/// 子节点通过 Consumer 获取 Counter 对象
Consumer<Counter>(
builder:(_, counter, _) => Text(counter.count.toString())
Or the example of the previous counter, here Counter
inherited
ChangeNotifier
through the top-level Provider.
The child node can get the instance through the Consumer,
After calling the increment
method, only the corresponding Text component changes.
The same function, in Get,
Only need to call in advance Get.put
method storage Counter
object,
Specify Counter
as a generic for the GetBuilder
component.
Because Get is based on a singleton, so GetBuilder
can get the stored object directly through generics,
and exposed in the builder method. In this way Counter
establishes a listening relationship with the component,
After the change of Counter
, it will only drive the update of the GetBuilder
component that uses it as a generic type.
class Counter extends GetxController {
int count = 0;
void increase() {
count++;
update();
}
}
/// 提前进行存储
final counter = Get.put(Counter( ));
/// 直接通过泛型获取存储好的实例
GetBuilder<Counter>(
builder: (Counter counter) => Text('${counter.count}') );
Frequently Asked Questions in Practice
In the process of using these frameworks, you may encounter the following problems:
The context level in the provider is too high
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Provider(
create: (_) => const Count(),
child: MaterialApp(
home: Scaffold(
body: Center(child: Text('${Provider.of<Counter>(context).count}')),
),
),
);
}
}
As shown in the code, when we directly nest the Provider and the component at the same level,
At this time, the Provider.of(context)
in the code throws ProviderNotFoundException
at runtime.
Because the context we use here comes from MyApp,
But the element node of Provider is below MyApp,
So Provider.of(context)
can't get the Provider node.
There are two ways to modify this problem, as shown in the following code:
Modification 1: By nesting the Builder component, use the context of the child node to access:
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Provider(
create: (_) => const Count(),
child: MaterialApp(
home: Scaffold(
body: Center(
child: Builder(builder: (builderContext) {
return Text('${Provider.of<Counter>(builderContext).count}');
}),
),
),
),
);
}
}
Modification 2: Bring Provider to the top level:
void main() {
runApp(
Provider(
create: (_) => Counter(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(child: Text('${Provider.of<Counter>(context).count}')),
),
);
}
}
Get problems due to global singletons
As mentioned earlier, Get uses a global singleton to store objects by default with runtimeType
as the key.
In some scenarios, the obtained objects may not meet expectations, such as jumping between product detail pages.
Because different details page instances correspond to the same Class, that is, runtimeType
the same.
If the tag parameter is not added, calling Get.find
on a certain page will get the objects that have been stored in other pages.
At the same time, you must pay attention to the recycling of objects in Get, otherwise it is likely to cause memory leaks.
Or do it manually when the page dispose
delete
operation,
Either use the components provided in Get entirely, e.g. GetBuilder
,
It will be released in dispose
.
GetBuilder
in the dispose
stage:
@override
void dispose() {
super.dispose();
widget.dispose?.call(this);
if (_isCreator! || widget.assignId) {
if (widget.autoRemove && GetInstance().isRegistered<T>(tag: widget.tag)) {
GetInstance().delete<T>(tag: widget.tag);
}
}
_remove?.call();
controller = null;
_isCreator = null;
_remove = null;
_filter = null;
}
Summary of the advantages and disadvantages of Get and Provider
Through this article, I introduced you to the necessity of state management, what problems it solves in Flutter development, and how to solve them.
At the same time, I have also summarized the common problems in practice for everyone. You may still have some doubts here. Do you need to use state management?
In my opinion, frameworks exist to solve problems. So it depends on whether you are also going through those questions that were posed at the beginning.
If there is, then you can try to solve it with state management; if not, there is no need to overdesign, use it for the sake of use.
Second, if using state management, which is better, Get or Provider?
Both frameworks have their pros and cons, I think if you or your team is new to Flutter,
Using Provider can help you understand the core mechanism of Flutter faster.
And if you already have an understanding of the principles of Flutter, Get's rich functions and concise API,
It can help you improve the development efficiency very well.
Thanks to community members Alex, Luke, Lynn, Ming for their contributions to this article.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。