
bloc,既是架构设计,也是状态管理。
年初入坑 bloc,在公司的项目中也一直在使用,个人感受:分层合理、逻辑清晰、使用方便。
并且它还提供了一个简洁版:cubit。多一种选择,多一种体验。
但是在使用过程中,偶尔会感觉更新 state 有点麻烦,相信很多使用 bloc 的朋友有同感。本文即是对此问题的思考。
如果想直接看答案,滚动到文末。
回顾一下 bloc 更新 UI 的步骤:
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 的核心思想:
那位博主的问题在于:
为了可以更灵活的操作 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
详情: