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

news/2024/4/29 11:27:05/文章来源:https://blog.csdn.net/ConcurrentHashMap/article/details/137003785

起因

每隔一段时间,都会出现一个新的状态管理框架,最近在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.luyixian.cn/news_show_1027793.aspx

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

相关文章

春秋云境CVE-2023-0562

简介 银行储物柜管理系统是一个基于网络的应用程序&#xff0c;用于处理存储银行客户贵重物品的银行储物柜。储物柜的所有详细信息都保存在数据库中。银行储物柜管理系统项目是使用 PHP 和 MySQLi 扩展开发的。 正文 进入靶场&#xff0c;首先就看到有个bankers&#xff0c;…

Java安全篇-Fastjson漏洞

前言知识&#xff1a; 一、json 概念&#xff1a; json全称是JavaScript object notation。即JavaScript对象标记法&#xff0c;使用键值对进行信息的存储。 格式&#xff1a; {"name":"wenda","age":21,} 作用&#xff1a; JSON 可以作为…

【Redis】redis哨兵模式

概述 Redis Sentinel&#xff0c;即Redis哨兵&#xff0c;在Redis 2.8版本开始引入。它是Redis高可用的实现方案之一。Sentinel是一个管理多个Redis实例的工具&#xff0c;它的核心功能是可以实现对Redis的监控、通知、自动故障转移。 监控&#xff08;Monitoring&#xff09…

OSX-02-Mac OS应用开发系列课程大纲和章节内容设计

本节笔者会详细介绍下本系统专题的大纲&#xff0c;以及每个专题章节的组织结构。这样读者会有一个全局的概念。 在开始前还是在再介绍一下下面这个框架图&#xff0c;因为比较重要&#xff0c;在这里再冗余介绍一下。开发Apple公司相关产品的软件时&#xff0c;主要有两个框架…

Untiy 布局控制器Aspect Ratio Fitter

Aspect Ratio Fitter是Unity中的一种布局控制器组件&#xff0c;用于根据指定的宽高比来调整包含它的UI元素的大小。实际开发中&#xff0c;它可以确保UI元素保持特定的宽高比&#xff0c;无论UI元素的内容或父容器的大小如何变化。 如图为Aspect Ratio Fitter组件的基本属性&…

深度学习 - PyTorch基本流程 (代码)

直接上代码 import torch import matplotlib.pyplot as plt from torch import nn# 创建data print("**** Create Data ****") weight 0.3 bias 0.9 X torch.arange(0,1,0.01).unsqueeze(dim 1) y weight * X bias print(f"Number of X samples: {len(…

Day24:私信列表、私信详情、发送私信

测试用户&#xff1a;用户名aaa 密码aaa 查询当前用户的会话列表&#xff1b;每个会话只显示一条最新的私信&#xff1b;支持分页显示。 首先看下表结构&#xff1a; conversation_id: 用from_id和to_id拼接&#xff0c;小的放前面去&#xff08;因为两个人的对话应该在一个会…

物联网实战--入门篇之(一)物联网概述

目录 一、前言 二、知识梳理 三、项目体验 四、项目分解 一、前言 近几年很多学校开设了物联网专业&#xff0c;但是确却地讲&#xff0c;物联网属于一个领域&#xff0c;包含了很多的专业或者说技能树&#xff0c;例如计算机、电子设计、传感器、单片机、网…

誉天华为认证云计算课程如何

HCIA-Cloud Computing 5.0 课程介绍&#xff1a;掌握华为企业级虚拟化、桌面云部署&#xff0c;具备企业一线部署实施及运维能力 掌握虚拟化技术、网络基础、存储基础等内容&#xff0c;拥有项目实施综合能力 满足企业虚拟化方案转型需求&#xff0c;应对企业日益多样的业务诉求…

快速上手Spring Cloud 十四:璀璨物联网之路

快速上手Spring Cloud 一&#xff1a;Spring Cloud 简介 快速上手Spring Cloud 二&#xff1a;核心组件解析 快速上手Spring Cloud 三&#xff1a;API网关深入探索与实战应用 快速上手Spring Cloud 四&#xff1a;微服务治理与安全 快速上手Spring Cloud 五&#xff1a;Spring …

JAVA面试八股文之集合

JAVA集合相关 集合&#xff1f;说一说Java提供的常见集合&#xff1f;hashmap的key可以为null嘛&#xff1f;hashMap线程是否安全, 如果不安全, 如何解决&#xff1f;HashSet和TreeSet&#xff1f;ArrayList底层是如何实现的&#xff1f;ArrayList listnew ArrayList(10)中的li…

【计算机网络】第 11、12 问:流量控制和可靠传输机制有哪些?

目录 正文流量控制的基本方法停止-等待流量控制基本原理滑动窗口流量控制基本原理 可靠传输机制1. 停止-等待协议2. 后退 N 帧协议&#xff08;GBN&#xff09;3. 选择重传协议&#xff08;SR&#xff09; 正文 流量控制涉及对链路上的帧的发送速率的控制&#xff0c;以使接收…

云数据仓库Snowflake论文完整版解读

本文是对于Snowflake论文的一个完整版解读&#xff0c;对于从事大数据数据仓库开发&#xff0c;数据湖开发的读者来说&#xff0c;这是一篇必须要详细了解和阅读的内容&#xff0c;通过全文你会发现整个数据湖设计的起初原因以及从各个维度&#xff08;架构设计、存算分离、弹性…

FPGA高端项目:解码索尼IMX327 MIPI相机转HDMI输出,提供FPGA开发板+2套工程源码+技术支持

目录 1、前言2、相关方案推荐本博主所有FPGA工程项目-->汇总目录我这里已有的 MIPI 编解码方案 3、本 MIPI CSI-RX IP 介绍4、个人 FPGA高端图像处理开发板简介5、详细设计方案设计原理框图IMX327 及其配置MIPI CSI RX图像 ISP 处理图像缓存HDMI输出工程源码架构 6、工程源码…

fastadmin学习04-一键crud

FastAdmin 默认内置一个 test 表&#xff0c;可根据表字段名、字段类型和字段注释通过一键 CRUD 自动生成。 create table fa_test (id int unsigned auto_increment comment ID primary key,user_id int(10) default 0 null…

吴恩达深度学习笔记:浅层神经网络(Shallow neural networks)3.1-3.5

目录 第一门课&#xff1a;神经网络和深度学习 (Neural Networks and Deep Learning)第三周&#xff1a;浅层神经网络(Shallow neural networks)3.1 神经网络概述&#xff08;Neural Network Overview&#xff09;3.2 神经网络的表示&#xff08;Neural Network Representation…

YOLOv8全网独家改进: 红外小目标 | 注意力改进 | 多膨胀通道精炼(MDCR)模块,红外小目标暴力涨点| 2024年3月最新成果

💡💡💡本文独家改进:多膨胀通道精炼(MDCR)模块,解决目标的大小微小以及红外图像中通常具有复杂的背景的问题点,2024年3月最新成果 💡💡💡红外小目标实现暴力涨点,只有几个像素的小目标识别率大幅度提升 改进结构图如下: 收录 YOLOv8原创自研 https://blog…

Day24 代码随想录(1刷) 回溯

目录 77. 组合 216. 组合总和 III 17. 电话号码的字母组合 给定两个整数 n 和 k&#xff0c;返回范围 [1, n] 中所有可能的 k 个数的组合。 你可以按 任何顺序 返回答案。 示例 1&#xff1a; 输入&#xff1a;n 4, k 2 输出&#xff1a; [[2,4],[3,4],[2,3],[1,2],[1,3],[1…

Vue生命周期,从听说到深入理解(全面分析)

每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤&#xff0c;比如设置好数据侦听&#xff0c;编译模板&#xff0c;挂载实例到 DOM&#xff0c;以及在数据改变时更新 DOM。在此过程中&#xff0c;它也会运行被称为生命周期钩子的函数&#xff0c;让开发者有机会在特定阶…

基于深度学习的OCR,如何解决图像像素差的问题?

基于深度学习的OCR技术在处理图像像素差的问题时确实面临一定的挑战。图像像素差可能导致OCR系统无法准确识别文本&#xff0c;从而影响其精度和可靠性。尽管已经有一些方法如SRN-Deblur、超分SR和GAN系列被尝试用于解决这个问题&#xff0c;但效果并不理想。然而&#xff0c;这…