一篇文章,告别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 checkDay
    retunr 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)动态的去匹配当下最重要的事。

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

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

相关文章

JavaParser 手动安装和配置

目录 前言 一、安装 Maven 工具 1.1 Maven 软件的下载 1.2 Maven 软件的安装 1.3 Maven 环境变量配置 1.4 通过命令检查 Maven 版本 二、配置 Maven 仓库 2.1 修改仓库目录 2.2 添加国内镜像 三、从 Github 下载 JavaParser 3.1 下载并解压 JavaParser 3.2 从路径打…

手摸手教你安装使用nvm(简单明了)

1.nvm定义 &#xff08;node.js version management&#xff09; nvm是node版本管理工具&#xff0c;通过nvm可以安装和切换不同版本的node.js 2.卸载之前安装的node 打开系统的控制面板&#xff0c;点击卸载程序&#xff0c;卸载nodejs 提示&#xff1a;如果你没有安装过…

【使用postman发送post请求】

1&#xff09;post http://ip:8090/version?appVersion1.0.0&channelgoogle&platformandroid&deviceId90991c4465e1886a81b00dac855fe098&notice1 这样子选择json格式提交数据&#xff0c;可读性强 好处&#xff1a; 1.最大的好处莫过于我可以记录下来曾经做…

使用easyYapi生成文档

easyYapi生成文档 背景1.安装配置1.1 介绍1.2 安装1.3 配置1.3.1 Export Postman1.3.2 Export Yapi1.3.3 Export Markdown1.3.4 Export Api1.3.6 常见问题补充 2. java注释规范2.1 接口注释规范2.2 出入参注释规范 3. 特定化支持3.1 必填校验3.2 忽略导出3.3 返回不一致3.4 设置…

智慧医疗包括哪些方面?智慧医疗发展前景如何?

近年来&#xff0c;随着云计算、物联网&#xff08;internet of things&#xff0c;IOT&#xff09;、移动互联网、大数据、人工智能&#xff08;artificial intelligence&#xff0c;AI&#xff09;、5G网络、区块链等新一代信息技术的逐步成熟和广泛应用&#xff0c;信息化已…

HTML(二)

一、表格标签 1.1表格的主要作用 表格主要用于显示、展示数据&#xff0c;因为它可以让数据显示的非常的规整&#xff0c;可读性非常好。特别是后台展示数据的时候&#xff0c;能够熟练运用表格就显得很重要。一个清爽简约的表格能够把繁杂的数据表现得很有条理。 1.2 表格的…

外地报医保怎么备案?异地就医备案流程是什么?

外地医疗保险如何办理&#xff1f; 外地医疗保险备案的具体流程可能因地区和医疗保险政策的不同而有所不同&#xff0c;但一般来说&#xff0c;可以通过以下方式进行备案&#xff1a; 1、网上备案&#xff1a;不少地区已经推出网上备案服务&#xff0c;可以通过当地医保局官网…

SQLServer SEQUENCE用法

SEQUENCE&#xff1a;数据库中的序列生成器 在数据库管理中&#xff0c;经常需要生成唯一且递增的数值序列&#xff0c;用于作为主键或其他需要唯一标识的列的值。为了实现这一功能&#xff0c;SQL Server 引入了 SEQUENCE 对象。SEQUENCE 是一个独立的数据库对象&#xff0c;用…

高等数学基础篇(数二)之微分方程(高阶线性微分方程)

高阶线性微分方程&#xff1a; 1.线性微分方程的解的结构 2.常系数齐次线性微分方程 3.常系数非齐次线性微分方程 4.欧拉方程 5.差分方程 目录 1.线性微分方程的解的结构 2.常系数齐次线性微分方程 3.常系数非齐次线性微分方程 4.欧拉方程 5.差分方程 1.线性微分方程…

【C++练级之路】【Lv.16】红黑树(冰与火的碰撞,红与黑的史诗)

快乐的流畅&#xff1a;个人主页 个人专栏&#xff1a;《C语言》《数据结构世界》《进击的C》 远方有一堆篝火&#xff0c;在为久候之人燃烧&#xff01; 文章目录 引言一、红黑树的概念二、红黑树的模拟实现2.1 结点2.2 成员变量2.3 插入情况一&#xff1a;uncle在左&#xff…

C++ STL- list 的使用以及练习

目录 0.引言 1. list 介绍 2. list 使用 2.1 构造函数 2.2 list iterator 的使用 3 list capacity 4. list element access 5. list modifiers 6. list 迭代器失效 7. list 与vector 对vector 8. OJ 题讲解 删除链表的倒数第 N 个节点&#xff1a; 0.引言 …

tcp/ip是什么意思,tcp/ip协议包含哪几层

TCP/IP是一种网络通信协议&#xff0c;它是互联网所采用的基本协议。TCP/IP协议是由美国国防部高级研究计划局&#xff08;ARPA&#xff09;在上世纪70年代设计开发的&#xff0c;经过多年发展和完善&#xff0c;已成为全球范围内最重要的网络通信协议之一。 首先&#xff0c;让…

python能做什么

python能做什么 Web开发&#xff1a;Python具有许多流行的Web框架&#xff0c;如Django和Flask&#xff0c;使得它成为Web开发的首选语言。它简洁、易于学习、且拥有丰富的生态系统&#xff0c;能够快速构建高性能的Web应用。 数据科学和机器学习&#xff1a;Python在数据科学…

Luminar Neo:重塑图像编辑新纪元,Mac与Win双平台畅享创意之旅

在数字时代的浪潮中&#xff0c;图像编辑软件已成为摄影师和设计师们不可或缺的创作工具。Luminar Neo&#xff0c;作为一款专为Mac与Windows双平台打造的图像编辑软件&#xff0c;正以其卓越的性能和创新的编辑功能&#xff0c;引领着图像编辑的新潮流。 Luminar Neo不仅继承…

Vue3更新Package.json版本号

由于我之前已经更新过了&#xff0c;下面的方法提示我已经是最新的了&#xff0c;记录一下&#xff0c;过段时间在测试一下 npm install -g vue/clivue upgrade

Python算法100例-4.2 列出真分数序列

完整源代码项目地址&#xff0c;关注博主私信源代码后可获取 1.问题描述2.问题分析3.算法设计4.确定程序框架5.完整的程序6.拓展训练 1&#xff0e;问题描述 按递增顺序依次列出所有分母为40、分子小于40的最简分数。 2&#xff0e;问题分析 分子和分母只有公因数1的分数&…

《手把手教你》系列技巧篇(五十四)-java+ selenium自动化测试-上传文件-中篇(详细教程)

1.简介 在实际工作中&#xff0c;我们进行web自动化的时候&#xff0c;文件上传是很常见的操作&#xff0c;例如上传用户头像&#xff0c;上传身份证信息等。所以宏哥打算按上传文件的分类对其进行一下讲解和分享。 2.为什么selenium没有提供API&#xff1f; 想必小伙伴们或者…

【学习】软件测试人员如何设计出优秀的测试用例

在软件开发的过程中&#xff0c;测试用例如同工程质量的守护者&#xff0c;它们的存在确保了软件产品的稳定性和可靠性。然而&#xff0c;如何设计出优秀的测试用例&#xff0c;让其在千变万化的软件世界中独领风骚&#xff0c;成为众多测试工程师追寻的目标。本文将为你揭示其…

9、垃圾回收器

为什么分代GC算法要把堆分成年轻代和老年代&#xff1f;首先我们要知道堆内存中对象的特性&#xff1a; 系统中的大部分对象&#xff0c;都是创建出来之后很快就不再使用可以被回收&#xff0c;比如用户获取订单数据&#xff0c;订单数据返回给用户之后就可以释放了。老年代中…

小红书矩阵批量发布工具,一键发布笔记软件

昨日&#xff0c;我收到了一条充满渴望与期待的私信&#xff0c;来自一位小红书的矩阵账号博主。他手握多个账号&#xff0c;渴望寻找一款能够助力他批量发布笔记的神器&#xff0c;每日能够轻松达到百篇的发布量。这份迫切的需求&#xff0c;我深感体会&#xff0c;因为这正是…