• Flutter | bloc 之 state 使用优化


    前尘镜

    前言

    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];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    官方的 copyWith 方法可以迅速生成一个 newStatenewState 的属性由 copyWith 的参数控制,这些参数都是可选的,如果不传,赋值原来的值,因此,当我们只想修改某一个属性的时候,就只需要传那一个参数。

    大部分时候,使用起来还是很舒服的。

    遇到的问题

    刚刚说了,大部分时候,使用起来还是很舒服的。就是说,还是有不舒服的时候。

    比如,我想给上文 statepassword 赋值 null,怎么办?

    官方 demo 的 copyWith,参数传 null,相当于赋值原来的值:

    password: password ?? this.password,
    
    • 1

    如果要赋值 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;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这位博主的思路是:先 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,
        );
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    现在有个需求:将 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,
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在原来的逻辑上,多了一层控制:

    • resetPassword 如果为 false,走原来的逻辑;
    • resetPassword 如果为 truepassword 直接赋值 null

    使用:

    // 给 password 赋值 null
    final newState = state.copyWith(resetPassword: true);
    emit(newState);
    
    • 1
    • 2
    • 3

    方案二:不添加属性,运用函数式编程,将参数类型改为 ValueGetter

    LoginPageState copyWith({
      String? phone,
      ValueGetter<String?>? password,
    }) {
      return LoginPageState(
        phone: phone ?? this.phone,
        password: password != null ? password() : this.password,
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    通过函数精准控制 password 的值:

    // 给 password 赋值 null
    final newState = state.copyWith(password: () => null);
    emit(newState);
    
    • 1
    • 2
    • 3

    这就是函数式编程的实际运用。

    PS: 看源码可知 ValueGetter 是一个有返回值的函数:

    typedef ValueGetter<T> = T Function();
    
    • 1

    方案三:借助三方插件 freezed

    详情:

  • 相关阅读:
    打卡第 2 天: urllib简记
    无为WiFi的一批服务器
    [R] ggplot2 - exercise (“fill =“)
    milvus数据管理-删除数据
    springboot和vue:十三、VueX简介与安装与推荐视频+前端数据模拟MockJS
    高数 | 导数极限定理、分段点求导能不能用公式?导数和导数的极限?
    idea设置项目启动的JVM运行内存大小
    [JS真好玩] 表格不支持排序?用4行JS排序!两种方案:基于flex order或replaceChildren
    C++中的继承
    .NET 高效灵活的API速率限制解决方案
  • 原文地址:https://blog.csdn.net/m0_59449563/article/details/126457591