上一篇文章详细说明了状态管理在开发中的位置和所依赖的基础方法,帧与帧之间的变化是对应状态变化的体现,但每个框架都有其侧重点,Getx侧重简单,简单的页面,简单的状态管理,相对应的是复杂参数, 以及依赖传递时非常臃肿,需要使用很多Listen来同步不同的状态。Bloc有完整的filter,状态转移的监听,非常适合编辑器等复杂状态,例如回退操作,这两者一个适合简单业务,一个适合复杂协同业务,大多数项目都没有那么极端,所以Flutter官方推荐了Riverpod作为首推的状态管理工具。
具体对比请移步 一篇文章,告别Flutter状态管理争论,问题和解决
任何工具都有缺点,与此同时,就会有一个或者丑陋,或者优雅的Work around级别的解决方案,riverpod也是,这篇文章试图解决riverpod丑陋的参数传递问题。例如: 如下场景,一个日程任务有多种来源和多个视图才能确定某条任务, 我们假定参数为来源日期,来源计划,来源看板,那我们定位这一条任务就需要如下代码
/// 声明状态,需要三个入参
class TaskDetail extends _$TaskDetail {TaskModel build(int taskId, int planId, int viewId) {return service.query(taskId, planId, viewId);}
}
/// 使用需要明确的三个参数
class TaskDetailScreen extends HookConsumerWidget {final int taskId;final int planId;final int viewId;const TaskDetailScreen({super.key,required this.taskId,required this.planId,required this.viewId,});Widget build(BuildContext context, WidgetRef ref) {final taskModel = ref.watch(taskDetailProvider(taskId, planId, viewId));return Container();}
}
目前为止这段代码没有体现出任何的缺点,反而做到了状态的声明和使用的分离, 对于简单到中等复杂页面,这非常友好,如果我们选择使用非组件化,将所有代码写到这个TaskDetailScreen那将没有任何问题。因为不涉及参数传递或者指针(notifier)传递。但实际复杂的项目中,通常可重用和可阅读也是很重要的指标,这个时候我们不得不考虑使用组件来提升这两个属性。例如:我们有一个富文本组件, 如果我们只有简单的交互,我们可以通过传递taskModel或者增加Callback(String)等方法,与provider进行交互,但这通常会写成如下代码。
/// 需要透传参数或者notifer, 或者抛出Callback
class RichEditor extends ConsumerWidget {final int taskId;final int planId;final int viewId;const RichEditor({super.key,required this.taskId,required this.planId,required this.viewId,});Widget build(BuildContext context, WidgetRef ref) {return GestureDetector(onTap: () => ref.read(taskDetailProvider(taskId, planId, viewId).notifier).saveContent('content'),child: Text('editor'),);}
}
这是非常丑陋的,虽然这种情况在使用Bloc时会被非常优雅的解决,但也可以通过一些结合来尝试解决中等复杂难度的场景。
分析痛点和难点
riverpod在这个场景下的痛点时需要透传参数,且组件化时非常丑陋,容易出错,那他的难点在于什么?在于全局的Scope, 所有全局的管理,例如Getx等都会面临多个层级重复页面,多参数的在同一个路由栈这样常见的问题,或者同时显示多个相同组件(同一个provider)。难点在于无法隔离,也就是跟Context关联,局部状态。
找到难点,我们就可以从这个点出发去尝试解决这个问题,或者是这种特定用例下的问题,在不脱离riverpod的情况下,我们的直观选择是在局部使用ProviderScope, 例如:
ProviderScope(overrides: [sharedPrefsProvider.overrideWithValue(sharedPrefs),], child: const TaskDetailScreen()),
这是一种方案,但这种不符合riverpod的最佳实践,也容易造成状态管理的混乱,第二种方案是将参数使用其它方式进行Context相关的关联,比如通过自定义的InheritedWidget 这种方案类似于ThemeData的局部化处理,这种方案理解简单,但需要不同的页面和不同的Provider定制化, 第二种方案明显也丑陋的,虽然解决了部分问题, 但需要引入自定义的Scope。
组合Hook?
我们知道Riverpod是状态分离的,也就是声明和管理和使用状态是完全分离的,所以一个简单好用的界面内状态就可以极大简化这种类型的状态处理。所以,Flutter Hook就完美的补充了这部分。例如
final calendarFormat = useState(CalendarFormat.week);TableCalendar(calendarFormat: calendarFormat.value,onFormatChanged: (format) {if (format != calendarFormat.value) {calendarFormat.value = format;}},),
这里状态只和页面有关,声明、使用、 修改 都在一个build函数中完成,所以使用注解riverpod是有一点冗余的。本以为可以像React一样,子组件可以跨级获取父组件的状态,后来发现flutter_hook并不是如此,hook和riverpod结合时,hook更像是一个完全的局部管理,并没有Recact Scope这种概念,跨级传递状态。
组合Bloc ?
Bloc是重量级的,声明需要BlocProvider, 消费需要使用BlocConsumer<BlocA, BlocAState>, 这也有点丑陋,虽然我们避免了每一个需要复杂参数都需要声明一个单独的InheritedWidget, 但同样多了很多模版代码。如果是这样的组合,加重了页面的复杂性以及阅读理解难度, 不如直接使用Bloc。
反思, 是不是违背了设计的初衷?
当笔者处处碰壁的时候,想起了之前为了解决打点参数透传而写的一个库data_trakcer, 似乎两者是同样的问题,但不同于打点的简单字段,这里需要明确的状态和通知程序。 回顾响应式的设计原则 数据向下传递,操作向上传递, 似乎认为难点或者痛点其实本不应该被关注, 因为按照设计原则,一切都是合理的,我们必须对每个有操作的组件回调到声明notifer进行调用,或者将参数或notifer进行一级一级的传递。
应该如何是好?
上述思考的过程,让笔者重新梳理了主流的一些状态管理,但还有很多是我不曾使用和了解的,笔者也不确定是否有其他方案解决了我所头疼的问题,也或许根本不是问题。当我在看Bloc Flutter时,笔者发现,Bloc实现局部Scope的原理其实底层是Provider。这个被遗忘的基础状态管理。
static T of<T extends StateStreamableSource<Object?>>(BuildContext context, {bool listen = false,}) {try {return Provider.of<T>(context, listen: listen);} on ProviderNotFoundException catch (e) {}}
所以,是否可以使用Provider + Rivderpod解决我所遇到的问题?如下代码:
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:https_sync_client/domain/task_provider.dart';
import 'package:provider/provider.dart' as provider;class TaskDetailScreen extends HookConsumerWidget {final int taskId;final int planId;final int viewId;const TaskDetailScreen({super.key,required this.taskId,required this.planId,required this.viewId,});Widget build(BuildContext context, WidgetRef ref) {final taskDetailNotifier =ref.watch(taskDetailProvider(taskId, planId, viewId).notifier);return provider.Provider(create: (context) => taskDetailNotifier,child: const RichEditor(),);}
}class RichEditor extends HookConsumerWidget {const RichEditor({super.key,});Widget build(BuildContext context, WidgetRef ref) {return GestureDetector(onTap: () {context.read<TaskDetail>().update(content: "");},child: Center(child: Container(color: Colors.amber,width: 100,height: 100,),),);}
}
这段代码似乎成功了,又似乎解决了我的痛点。成本似乎只有provider.Provider一个容器和context.read。能不能真的实现痛点,希望各位自己验证一下,实践才是检验真理的唯一标准, 笔者也不确定这么操作是不是符合最佳实践,但在笔者工作当中两个场景是非常令人头痛的,一个打点,重运营类项目,经常需要按钮级别的打点需求, 第二是,业务复杂的组件且会堆叠的情况。
总结
遇到问题,解决问题是技术人的一个思考准则,当我们遇到丑陋代码时,总可以找到合适的姿势去改变一点,使之优雅好用一点,世界上没有银弹, 但软件开发领域因为不断的进步,也在逐渐的越来越好(至少不想回去做Android原生)。可以把疑问放到评论区,一起讨论。