Cadre de gestion de l'état de flottement Fournisseur et analyse Get

Posté par Nayuta, Communauté CFUG

La gestion des états a toujours été un sujet brûlant dans le développement de Flutter. En ce qui concerne le cadre de gestion de l'état, la communauté dispose également de divers schémas représentés par Get et Provider , et ils ont leurs propres avantages et inconvénients. Face à tant de choix, vous pouvez penser : " Ai-je besoin d'utiliser la gestion d'état ? Quel framework me convient le mieux ?" Cet article partira de l'expérience de développement réelle de l'auteur, analysera les problèmes et les idées résolus par la gestion d'état, et J'espère vous aider à faire un choix.

Pourquoi avez-vous besoin d'une gestion d'état ?

Tout d'abord, pourquoi avez-vous besoin d'une gestion d'état ? Selon l'expérience de l'auteur, cela est dû au fait que Flutter construit une interface utilisateur basée sur le déclaratif, et l'un des objectifs de l'utilisation de la gestion d'état est de résoudre les problèmes causés par le développement "déclaratif".

Le développement "déclaratif" est différent du développement natif, nous n'avons donc pas entendu parler de la gestion de l'état dans le développement natif, alors comment comprenons-nous le développement "déclaratif" ?

Analyse "déclarative" VS "impérative"

Prenons le contre-exemple le plus classique pour analyser :

Comprendre le "déclaratif" et "l'impératif" de Flutter via l'application de compteur

Comme indiqué dans la figure ci-dessus : Cliquez sur le bouton dans le coin inférieur droit et le numéro de texte affiché augmente d'une unité. Cela peut être implémenté dans Android: lorsque le bouton dans le coin inférieur droit est cliqué, TextViewl' objet obtenu, définissez manuellement le texte affiché.

Le code d'implémentation est le suivant :

// 一、定义展示的内容
private int mCount =0;
 
// 二、中间展示数字的控件 TextView
private TextView mTvCount;
 
// 三、关联 TextView 与 xml 中的组件
mTvCount = findViewById(R.id.tv_count)
 
// 四、点击按钮控制组件更新
private void increase( ){ 
	mCount++;
	mTvCounter.setText(mCount.toString()); 
}

复制代码

Dans Flutter, nous n'avons besoin setState((){})d' . setStateLa page entière sera actualisée, ce qui entraînera la modification de la valeur affichée au milieu.

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

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

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

On peut constater que seule la _counterpropriété et qu'aucune opération n'est effectuée sur le composant Text, toute l'interface change avec le changement d'état.

Il y a donc un tel dicton dans Flutter : UI = f(state) :

上面的例子中,状态 (state) 就是 _counter 的值,调用 setState 驱动 f build 方法生成新的 UI。

那么,声明式有哪些优势,并带来了哪些问题呢?

优势: 让开发者摆脱组件的繁琐控制,聚焦于状态处理

习惯 Flutter 开发之后,回到原生平台开发,你会发现当多个组件之间相互关联时,对于 View 的控制非常麻烦。

而在 Flutter 中我们只需要处理好状态即可 (复杂度转移到了状态 -> UI 的映射,也就是 Widget 的构建)。包括 Jetpack Compose、Swift 等技术的最新发展,也是在朝着「声明式」的方向演进。

声明式开发带来的问题

没有使用状态管理,直接「声明式」开发的时候,遇到的问题总结有三个:

  1. 逻辑和页面 UI 耦合,导致无法复用/单元测试、修改混乱等
  2. 难以跨组件 (跨页面) 访问数据
  3. 无法轻松的控制刷新范围 (页面 setState 的变化会导致全局页面的变化)

接下来,我先带领大家逐个了解这些问题,下一章向大家详细描述状态管理框架如何解决这些问题。

1) 逻辑和页面 UI 耦合,导致无法复用/单元测试、修改混乱等

一开始业务不复杂的时候,所有的代码都直接写到 widget 中,随着业务迭代, 文件越来越大,其他开发者很难直观地明白里面的业务逻辑。 并且一些通用逻辑,例如网络请求状态的处理、分页等,在不同的页面来回粘贴。

这个问题在原生上同样存在,后面也衍生了诸如 MVP 设计模式的思路去解决。

2) 难以跨组件 (跨页面) 访问数据

第二点在于跨组件交互,比如在 Widget 结构中, 一个子组件想要展示父组件中的 name 字段, 可能需要层层进行传递。

又或者是要在两个页面之间共享筛选数据, 并没有一个很优雅的机制去解决这种跨页面的数据访问。

3) 无法轻松的控制刷新范围 (页面 setState 的变化会导致全局页面的变化)

最后一个问题也是上面提到的优点,很多场景我们只是部分状态的修改,例如按钮的颜色。 但是整个页面的 setState 会使得其他不需要变化的地方也进行重建, 带来不必要的开销。

Provider、Get 状态管理框架设计分析

Flutter 中状态管理框架的核心在于这三个问题的解决思路, 下面一起看看 Provider、Get 是如何解决的:

解决逻辑和页面 UI 耦合问题

传统的原生开发同样存在这个问题,Activity 文件也可能随着迭代变得难以维护, 这个问题可以通过 MVP 模式进行解耦。

简单来说就是将 View 中的逻辑代码抽离到 Presenter 层, View 只负责视图的构建。

这也是 Flutter 中几乎所有状态管理框架的解决思路, 上图的 Presenter 你可以认为是 Get 中的 GetController、 Provider 中的 ChangeNotifier 或者 Bloc 中的 Bloc。 值得一提的是,具体做法上 Flutter 和原生 MVP 框架有所不同。

我们知道在经典 MVP 模式中, 一般 View 和 Presenter 以接口定义自身行为 (action), 相互持有接口进行调用

但 Flutter 中不太适合这么做, 从 Presenter → View 关系上 View 在 Flutter 中对应 Widget, 但在 Flutter 中 Widget 只是用户声明 UI 的配置, 直接控制 Widget 实例并不是好的做法。

而在从 View → Presenter 的关系上, Widget 可以确实可以直接持有 Presenter, 但是这样又会带来难以数据通信的问题。

这一点不同状态管理框架的解决思路不一样,从实现上他们可以分为两大类:

  • Résolu par le mécanisme d'arborescence Flutter , tel que Provider;
  • Via Dependency Injection , comme Get.

1) Gérer l'acquisition de V → P via le mécanisme Flutter Tree

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 implémente la méthode de manipulation de l'arborescence dans la classe parent BuildContext

Nous savons qu'il existe trois arbres dans Flutter, Widget, Element et RenderObject. Le soi-disant arbre Widget est en fait juste une façon de décrire la relation d'imbrication des composants, qui est une structure virtuelle . Mais Element et RenderObject existent réellement au moment de l'exécution.Vous pouvez voir que le composant Element contient des _parentpropriétés pour stocker ses nœuds parents. Et il implémente l' BuildContextinterface , qui contient de nombreuses méthodes pour les opérations de structure arborescente, telles que la findAncestorStateOfTyperecherche de nœuds parents et la visitChildElementstraversée de nœuds enfants.

Dans le premier exemple, nous pouvons trouver l'objet Element requis couche par context.findAncestorStateOfTypecouche et obtenir la variable requise après avoir obtenu le Widget ou l'État.

Le fournisseur utilise également ce mécanisme pour compléter l'acquisition de View -> Presenter. Provider.ofObtenez l'objet Present dans le composant Provider de niveau supérieur via . De toute évidence, tous les nœuds Widget sous le fournisseur peuvent accéder au présentateur dans le fournisseur via leur propre contexte, ce qui résout bien le problème de la communication entre composants.

2) Résoudre V → P au moyen de l'injection de dépendance

树机制很不错,但依赖于 context,这一点有时很让人抓狂。 我们知道 Dart 是一种单线程的模型, 所以不存在多线程下对于对象访问的竞态问题。 基于此 Get 借助一个全局单例的 Map 存储对象。 通过依赖注入的方式,实现了对 Presenter 层的获取。 这样在任意的类中都可以获取到 Presenter。

这个 Map 对应的 key 是 runtimeType + tag, 其中 tag 是可选参数,而 value 对应 Object, 也就是说我们可以存入任何类型的对象,并且在任意位置获取。

解决难以跨组件 (跨页面) 访问数据的问题

这个问题其实和上一部分的思考基本类似,所以我们可以总结一下两种方案特点:

Provider

  • 依赖树机制,必须基于 context
  • 提供了子组件访问上层的能力

Get

  • 全局单例,任意位置可以存取
  • 存在类型重复,内存回收问题

解决高层级 setState 引起不必要刷新的问题

最后就是我们提到的高层级 setState 引起不必要刷新的问题, Flutter 通过采用观察者模式解决,其关键在于两步:

  1. 观察者去订阅被观察的对象;
  2. 被观察的对象通知观察者。

系统也提供了 ValueNotifier 等组件的实现:

/// 声明可能变化的数据
ValueNotifier<int> _statusNotifier = ValueNotifier(0); 

ValueListenableBuilder<int>(
	// 建立与 _statusNotifier 的绑定关系 
	valueListenable: _statusNotifier, 
	builder: (c, data, _) {
		return Text('$data'); 
})

///数据变化驱动 ValueListenableBuilder 局部刷新 
_statusNotifier.value += 1;

复制代码

了解到最基础的观察者模式后,看看不同框架中提供的组件:

比如 Provider 中提供了 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()) 

复制代码

还是之前计数器的例子,这里 Counter 继承了 ChangeNotifier 通过顶层的 Provider 进行存储。 子节点通过 Consumer 即可获取实例, 调用了 increment 方法之后,只有对应的 Text 组件进行变化。

同样的功能,在 Get 中, 只需要提前调用 Get.put 方法存储 Counter 对象, 为 GetBuilder 组件指定 Counter 作为泛型。 因为 Get 基于单例,所以 GetBuilder 可以直接通过泛型获取到存入的对象, 并在 builder 方法中暴露。这样 Counter 便与组件建立了监听关系, 之后 Counter 的变动,只会驱动以它作为泛型的 GetBuilder 组件更新。

class Counter extends GetxController { 
	int count = 0;

	void increase() { 
		count++;
		update(); 
	}
}

/// 提前进行存储
final counter = Get.put(Counter( )); 

/// 直接通过泛型获取存储好的实例
GetBuilder<Counter>(
	builder: (Counter counter) => Text('${counter.count}') ); 

复制代码

实践中的常见问题

在使用这些框架过程中,可能会遇到以下的问题:

Provider 中 context 层级过高

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}')),
        ),
      ),
    );
  }
}
复制代码

如代码所示,当我们直接将 Provider 与组件嵌套于同一层级时, 这时代码中的 Provider.of(context) 运行时抛出 ProviderNotFoundException。 因为此处我们使用的 context 来自于 MyApp, 但 Provider 的 element 节点位于 MyApp 的下方, 所以 Provider.of(context) 无法获取到 Provider 节点。 这个问题可以有两种改法,如下方代码所示:

改法 1: 通过嵌套 Builder 组件,使用子节点的 context 访问:

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}');
            }),
          ),
        ),
      ),
    );
  }
}
复制代码

改法 2: 将 Provider 提至顶层:

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 由于全局单例带来的问题

Comme mentionné précédemment, Get utilise un singleton global pour stocker des objets runtimeTypecomme certains scénarios, les objets obtenus peuvent ne pas répondre aux attentes, comme le saut entre les pages de détails du produit. Étant donné que différentes instances de page de détails correspondent à la même classe, c'est-à-dire au runtimeTypemême fichier . Si le paramètre tag n'est pas ajouté, l'appel sur une certaine Get.findpage obtiendra les objets qui ont été stockés dans d'autres pages. En parallèle, il faut faire attention au recyclage des objets dans Get, sinon cela risque de provoquer des fuites mémoire. Faites-le manuellement au moment de la disposepage delete, ou utilisez complètement le composant fourni dans Get , par exemple GetBuilder, il disposesera publié dans .

GetBuilderRecyclage en disposephase :

@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;
}

复制代码

Résumé des avantages et inconvénients de Get et Provider

A travers cet article, je vous ai présenté la nécessité de la gestion d'état, quels problèmes elle résout dans le développement de Flutter et comment les résoudre. En même temps, je vous résume également les problèmes courants en pratique, voir ici Vous pouvez encore avoir quelques doutes, avez-vous besoin d'utiliser la gestion d'état ?

Selon moi, les frameworks existent pour résoudre les problèmes. Cela dépend donc si vous passez également par ces questions qui ont été posées au début. Si c'est le cas, vous pouvez essayer de le résoudre avec la gestion d'état ; sinon, il n'est pas nécessaire de surconcevoir, utilisez-le pour le plaisir d'utilisation.

Deuxièmement, si vous utilisez la gestion d'état, qu'est-ce qui est le mieux, Get ou Provider ?

Ces deux frameworks ont leurs propres avantages et inconvénients.Je pense que si vous ou votre équipe êtes nouveau dans Flutter, l'utilisation de Provider peut vous aider à comprendre plus rapidement le mécanisme de base de Flutter. Si vous avez déjà une compréhension des principes de Flutter, les fonctions riches et l'API concise de Get peuvent vous aider à améliorer l'efficacité du développement.

Merci aux membres de la communauté Alex, Luke, Lynn, Ming pour leurs contributions à cet article.

Je suppose que tu aimes

Origine juejin.im/post/7094520232575762446
conseillé
Classement