前言
bloc,既是架构设计,也是状态管理。
年初入坑 bloc,在公司的项目中也一直在使用,个人感受:分层合理、逻辑清晰、使用方便。
并且它还提供了一个简洁版:cubit。多一种选择,多一种体验。
但是在使用过程中,偶尔会感觉更新 state 有点麻烦,相信很多使用 bloc 的朋友有同感。本文即是对此问题的思考。
如果想直接看答案,滚动到文末。
官方的 state
回顾一下 bloc 更新 UI 的步骤:
- copy 一个新的 state
-
emit(newState)
下面是 bloc 的一个官方 demo 中 state 的代码:
part of 'login_bloc.dart';
class LoginState extends Equatable {
const LoginState({
this.status = FormzStatus.pure,
this.username = const Username.pure(),
this.password = const Password.pure(),
});
final FormzStatus status;
final Username username;
final Password password;
LoginState copyWith({
FormzStatus? status,
Username? username,
Password? password,
}) {
return LoginState(
status: status ?? this.status,
username: username ?? this.username,
password: password ?? this.password,
);
}
@override
List<Object> get props => [status, username, password];
}
官方的
copyWith
方法可以迅速生成一个
newState
,
newState
的属性由
copyWith
的参数控制,
这些参数都是可选的
,如果不传,赋值原来的值,因此,当我们只想修改某一个属性的时候,就只需要传那一个参数。
大部分时候
,使用起来还是很舒服的。
遇到的问题
刚刚说了,
大部分时候
,使用起来还是很舒服的。就是说,还是有不舒服的时候。
比如,我想给上文
state
的
password
赋值
null
,怎么办?
官方 demo 的
copyWith
,参数传
null
,相当于赋值原来的值:
password: password ?? this.password,
如果要赋值 null,只有把
password: password ?? this.password
改成
password: password
。但这样一来,每次调用
copyWith
方法的时候都要给参数
password
赋值,即使不想改变它的值。
这有点蛋疼。
如果有一堆属性都需要赋值
null
,那就非常蛋疼了。
寻找答案
我搜寻全网,只为找到更加优雅的写法。
class MainState {
int selectedIndex;
bool isExtended;
MainState clone() {
return MainState()
..selectedIndex = selectedIndex
..isExtended = isExtended;
}
}
这位博主的思路是:先 clone 一个
newState
,再修改
newState
的值,最后
emit(newState)
。
貌似没什么问题,用起也很方便,同时解决了官方
copyWith
给 state 的属性赋值
null
特别蛋疼的问题。
但是,解决了一个问题,又创造了一个更大的问题。
来回顾一下 bloc 的核心思想:
- bloc 的核心思想是什么?
- stream
- stream 是什么?
- 流
- 什么流?
-
单向数据流
那位博主的问题在于:
为了可以更灵活的操作 state,去掉了 state 中各个属性的
final
修饰符,这样就可以直接修改
newState
的属性,但问题也在此:
当前 state 的属性也可以直接修改了
。
这种写法虽然解决了眼前的问题,但是它违背了 bloc
单向数据流
的设计思想,也因此存在一个非常大的隐患:
UI 与 data 不一致
。
按照 bloc 的设计初衷,state 的修改
只能
通过调用
emit(newState)
来实现,并且,state 改变了,UI 一定会跟着改变,UI 改变的前提
一定是
state 改变,
UI 与 data 一定同步
。
如果把 state 的各个属性的 final 去掉,可以直接修改 state 的属性而不需调用
emit(newState)
,state 变了 UI 可以不变,这就导致 UI 和 state 可以
不同步
,单向数据流因此瓦解。
官方原话:
Bloc试图通过调节何时可以发生状态更改并在整个应用程序中强制采用一种更改状态的方式来使状态更改
可预测
。
去掉 state 属性的
final
修饰符,意味着状态更改
不可预测
。
故,那位博主对 state 的优化表面上是优化了,实际上挖了个坑。
三种优化方案
虽然官方的
copyWith
不能完全满足我们,但是我们只需要在它的基础上稍加修改。
先仿照官方写一个最普通的 state:
class LoginPageState {
const LoginPageState({
this.phone,
this.password,
});
final String? phone;
final String? password;
LoginPageState copyWith({
String? phone,
String? password,
}) {
return LoginPageState(
phone: phone ?? this.phone,
password: password ?? this.password,
);
}
}
现在有个需求:将
password
值改为
null
。
方案一:给 copyWith 添加一个参数来控制
LoginPageState copyWith({
String? phone,
String? password,
bool resetPassword = false,
}) {
return LoginPageState(
phone: phone ?? this.phone,
password: resetPassword == true ? null : password ?? this.password,
);
}
在原来的逻辑上,多了一层控制:
-
resetPassword
如果为
false
,走原来的逻辑; -
resetPassword
如果为
true
,
password
直接赋值
null
。
使用:
// 给 password 赋值 null
final newState = state.copyWith(resetPassword: true);
emit(newState);
方案二:不添加属性,运用函数式编程,将参数类型改为
ValueGetter
LoginPageState copyWith({
String? phone,
ValueGetter<String?>? password,
}) {
return LoginPageState(
phone: phone ?? this.phone,
password: password != null ? password() : this.password,
);
}
通过函数精准控制
password
的值:
// 给 password 赋值 null
final newState = state.copyWith(password: () => null);
emit(newState);
这就是函数式编程的实际运用。
PS: 看源码可知
ValueGetter
是一个有返回值的函数:
typedef ValueGetter<T> = T Function();
方案三:借助三方插件
freezed
详情: