Principle Analysis of Provider and Get in Flutter State Management Framework

Why is state management needed?

First of all, why state management is needed? This is because Flutter builds UI based on  declarative style  . One of the purposes of using state management is to solve the problems caused by "declarative" development.

"Declarative" development is a way that is different from native development, so we have never heard of state management in native development. How do we understand "declarative" development?

"Declarative" VS "Imperative" analysis

Analyze with the most classic counter example

// 一、定义展示的内容
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 after the variable is incremented  setState((){}) . setState The entire page will be refreshed, causing the value displayed in the middle to change.

// 一、声明变量
int _counter =0; 

// 二、展示变量 
Text('$_counter')

//  三、变量增加,更新界面
setState(() {
   _counter++; 
});

It can be found that only  _counter the properties are modified in Flutter, and no operations are performed on the Text component. The entire interface changes as the state changes.

So there is such a saying in Flutter:  UI = f(state) :

 

In the above example, the state is  _counter the value of , and  setState the driver  f build method is called 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, return to native platform development, you will find that when multiple components are related to each other, it is very troublesome to control View.

In Flutter, we only need to deal with the state (the complexity is transferred to the mapping of state -> UI, that is, the construction of Widget). The latest developments including Jetpack Compose, Swift and other technologies are also evolving in the direction of "declarative".

Problems with declarative development

When developing directly without using state management, there are three problems encountered:

  1. The logic is coupled with the page UI, resulting in the inability to reuse/unit test, modification confusion, etc.
  2. Difficult to access data across components (cross pages)
  3. Unable to easily control the refresh range (the change of the page setState will cause the change of the global page)

Next, I will lead you to understand these problems one by one. The next chapter will describe in detail how the state management framework solves these problems.

1) The logic is coupled with the UI of the page, resulting in the inability to reuse/unit test, modification confusion, etc.

When the business was not complicated at the beginning, all the codes were written directly into the widget. As the business iterated, the files became larger and larger, making it difficult for other developers to intuitively understand the business logic inside. And some general logic, such as the processing of network request status, paging, etc., is pasted back and forth on different pages.

This problem also exists in the original, and ideas such as the MVP design pattern were derived later to solve it.

2) Difficult to access data across components (cross pages)

The second point is cross-component interaction. For example, in the Widget structure, if a child component wants to display the fields in the parent component  name , it may need to be passed layer by layer.

Or it is necessary to share screening data between two pages, and there is no very elegant mechanism to solve this cross-page data access.

3) It is impossible to easily control the refresh range (the change of page setState will lead to the change of the global page)

The last problem 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 do not need to be changed to be rebuilt, bringing 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 solve them:

Solve the coupling problem of logic and page UI

This problem also exists in traditional native development. Activity files may also become difficult to maintain with iterations. This problem can be decoupled through the MVP model.

Simply put, it is to extract the logic code in the View to the Presenter layer, and the View is only responsible for the construction of the view

 

This is also the solution for almost all state management frameworks in Flutter. You can think of the Presenter in the above picture as being in Get  GetController, Provider  ChangeNotifier or Bloc  Bloc. It is worth mentioning that the specific approach is different between Flutter and the native MVP framework.

We know that in the classic MVP mode, generally View and Presenter define their own behavior (action) with interfaces, and  hold interfaces to call each other  .

 

However, this is not suitable for Flutter. From the perspective of the Presenter → View relationship, View corresponds to Widget in Flutter, but in Flutter, Widget is only the configuration of the UI declared by the user, and it is not a good practice to directly control the Widget instance.

In the relationship from View → Presenter, Widget can indeed hold the Presenter directly, but this will bring the problem of difficult data communication .

Different state management frameworks have different solutions to this point. From the perspective of implementation, they can be divided into two categories:

  • Solved by the Flutter tree mechanism, such as Provider;
  • Through dependency injection, such as Get.

1) Process 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 });
	……
} 

 

Element implements the method of manipulating the tree structure in the parent class BuildContext

We know that there are three trees in Flutter, Widget, Element and RenderObject. The so-called  Widget tree is actually just a way for us to describe the nesting relationship of components, and it is a virtual structure . However, Element and RenderObject actually exist at runtime. You can see that the Element component contains  _parent properties to store its parent node. And it implements  BuildContext the interface, including many methods for tree structure operations, for example  findAncestorStateOfType, looking up the parent node;  visitChildElements traversing the child nodes.

In the initial example, we can   find the required Element objects layer by layer, obtain the Widget or State, and then retrieve the required variables.context.findAncestorStateOfType

 

The provider also uses this mechanism to complete the acquisition of View -> Presenter. By  Provider.of getting the Present object in the top-level Provider component. Obviously, all Widget nodes below the Provider can access the Presenter in the Provider through their own context, which solves the problem of cross-component communication well.

2) Solve V → P through dependency injection

The tree mechanism is nice, but depends on the context, which can be maddening at times. We know that Dart is a single-threaded model, so there is no race condition for object access under multi-threading. Based on this Get, use a global singleton Map to store objects. Through the way of dependency injection, the acquisition of the Presenter layer is realized. 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  Object, that is to say, we can store any type of object and get it at any position.

Solve the problem that it is difficult to access data across components (cross pages)

This problem is basically similar to the thinking in the previous part, so we can summarize the characteristics of the two solutions:

Provider 

  • Dependency tree mechanism must be based on context
  • Provides the ability for subcomponents to access the upper layer

Get

  • Global singleton, accessible anywhere
  • There are duplication of types and memory recovery problems

 

Solve the problem of unnecessary refresh caused by high-level setState

Finally,  setState the problem of unnecessary refresh caused by the high-level we mentioned, Flutter solves it by adopting the observer mode, the key lies in two steps:

  1. The observer subscribes to the observed object;
  2. The observed object notifies the observer.

 

The system also provides  ValueNotifier the implementation of other components: 

/// 声明可能变化的数据
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, look at the components provided in 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()) 

Still the previous counter example, here  Counter it is inherited  ChangeNotifier through the top-level Provider for storage. The child node can get the instance through Consumer, after calling  increment the method, only the corresponding Text component will change.

The same function, in Get, only needs to call  Get.put the method storage  Counter object in advance, and  GetBuilder specify it as a generic type for the component  Counter . Because Get is based on a singleton,  GetBuilder the stored object can be directly obtained through generics and exposed in the builder method. In this  Counter way, a monitoring relationship is established with the component, and subsequent  Counter changes will only drive  GetBuilder the update of the component that uses it as a generic.

 

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 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, the  Provider.of(context) runtime in the code throws  it ProviderNotFoundException. Because the context we use here comes from MyApp, but the Provider element node is located below MyApp, so the  Provider.of(context) Provider node cannot be obtained. There are two ways to fix this problem, as shown in the following code:

Improvement 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}');
            }),
          ),
        ),
      ),
    );
  }
}

Improvement 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 has problems with global singletons

As mentioned earlier, Get uses a global singleton, which stores objects as keys runtimeType by . In some scenarios, the obtained objects may not meet expectations, such as jumping between product detail pages. Since different details page instances correspond to the same Class, they are  runtimeType the same. If you do not add the tag parameter, calling on a certain page  Get.find 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. Either  dispose do it manually at page time  delete , or fully use the component provided in Get, for example  GetBuilder, it will  dispose release in .

 

 GetBuilder Recycling is carried out in  dispose phases:

@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 advantages and disadvantages of Get and Provider

Through this article, I introduced to you the necessity of state management, which problems it solves in Flutter development and how to solve them. At the same time, I also summarized common problems in practice, etc., see here You may still have some doubts, 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 issues that were raised at the beginning. If there is, then you can try to use state management to solve it; if not, there is no need to over-design it, use it for use.

Second, if state management is used, which is better, Get or Provider?

These two frameworks have their own advantages and disadvantages. I think that if you or your team are 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 can help you improve your development efficiency.

 

Guess you like

Origin blog.csdn.net/RreamigOfGirls/article/details/130323536