GitHub 高赞的 Flutter 状态管理插件BLoC 简介

前言

在 Flutter 的状态管理插件中,BLoC(Business Logic Component)非常受欢迎,事实上在 GitHub 上,BLoC 在众多的状态管理插件中的 Star 是最多的( 共7.8k,Provider 是3.9k,GetX 是4.6k)。这主要的原因是 BLoC 更多的是一种设计模式,按照这种设计模式可以转变为很多种状态管理实现。实际上在 pub 搜索 BLoC 会出现很多相关的插件,当然,官方的还是 bloc 和 flutter_bloc 组合。我们本篇先来介绍一下 BLoC 的基本概念。

1.jpg

BLoC 与 Stream

BLoC 依赖 StreamStreamController实现,组件通过Sinks发送更新状态的事件,然后再通过 Streams 通知其他组件更新。事件处理和通知刷新的业务逻辑都是由 BLoC 完成,从而实现业务逻辑与 UI 层的分离(有点类似 Redux),并且逻辑部分可以复用和可以单独进行单元测试。

2.jpg

Flutter 中的 BLoC

bloc package是为了快速在 Flutter 或 Dart 中实现 BLoC 模式的插件,BLoC 官网提供了很多示例和详细的文档,有兴趣深入了解的可以上官网啃英文文档和浏览示例项目的代码。在 BLoC 有三个重要的概念,分别是 CubitBlocObserverBLoC

Cubit

Cubit是管理状态数据的 BlocBase 子类,它可以管理任意类型的数据,包括基本类型到复杂对象。在Cubit调用 emit 构建新的状态数据前需要给状态数据一个初始值。当状态数据发生改变的时候,会触发 CubitonChange 回调,如果出现错误则会触发 onError 回调。

3.jpg
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);

  @override
  void onChange(Change<int> change) {
    super.onChange(change);
    print(change);
  }

  @override
  void onError(Object error, StackTrace stackTrace) {
    print('$error, $stackTrace');
    super.onError(error, stackTrace);
  }
}

UI界面可以通过调用 Cubit 对外暴露的更新状态方法触发状态更新,而在 onChange 中会得到更新前后的状态,从而可以触发界面刷新。
[图片上传失败...(image-613730-1648814946206)]

BlocObserver

BlocObserver 可以监听所有的 Cubit 的变化,从而使得我们可以同时监听多个 Cubit。例如运行下面的代码

class CounterCubit extends Cubit<int> {
  CounterCubit({initial = 0}) : super(initial);

  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
}

class MyBlocObserver extends BlocObserver {
  @override
  void onCreate(BlocBase bloc) {
    print('BloC Observer onCreate:  ${bloc.state}');
    super.onCreate(bloc);
  }

  @override
  void onChange(BlocBase bloc, Change change) {
    print('BloC Observer onChange: $change');
    super.onChange(bloc, change);
  }

  @override
  void onClose(BlocBase bloc) {
    print('BloC Observer onClose: ${bloc.state}');
    super.onClose(bloc);
  }

  @override
  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
    print('Bloc Observer onError: $error, $stackTrace');
    super.onError(bloc, error, stackTrace);
  }

  @override
  void onEvent(Bloc bloc, Object? event) {
    print('Bloc Observer onEvent: $event, ${bloc.state}');
    super.onEvent(bloc, event);
  }
}

void main() {
  Bloc.observer = MyBlocObserver();
  final cubit = CounterCubit();

  cubit.increment();
  print('after increment: ${cubit.state}');

  cubit.decrement();
  print('after decrement: ${cubit.state}');

  final anotherCubit = CounterCubit();
  anotherCubit.increment();

  cubit.close();
  anotherCubit.close();
}

控制台打印结果为:

BloC Observer onCreate:  0
BloC Observer onChange: Change { currentState: 0, nextState: 1 }
BloC Observer onChange: Change { currentState: 1, nextState: 0 }
BloC Observer onCreate:  10
BloC Observer onChange: Change { currentState: 10, nextState: 11 }
BloC Observer onClose: 0
BloC Observer onClose: 11

Bloc

Bloc也是继承 BlocBase 的类,但相比 Cubit 来说更为高级一些。它使用的是 events 而不是暴露的函数来更新状态。

[图片上传失败...(image-6264b6-1648814946206)]

Bloc 内部,有一个onEvent 方法,在这个方法里,通过 EventTransformerevent转换为更新状态的方法来刷新状态数据。每个event都可以有对应的 EventHandler来处理该 event,完成后再通过 emit 触发通知状态更新。当状态转变前会调用 onTransition,在这里会有当前的状态,触发更新的 event 和下一个状态。

[图片上传失败...(image-f9e883-1648814946206)]

用 Bloc 改写之前的 CounterCubit 形式如下,由于 Bloc 也是 BlocBase 的子类,因此也可以使用 Observer 监测它的变化。

abstract class CounterEvent {}

class IncrementEvent extends CounterEvent {}

class DecrementEvent extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc(int initialState) : super(initialState) {
    on<IncrementEvent>((event, emit) => emit(state + 1));
    on<DecrementEvent>((event, emit) => emit(state - 1));
  }

  @override
  void onTransition(Transition<CounterEvent, int> transition) {
    print(
        'Current: ${transition.currentState}, Next: ${transition.nextState}, Event: ${transition.event}');
    super.onTransition(transition);
  }
}

void main() {
  Bloc.observer = MyBlocObserver();
  
  final counterBloc = CounterBloc(5);
  counterBloc.add(IncrementEvent());
  counterBloc.add(DecrementEvent());

  counterBloc.close();
}

执行结果如下:

Current: 5, Next: 6, Event: Instance of 'IncrementEvent'
BloC Observer onChange: Change { currentState: 5, nextState: 6 }
Current: 6, Next: 5, Event: Instance of 'DecrementEvent'
BloC Observer onChange: Change { currentState: 6, nextState: 5 }
BloC Observer onClose: 5

总结

Bloc 的设计来看,使用了函数(Cubit形式)和事件( Bloc形式)的方式来驱动状态数据更新,然后再通过emit通知状态更新。通过这种方式解耦 UI 界面和业务逻辑。可以看到,Bloc 本身的业务逻辑和界面完全无关,这使得我们可以直接编写测试代码,而无需依赖界面,如同本篇的 main 方法中的代码其实就可以作为单元测试代码来验证业务逻辑是否正确。这使得 Bloc 构建的应用程序的可维护性会更好。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容