一篇文章,告别Flutter状态管理争论,问题和解决

起因

每隔一段时间,都会出现一个新的状态管理框架,最近在YouTube上也发现了有人在推signals, 一个起源于React的状态管理框架,人们总是乐此不疲的发明各种好用或者为了解决特定问题而产生的方案,比如Bloc, 工具会推陈出新,新的语法会带来更便捷的方式,但原理和优缺点是更重要的一面,我们接下来聊聊这一点。

原理

状态管理的起点是值的改变也就是通常代码中的set方法, 状态的终点在Flutter或者其他UI框架中,对单个绘制节点进行setDirty来标记一个需要重新绘制节点,最后生成新的持续帧来承接新的数据,在Flutter中通过setState 来实现。当我们明确了状态管理的起点终点, 中间对于值的操作和缓存,diff算法,状态传递,状态转移过程等是这些状态管理框架们根据要解决的实际问题来做的权衡(trade off)。

  • figure 1 状态管理的空间

state_manage_space.png

需要权衡什么?

易用性

在Flutter开发中,有一句著名的话你可以不了解Flutter,但不能不知Getx, 这点很像Java服务端的spring框架, 提供了一套完备的开发工具,也许不是最合适于特定项目,但一定合适于简单项目。那Getx的权衡是什么? 笔者觉得托管,完备简单,是Getx的最大权衡,比如,你需要全局使用Getx的组件,来包裹所有的Widget,这样Getx就可以托管你的程序,依赖注入,主题,屏幕尺寸,路由都可以被Getx管理。比如简单的Obx函数将StatefulWidget封装,通过全局的NotifyManager来注册,管理被捕获的值。

总的来说,Getx通过巧妙的架构设计,侵入式的托管式框架,确实做到了在简单项目中的面向Getx开发

所以我们知道在状态管理时,第一个权衡,简单好上手

与Getx简单相反的是Bloc的复杂, Bloc 提供了全面且复杂的状态管理模式,Predictable可预测的以及Bloc的设计模式的结合使框架有很多的模版,是的更细致的状态更新管理,隔离的状态称为可能, 后面会结合Riverpod来解释这一优势。

状态传递

第一个需要权衡的点是状态转移,如果使用原生StatefulWidget,当我们Widget数开始庞大起来,不可避免的要进行组件的拆分降低维护难度,或者出于重用的目的,这样的操作势必会进行状态的转移,在这个维度就是百花齐放了,Getx和Rivierpod是将依赖管理状态相结合,通过全局的状态声明状态获取来达到这个目的,例如

/// 声明并加入到全局Scope

class DayChecked extends _$DayChecked {DateTime build() {return DateTime.now();}
}
/// 任意地方使用
final focusedDay = ref.watch(dayCheckedProvider); // 读取
ref.read(dayCheckedProvider.notifier).set(focusedDay) // 修改// Getx用法
class ScheduleState {Rx<DateTime> focusedDay = DateTime.now().obs;
}
class ScheduleLogic extends GetxController {final ScheduleState state = ScheduleState();
}
// 使用
Get.find<ScheduleLogic>().state.focusedDay

全局的状态管理易用程度是中等的,Getx和flutter_hooks 都提供了局部的状态声明,这在Widget内部状态处理时更方便的,hook的概念起源于React,状态管理相对于Getx是一种更轻量的解决方案,稍后我们提及其他方面的优势。我们可以认为Hook更适合内部状态变化。

全局的状态管理带来一个较为严重的问题,虽然可以通过组合别的方式来解决,但确实不够完备。

例如:TodoList中,任务的子父级关系的,虽然是同一个页面,使用同一个状态管理类,但状态应该是独自的,全局的状态管理解决这个问题的方法是通过入参,这种形式,例如Getx中Get.find(tag)和riverpod的build(params)来生成多个状态管理.

  // 使用参数化区分不同状态logic = Get.find<TaskDetailLogic>(tag: task.id);ref.watch(taskDetailProvider(task.id));

这样会带来一个比较棘手的问题,我们必须将logic或者参数tag一级一级的传递,或者通过callback一级一级的回传操作,来保证状态的正确读取和修改,这点在复杂项目当中是很棘手的,当锚定的入参过多时,我们会考虑父子Widget传递logicstate,保证状态的正确引用,但这丧失了灵活性和Widget重用的范围。

这种情况下,在全局状态和Widget内部状态中间,需要一种状态管理, 局部状态管理的概念就能够很好的解决这个问题,Bloc是这种方式的代表,通过BlocProvider提供一个局部, 被挂载到局部的状态,可以被context.read<SignInBloc>()获取相关联的状态。riverpod也能做到局部状态,只是他的设计初衷并不是如此,所以使用局部状态时,会显得很蹩脚,分离context,使用ref, 也导致官方并不推荐使用局部状态的方式,这将会造成混乱。

figure 2 状态范围(Scope)

scope.png

状态操作

使用Riverpod的同学会特别喜欢Flutter Hook, 因为riverpod对状态的声明和操作是分离的,尽管代码上他们封装在一起,但我们知道,这是OOP的写法, 便于理解,因为在一些语言中,方法状态是分离的,对象中内聚函数,第一个参数是Self, 同样,生成式的Riverpod通过另一种方式实现了分离。如下


abstract class NotifierBase<State> {NotifierProviderElement<NotifierBase<State>, State> get _element;State get state {_element.flush();return _element.requireState;}set state(State value) {// ignore: invalid_use_of_protected_member_element.setState(value);}Ref<State> get ref;

我们暂时关心如何通过设计达到这个目的,我们需要关心的是状态的操作是内部的,也就是被限制的,在ReactHook中,类似于[set, get], 返回的值并没有直接修改的能力,相较于Getx的 1.obs和 flutter hook的useState(1),是一个更加内聚的设计,但丧失了灵活性,两者需要在不同场景下结合使用

状态的声明有两种,基于Stream或者Listenable,两者都是响应式中重要的组成部分,不同的状态管理框架会权衡选择或者组合使用,例如,riverpod是基于Listenable, Bloc基于Stream, Getx则是混合使用的,我们大部分情况下不需要考虑状态的声明, 状态的修改是更影响开发体验,例如我们使用riverpod必须使用模版方法改变内部的值,然后借用freezed生成不可变对象, 保证值被完整的更新。或者实现equal方法主动进行diff并通知值的变化。例如:bloc+freezed


class SignInState with _$SignInState {const factory SignInState({String? password,required Option<String> passwordError,}) = _SignInState;factory SignInState.initial() => SignInState(passwordError: none(),);
}
class SignInBloc extends Bloc<SignInEvent, SignInState> {SignInBloc() : super(SignInState.initial()) {on<SignInEvent>((event, emit) async {await event.map(passwordChanged: (PasswordChanged value) {emit(state.copyWith(password: value.password,emailError: none(),),);},);});}
}

这种状态的修改对简单状态修改来说比较冗余,对层级复杂状态的修改,freezed就会很累赘,试想一下一个三级以上的嵌套, 例如:


class Task with _$Task {const factory Task({String? id,Alert? alert,}) = _Task;
}
class Alert with _$Alert {const factory Alert({String? id,Trigger? trigger,}) = _Alert;
}
class Trigger with _$Trigger {const factory Trigger({String? id,required DateTime time,}) = _Trigger;
}
class TaskDetail extends _$TaskDetail {Task build() {return Task();}/// 修改其中的一个值changeTrigger(DateTime dateTime) {state = state.copyWith(alert: state.alert?.copyWith(trigger: state.alert?.trigger?.copyWith(time: dateTime)));}
}

这是一个很常见的场景,在riverpod的官方文档中,我们可以看到将网络请求作为状态返回。这种在实际的复杂项目中是极其不推荐使用freezed或者说,引入不可变性, 这会导致灾难,虽然可以使用unfreezed放开部分权限,但这违背了freezed的设计初衷多层级且要修改 的Http请求并不适合作为状态返回,我们的状态管理,需要针对是ViewModel, 这种情况下,一般解决方案会有两种。

  • 主动通知 ref.notifyListener
  • 将多层级数据结构展开(flat)

这取决于对数据操作的频率和数据字段的难度,需要在实际使用当中进行权衡,需要额外关注的是,如果我们需要记录状态的变化,则freezed的不可变性会是更好的解决方案,需要优先考虑。

第二个常见的场景是需要持久化存储的时候, 不可变性限制了很多,比如不能修改内部变量,不能添加方法(可以通过extension),不能够使用继承, 例如 Hive 的Object就不能被继承来实现更改,当然,因为不可变的特性,也无法修改。

总的来说,在Getx中,模版中的state概念类似ViewModel的载体,内部是单独的小的state, 我们可以直接监听小的state,这也是Getx很灵活的一点。其他的框架则更多偏向于state整体的管理和更新(bloc中cubit很像但需要太多模版代码),freezed的限制和使用可以参考我的另一篇文章,不再赘述,因为这种生成式或者将来添加的都会遇到类似的问题,所以多聊一点。

状态的结束

riverpod在官网提及自己的优势是,自动生命周期管理。这点确实是状态管理的一个难点,也是开发者经常会忽略的一个点,当我们使用StatefulWidget时我们并不需要考虑这个问题,因为state随着Widget结束而释放,但我们使用Stream或者Listenable时,如何释放,变成了一个棘手的问题,flutter hook通过包装的Element, LinkedList<_Entry<HookState<Object?, Hook<Object?>>>>? _needDispose;, 调用use时加入dispose列表。riverpod通过侵入Widget树,通过Element监听生命周期,并管理依赖。

  void dispose() {runOnDispose(); // 魔法for (final sub in _dependencies.entries) {sub.key._providerDependents.remove(this);sub.key._onRemoveListener();}_dependencies.clear();_externalDependents.clear();}

其中runOnDispose() 就是riverpod的魔法, 可以通过他来自动取消Http请求的UI相关的耗时作业。 对于状态结束的监听和操作每个框架的实现细节不一致,但本质上却是要回归到Element 的生命周期函数,以及针对Stream和Listenable两种方式的Dispose的处理。

状态依赖和传递

我们大部分时间,大部分需求都是在处理简单任务,例如,将数据库数据或服务接口整合,然后布局,填充样式,最终渲染到页面。简单任务通常是简单的流程,这也是为什么官方更推荐riverpod的原因,因为它更契合这种场景,我们不需要做业务层级的分离,只需要在某个地方声明某个网络请求provider,然后监听结果,或者修改结果。这是非常理想的情况,稍微复杂的情况,就需要状态之间的依赖和传递了,例如这个场景,

这个场景下,任务列表是依赖于选中日期的,不同的状态处理框架提供了不同的解决方法,虽然本质上都是对Stream和Listenable的监听,但对于状态的依赖管理易用性是有很大差别的,Getx的方式会比较普通,扩展了Stream的函数,但仍需要主动Listen, riverpod的写法更巧妙,在语法上有很大的改善,比如当前这个场景。我们如果使用riverpod, 代码如下

(dependencies: [DayChecked, TaskList])
class DayTasks extends _$DayTasks {Future<List<TaskModel>> build() async {final checkedDay = ref.watch(dayCheckedProvider);// get task by checkDayretunr await serivce.get(checkedDay);}
}

区别于flutter_hook的函数是不明确的,例如实现相同功能,可能需要如下代码

    final checkedDate = useRef(DateTime.now());final taskList = useState(<TaskModel>[]);useEffect(() {taskList.value = service.get(checkedDate);}, [checkedDate.value]);

对于状态的传递,方程式的传递是相对优雅的,在数学上,我们知道y = 2x, 或者 z = 3x + 2y + 1 这样的简单等式, 能够优雅的以纯函数式来实现状态的转移是极其优雅的,对于这样的依赖传递形式的状态转移方程,riverpod的实现要优雅且严谨很多,更不容易出错。设想一个场景,用户是否为VIP, 这是一个简单状态,大多数情况下比用户信息更频繁的去读取,这个字段也需要被设计到用户信息接口,如果我们对UI层只提供简单的VIP状态,我们可以使用如下写法。

()
class UserConifg extends _$UserConifg {UserModel build() {return UserModel(avatar: '', id: "1", nickName: "nickName", pay: true);}
}(dependencies: [UserConifg])
class Vip extends _$Vip {bool build() {final user = ref.watch(userConifgProvider);return user.pay;}
}

上述方式体现了这种方式的优雅,我们甚至可以将UserConifg放到别的包下,将Vip状态和UI关联,这种方式优雅的结构了复杂场景下的状态转移高内聚,低耦合的原则。回到线性方程,它也符合y = 2x代数方程的思维。

总结:

任何领域都大概率都没有银弹, 软件开发领域也是如此,我们创造工具,使用工具,改进工具,才有软件的繁荣。不一定非要讨论那个框架或技术有高低差异。在实际开发中,稳定,熟悉是稳定三角的另外两个重要的方面,不同的框架的缺点,总会有一些或者优雅,或者败絮其中的解决方案,在项目中,最重要的适合,合适的工具会让我们开发过程事半功倍,其次是稳定性学习难度, 不过,一切都需要合适的权衡(trade off)动态的去匹配当下最重要的事。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[Vue warn]: Invalid vnode type when creating vnode: false

如题&#xff0c;意思是创建vnode时&#xff0c;vnode类型无效:false。 根据右边的索引点进去&#xff0c;发现定位的是组件loading。搜索loading发现声明了变量loading&#xff0c;更改后问题消失。

【python】获取4K壁纸保存到本地文件夹【附源码】

图片信息丰富多彩&#xff0c;许多网站上都有大量精美的图片资源。有时候我们可能需要批量下载这些图片&#xff0c;而手动一个个下载显然效率太低。因此&#xff0c;编写一个简单的网站图片爬取程序可以帮助我们高效地获取所需的图片资源。 目标网站&#xff1a; 如果出现模…

物联网云组态是什么?部署物联网云组态有什么作用?

在信息化与工业化的深度融合进程中&#xff0c;物联网云组态以其独特的优势&#xff0c;正在成为企业数字化转型的重要工具。那么&#xff0c;物联网云组态究竟是什么呢&#xff1f;部署物联网云组态又能给企业带来哪些实质性的好处呢&#xff1f;今天&#xff0c;我们将围绕这…

vue 消息左右滚动(前后无缝衔接)

之前一直用vue-seamless-scroll&#xff0c;无奈此组件有两个缺点不满足实际效果&#xff1a;1&#xff09;标题过长被截取、2&#xff09;标题果断也会滚动&#xff0c;无奈我自己封装一个&#xff0c;满足此两个弊端&#xff0c;也能达到vue-seamless-scroll组件的功能&#…

AI:Nvidia官网人工智能大模型工具合集(文本生成/图像生成/视频生成)的简介、使用方法、案例应用之详细攻略

AI&#xff1a;Nvidia官网人工智能大模型工具合集(文本生成/图像生成/视频生成)的简介、使用方法、案例应用之详细攻略 目录 Nvidia官网人工智能大模型工具合集的简介 1、网站主要功能包括: Nvidia官网人工智能大模型工具合集的使用方法 1、SDXL-Turbo的使用 2、GEMMA-7B的…

详解 net user

net user 是 Windows 操作系统自带的一个命令行工具&#xff0c;用于管理和查询本地用户账户信息。在域环境中&#xff0c;它可以用于管理本地用户账户&#xff0c;但对域用户账户的操作能力有限&#xff0c;尤其是查看域账户锁定状态等功能。以下是 net user 命令的详解&#…

【学习】Web安全测试需要考虑哪些情形

一、数据加密 某些数据需要进行信息加密和过滤后才能在客户端和服务器之间进行传输&#xff0c;包括用户登录密码、信用卡信息等。例如&#xff0c;在登录某银行网站时&#xff0c;该网站必须支持SSL协议&#xff0c;通过浏览器访问该网站时&#xff0c;地址栏的http变成https…

中国中药有限公司邀您到场参观2024燕窝滋补品展

参展企业介绍 中国中药有限公司是中国医药集团有限公司&#xff08;简称“国药集团”&#xff09;的全资子公司&#xff0c;是国药集团中药产业板块的核心投资平台&#xff0c;也是中药一类新药、中药科技进步一等奖、中药保密品种——“人工麝香”全国总代理。 公司经营范围…

Python装饰器深度解析:提升代码效率与可读性的实战指南

Python装饰器深度解析&#xff1a;提升代码效率与可读性的实战指南 摘要装饰器简介定义及基本用途装饰器在Python中的角色 装饰器的工作原理Python函数的运行时特性首个装饰器示例&#xff1a;简单函数计时装饰器的执行流程解析 使用装饰器增强函数功能编写可重用的日志记录装饰…

AI论文速读 | 具有时间动态的路网语义增强表示学习

论文标题&#xff1a; Semantic-Enhanced Representation Learning for Road Networks with Temporal Dynamics 作者&#xff1a; Yile Chen&#xff08;陈亦乐&#xff09; ; Xiucheng Li&#xff08;李修成&#xff09;; Gao Cong&#xff08;丛高&#xff09; ; Zhifeng Ba…

web前端性能优化【多年工作经验总结,一举拿下】

浏览器方面&#xff1a; 减少HTTP请求 HTTP 请求是指客户端&#xff08;例如浏览器&#xff09;向服务器发出的请求消息&#xff0c;用于获取特定资源或执行特定操作 为什么能够优化性能&#xff1f; 减少网络延迟&#xff1a;每次发起HTTP请求都需要经过网络传输&#xff…

openlayers 入门教程(五):sources 篇

还是大剑师兰特&#xff1a;曾是美国某知名大学计算机专业研究生&#xff0c;现为航空航海领域高级前端工程师&#xff1b;CSDN知名博主&#xff0c;GIS领域优质创作者&#xff0c;深耕openlayers、leaflet、mapbox、cesium&#xff0c;canvas&#xff0c;webgl&#xff0c;ech…

利用云手机技术,开拓海外社交市场

近年来&#xff0c;随着科技的不断进步&#xff0c;云手机技术逐渐在海外社交营销领域崭露头角。其灵活性、成本效益和全球性特征使其成为海外社交营销的利器。那么&#xff0c;究竟云手机在海外社交营销中扮演了怎样的角色呢&#xff1f; 首先&#xff0c;云手机技术能够消除地…

UE4 根据任意多个点,生成最近的线条

1.计算所有线条的组合 2.Clear0宏&#xff1a;清除掉数组Distance0的值。注意这里是设置成最大值&#xff0c;而不是使用Clear&#xff01; 3.清除掉数组中的最小值&#xff0c;避免重复生成相同长度的线条。注意这里是设置成最大值&#xff0c;而不是使用Clear&#xff01; …

接口关联和requests库

一、接口关联 postman的接口 postman的接口关联配置&#xff1a;js代码&#xff0c;重点在于思路。 // 定义jsonData这个变量 接受登录接口的返回结果 var jsonData JSON.parse(responseBody); // 从返回结果里提取token/id值&#xff0c;并赋值给token/id变量值作为环境变…

如何添加随机种子保证代码每次复现的一致性?

如何添加随机种子保证代码每次复现的一致性&#xff1f; 在main()程序中首先设定随机种子&#xff1a; def set_seed(seed42):os.environ[PYTHONHASHSEED] str(seed)random.seed(seed)np.random.seed(seed)torch.manual_seed(seed)torch.cuda.manual_seed(seed)torch.backends…

Spark基于DPU Snappy压缩算法的异构加速方案

一、总体介绍 1.1 背景介绍 Apache Spark是专为大规模数据计算而设计的快速通用的计算引擎&#xff0c;是一种与 Hadoop 相似的开源集群计算环境&#xff0c;但是两者之间还存在一些不同之处&#xff0c;这些不同之处使 Spark 在某些工作负载方面表现得更加优越。换句话说&am…

Java毕业设计 基于SSM网上二手书店系统

Java毕业设计 基于SSM网上二手书店系统 SSM jsp 网上二手书店系统 功能介绍 用户&#xff1a;首页 图片轮播 图书查询 图书分类显示 友情链接 登录 注册 图书信息 图片详情 评价信息 加入购物车 资讯信息 资讯详情 个人中心 个人信息 修改密码 意见信息 图书收藏 已经付款 邮…

数据仓库的魅力及其在企业中的应用实践

数据仓库&#xff0c;这一创新性的概念来自于比尔恩门&#xff0c;从1980年代末提出以来&#xff0c;便凭借其独特的架构设计和强大的数据处理能力&#xff0c;在全球商业领域中掀起了一场革命。它不仅是解决企业海量数据存储和查询需求的关键技术&#xff0c;更是推动企业实现…

rel=“dns-prefetch“和rel=“preconnect“ 结合使用,以及link属性的详细介绍

使用 dns-prefetch DNS-prefetch 尝试在请求资源之前解析域名。这可能是后面要加载的文件&#xff0c;也可能是用户尝试打开的链接目标。 为什么要使用 dns-prefetch&#xff1f; 当浏览器从&#xff08;第三方&#xff09;服务器请求资源时&#xff0c;必须先将该跨源域名解…