Flutter | bloc 之 state 使用优化

  • Post author:
  • Post category:其他


前尘镜



前言

bloc,既是架构设计,也是状态管理。

年初入坑 bloc,在公司的项目中也一直在使用,个人感受:分层合理、逻辑清晰、使用方便。

并且它还提供了一个简洁版:cubit。多一种选择,多一种体验。

但是在使用过程中,偶尔会感觉更新 state 有点麻烦,相信很多使用 bloc 的朋友有同感。本文即是对此问题的思考。

如果想直接看答案,滚动到文末。



官方的 state

回顾一下 bloc 更新 UI 的步骤:

  1. copy 一个新的 state

  2. 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

,那就非常蛋疼了。



寻找答案

我搜寻全网,只为找到更加优雅的写法。


正好看到有个博主分享了他对 state 问题的优化:

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

详情:



版权声明:本文为m0_59449563原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。