Flutter 状态管理框架 Provider 和 Get 分析

在这里插入图片描述

状态管理一直是 Flutter 开发中一个火热的话题。谈到状态管理框架,社区也有诸如有以GetProvider为代表的多种方案,它们有各自的优缺点。面对这么多的选择,你可能会想:「我需要使用状态管理么?哪种框架更适合我?本文将从作者的实际开发经验出发,分析状态管理解决的问题以及思路,希望能帮助你做出选择。

为什么需要状态管理?

首先,为什么需要状态管理?根据笔者的经验,这是因为 Flutter 基于声明式构建 UI ,使用状态管理的目的之一就是解决声明式开发带来的问题。

声明式开发是一种区别于传原生的方式,所以我们没有在原生开发中听到过状态管理,那如何理解声明式开发呢?

声明式 VS 命令式分析,以最经典的的计数器例子分析:
在这里插入图片描述
如上图所示:点击右下角按钮,显示的文本数字加一。Android 中可以这么实现:当右下角按钮点中时,拿到 TextView 的对象,手动设置其展示的文本。

实现代码如下:

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

而在 Flutter 中,我们只需要使变量增加之后调用 setState((){}) 即可。setState 会刷新整个页面,使得中间展示的值进行变更。

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

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

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

可以发现,Flutter 中只对 _counter 属性进行了修改,并没有对 Text 组件进行任何的操作,整个界面随着状态的改变而改变。

所以在 Flutter 中有这么一种说法: UI = f(state):
在这里插入图片描述
上面的例子中,状态 (state) 就是 _counter 的值,调用 setState 驱动 f build 方法生成新的 UI。

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

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

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

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

声明式开发带来的问题

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

逻辑和页面 UI 耦合,导致无法复用/单元测试、修改混乱等
难以跨组件 (跨页面) 访问数据
无法轻松的控制刷新范围 (页面 setState 的变化会导致全局页面的变化)
接下来,我先带领大家逐个了解这些问题,下一章向大家详细描述状态管理框架如何解决这些问题。

1) 逻辑和页面 UI 耦合,导致无法复用/单元测试、修改混乱等
在这里插入图片描述
一开始业务不复杂的时候,所有的代码都直接写到 widget 中,随着业务迭代,文件越来越大,其他开发者很难直观地明白里面的业务逻辑。并且一些通用逻辑,例如网络请求状态的处理、分页等,在不同的页面来回粘贴。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

通过 Flutter 树机制 解决,例如 Provider
通过 依赖注入,例如 Get

1) 通过 Flutter 树机制处理 V → P 的获取

Element 实现了父类 BuildContext 中操作树结构的方法:
在这里插入图片描述

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

我们知道 Flutter 中存在三棵树,WidgetElementRenderObject。所谓的 Widget 树其实只是我们描述组件嵌套关系的一种说法,是一种虚拟的结构。但 ElementRenderObject 在运行时实际存在,可以看到 Element 组件中包含了 _parent 属性,存放其父节点。而它实现了 BuildContext 接口,包含了诸多对于树结构操作的方法,例如 findAncestorStateOfType,向上查找父节点;visitChildElements 遍历子节点。

在一开始的例子中,我们可以通过 context.findAncestorStateOfType一层一层地向上查找到需要的 Element 对象,获取 Widget 或者 State 后即可取出需要的变量。

在这里插入图片描述
provider 也是借助了这样的机制,完成了 View -> Presenter 的获取。通过 Provider.of 获取顶层 Provider 组件中的 Present 对象。显然,所有 Provider 以下的 Widget 节点,都可以通过自身的 context 访问到 Provider 中的 Presenter,很好地解决了跨组件的通信问题。

2) 通过依赖注入的方式解决 V → P

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

在这里插入图片描述
这个 Map 对应的 keyruntimeType + 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);

  
  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);

  
  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);

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(child: Text('${Provider.of<Counter>(context).count}')),
      ),
    );
  }
}

Get 由于全局单例带来的问题

正如前面提到 Get 通过全局单例,默认以 runtimeTypekey 进行对象的存储,部分场景可能获取到的对象不符合预期,例如商品详情页之间跳转。由于不同的详情页实例对应的是同一 Class,即 runtimeType 相同。如果不添加 tag 参数,在某个页面调用 Get.find 会获取到其它页面已经存储过的对象。同时 Get 中一定要注意考虑到对象的回收,不然很有可能引起内存泄漏。要么手动在页面 dispose 的时候做 delete 操作,要么完全使用 Get 中提供的组件,例如 GetBuilder,它会在 dispose 中释放。

在这里插入图片描述
GetBuilder 中在 dispose 阶段进行回收:


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

Get 与 Provider 优缺点总结

通过本文,我向大家介绍了状态管理的必要性、它解决了 Flutter 开发中的哪些问题以及是如何解决的,与此同时,我也为大家总结了在实践中常见的问题等,看到这里你可能还会有些疑惑,到底是否需要使用状态管理?

在我看来,框架是为了解决问题而存在。所以这取决于你是否也在经历一开始提出的那些问题。如果有,那么你可以尝试使用状态管理解决;如果没有,则没必要过度设计,为了使用而使用。

其次,如果使用状态管理,那么 GetProvider 哪个更好?

这两个框架各有优缺点,我认为如果你或者你的团队刚接触 Flutter,使用 Provider 能帮助你们更快理解 Flutter 的核心机制。而如果已经对 Flutter 的原理有了解,Get 丰富的功能和简洁的 API,则能帮助你很好地提高开发效率。

参考资料

[1]Get: https://pub.flutter-io.cn/packages/get
[2]Provider: https://pub.flutter-io.cn/packages/provider
[3]声明式: https://flutter.cn/docs/resources/architectural-overview#reactive-user-interfaces

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/37673.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

集群基础1——集群概念、LVS负载均衡

文章目录 一、基本了解二、LVS负载均衡2.1 基本了解2.2 工作模式2.2.1 NAT模式2.2.2 DR模式2.2.3 LVS-TUN模式2.2.4 LVS-FULLNAT模式 三、调度器算法四、ipvsadm命令 一、基本了解 什么是集群&#xff1f; 多台服务器做同一件事情。 集群扩展方式&#xff1a; scale up&#xf…

每日科技分享-POE新增文件和链接发送功能

POE推出新功能 注意POE需要魔法上午才能进去。 实测 实测可以发送论文给chatgpt&#xff0c;然后和AI进行共享的对话。 POE网站链接&#xff1a; 也可以发送链接&#xff0c;实测了一下&#xff0c;似乎有时候并不准确&#xff0c;我发送了关于分层强化的文章&#xff0c;但是…

05 Docker 安装常用软件 (mongoDB)

目录 1. mongoDB简介 1.1 mongodb的优势 2. mongodb的安装 2.1 创建数据文件夹 2.2 备份日志 2.3 配置文件夹 2.4 创建两个文件 ---> 2.4.1 配置如下: 2.5 拉取mongodb 2.6 运行容器 2.7 进入mongodb容器 ---> 2.7.0 高版本(6.0)以上是这样的 , 旧版的没研究 …

SpringBoot 集成 Mybatis

SpringBoot 集成 Mybatis 详细教程 &#xff08;只有操作&#xff0c;没有理论&#xff0c;仅供参考学习&#xff09; 一、操作部分 1. 准备数据库 1.1 数据库版本&#xff1a; C:\WINDOWS\system32>mysql -V mysql Ver 8.0.25 for Win64 on x86_64 (MySQL Community …

PyTorch深度学习实战(5)——计算机视觉

PyTorch深度学习实战&#xff08;5&#xff09;——计算机视觉 0. 前言1. 图像表示2. 将图像转换为结构化数组2.1 灰度图像表示2.2 彩色图像表示 3 利用神经网络进行图像分析的优势小结系列链接 0. 前言 计算机视觉是指通过计算机系统对图像和视频进行处理和分析&#xff0c;利…

笔记本电脑清灰换硅脂

文章目录 一、完整过程0.准备工具1.拆笔记本后盖2.洗手擦干断电3.清理部件浮尘4.拆风扇5.拆散热模具6.换硅脂7.装回去 二、图片 一、完整过程 0.准备工具 拆机螺丝刀、硅脂、撬片/撬棒、毛刷、气吹、卫生纸。 正常电脑是十字螺丝&#xff0c;推荐刀头使用 PH00 或 PH0。 1.拆…

基于单片机的智能太阳能手机充电器的设计与实现

功能介绍 以STM32/51单片机作为主控系统&#xff1b;LCD1602液晶显示当前电压值&#xff1b;太阳能电池板采集当前光照转换为电能&#xff0c;然后TP4056锂电池充放电模块给锂电池进行充电&#xff0c;充完后自动断电&#xff0c;防过充&#xff1b;通过CE8301模块对锂电池电压…

1-4 架构师所需要具备的技术栈与能力

架构师所需要具备的技术栈与能力 全局图解 全局图解

CSS整段文字缩进(一段多行文字中首列位置相对应)

<style>p {text-align: justify;padding-left: 2em;} </style>

学习系统编程No.28【多线程概念实战】

引言&#xff1a; 北京时间&#xff1a;2023/6/29/15:33&#xff0c;刚刚更新完博客&#xff0c;目前没什么状态&#xff0c;不好趁热打铁&#xff0c;需要去睡一会会&#xff0c;昨天睡的有点迟&#xff0c;然后忘记把7点到8点30之间的4个闹钟关掉了&#xff0c;恶心了我自己…

基于单片机智能饮水机加热系统的设计与实现

功能介绍 以51单片机作为主控系统&#xff1b;LCD1602液晶显示当前水温&#xff0c;定时提醒&#xff0c;水量变化DS18B20检测当前水体温度&#xff1b;水位传感器检测当前水位&#xff1b;继电器驱动加热片进行水温加热&#xff1b;定时提醒喝水&#xff0c;蜂鸣器报警&#x…

Java-通过IP获取真实地址

文章目录 前言功能实现测试 前言 最近写了一个日志系统&#xff0c;需要通过访问的 IP 地址来获取真实的地址&#xff0c;并且存到数据库中&#xff0c;我也是在网上看了一些文章&#xff0c;遂即整理了一下供大家参考。 功能实现 这个是获取正确 IP 地址的方法&#xff0c;可…

【css】用css样式快速写右上角badge徽标,颜色设置为渐变色

先看效果展示&#xff0c;已公开显示在图片卡片的右上角。 首先是dom代码&#xff1a;需要两个view或者div&#xff0c;public-badge是“已公开”那个矩形&#xff0c;show-signal是右边那个下三角&#xff0c;也就是阴影部分&#xff0c;这样看起来比较有立体感。 <view…

LabVIEW-实现波形发生器

一、题目 用两种方法实现一种多类型信号波形发生器&#xff08;至少包括&#xff1a;正弦波、三角波、方波等&#xff09;&#xff0c;可以调节信号频率、幅度、相位等参数&#xff0c;可以图形化显示信号波形。 需要给出产生信号波形的基本方法、程序设计基本方法以及程序实现…

云计算的学习(二)

二、计算虚拟化 1.计算虚拟化的介绍 1.1虚拟化简介 a.什么是虚拟化 将物理设备逻辑化&#xff0c;转化成文件或者文件夹&#xff0c;这个文件或文件夹一定包含两个部分&#xff1a;一部分用于记录设备配置信息&#xff0c;另一部分记录用户数据。 虚拟机摆脱了服务器的禁锢…

性能测试工具 Jmeter 测试 JMS (Java Message Service)/ActiveMQ 性能

目录 前言 ActiveMQ 介绍 准备工作 编写jndi.properties添加到ApacheJMeter.jar 中 下载 ActiveMQ 配置 Jmeter 进行测试 点对点 (Queues 队列) 配置 Jmeter 进行测试 发布/订阅 (Topic 队列) 配置发布 Publisher 配置订阅 Subscriber 总结 前言 JMeter是一个功能强大…

全方位对比 Postgres 和 MySQL (2023 版)

根据 2023 年 Stack Overflow 调研&#xff0c;Postgres 已经取代 MySQL 成为最受敬仰和渴望的数据库。 随着 Postgres 的发展势头愈发强劲&#xff0c;在 Postgres 和 MySQL 之间做选择变得更难了。 如果看安装数量&#xff0c;MySQL 可能仍是全球最大的开源数据库。 Postgre…

Windows环境下安装Nacos

文章目录 一、什么是Nacos1. 主要特点&#xff1a;1.1 服务发现和注册&#xff1a;1.2 配置管理&#xff1a;1.3 服务管理&#xff1a;1.4 多语言支持&#xff1a;1.5 高可用性和扩展性&#xff1a; 二、Windows下安装单机版Nacos1. 安装包下载&#xff1a;2. 目录文件说明&…

冒泡排序模拟实现qsort()函数

冒泡排序模拟实现qsort函数 前言1. 分析2. 解决一&#xff0c;如何接受不同数据3. 解决二&#xff0c;如何实现不同数据的比较4. 解决三&#xff0c;如何实现不同数据交换5. 模拟bubble_sort&#xff08;&#xff09;函数排序整型所有代码实现6. 结构体排序实现7. 结尾 前言 要…

Android Studio无法打开问题解决记录

目录 1 问题起因2 发现问题3 解决问题 1 问题起因 问题的起因是我为了运行一个Kotlin项目&#xff0c;但是报了一个错误&#xff1a; Kotlin报错The binary version of its metadata is 1.5.1, expected version is 1.1.16 然后我就上百度去搜了以下&#xff0c;一篇博客让禁用…