上一篇文章详细说明了状态管理
在开发中的位置和所依赖的基础方法,帧与帧
之间的变化是对应状态变化的体现,但每个框架都有其侧重点
,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原生)。可以把疑问放到评论区,一起讨论。