FlutterBloc实战应用

Flutter Bloc

英文全称(business logic),用于处理业务逻辑,其内部实现主要是对Stream的输入和输出进行了封装,它的实现原理是利用RxDart(基于Stream封装)提供的PublishSubjectBehivorSubject实现了EventState之间的转换,以及利用Flutter提供的局部状态管理控件InheritedWidget来传递Bloc对象,涉及的类比较多,但基本都围绕着对上面这个对象的封装。

Usage

https://bloclibrary.dev/

WorkFlow

  • Provider负责提供Bloc对象,注册到当前的context中
  • Bloc接口来自service和ui的事件,并将其转换为对应的state
  • service和UI根据接受的state进行更新各自的状态
  • 利用hydrateBloc对bloc的信息进行缓存
截屏2020-09-30 上午6.26.53.png

主要类之间的关系

FlutterBloc Classes.png

实现步骤

  1. 首先自定义Bloc

    自定义一个继承Bloc的类,并实现mapEventToState的方法逻辑,完成事件输入到数据输出之间的转换,

  2. 注册Bloc

BlocProvider(
    create: (context) => MyCustomBloc(),
    child: YourCustomWidget(),
);
  • 这里需要用到BlocProvider来包装Bloc的构造方法和它的获取方法
    BlocProvider继承于ValueDelegateWidget,并实现了SingleChildCloneableWidget,包含一个ValueStateDelegate对象

  • ValueStateDelegate主要是用于包装Bloc对象,用于控制是否更新或者销毁bloc

  • ValueDelegateWidget继承于DelegateWidget,在其State事件initState,didUpdateWidget,dispose中对ValueStateDelegate进行去重,更新和销毁操作,这样就能间接作用于bloc上(这样减少了Bloc的方法,起到了一定的解耦合作用),StateDelegate分为2中,BuilderStateDelegate支持关闭bloc,而ValueStateDelegate不会自动关闭bloc,在使用中如果不是全局单例Bloc,则容易出现内存泄漏

-BlocProvider在build时他会在内部创建一个InheritedProvider,它接受了一个Bloc作为值,由于InheritedProvider是继承于InheritedWidget,所以Bloc就可以通过InhertedElement间接的存储到当前的的Element中,它的child内部就能直接通过Context获取到.

  • SingleChildCloneableWidget这个类主要是定义了一个clone的协议,主要是为后面的MutliBlocProvider服务的,将单个child拷贝多分,设置到每个BlocProvider中,是框架内部的一个语法糖。

  • BlocProvider注册的过程中,可以看到它必须要又一个child,它自己本身是不需要在界面显示,只是作为一个数据的提供者,所有在它的之下的widget都能通过BlocProvder.of<BlocName>(context)获取到这个对应的Bloc。

class BlocProvider<T extends Bloc<dynamic, dynamic>>
    extends ValueDelegateWidget<T> implements SingleChildCloneableWidget { 
  final Widget child;

  //BlocProvider销毁时bloc会被自动关闭
  BlocProvider({ ...
    @required ValueBuilder<T> create,//在init时执行此方法创建bloc
    Widget child,
  })  

  //BlocProvider销毁时bloc不会自动关闭
  BlocProvider.value({ ...
    @required T value, //bloc
    Widget child, //具体的页面
  });

  //这里只是用`Provider`的类方法包装了一层,将通过InheritedElment获取方法`context.inheritFromWidgetOfExactType(type) as InheritedProvider<T>`
  static T of<T extends Bloc<dynamic, dynamic>>(BuildContext context) { ...
    
  //通过`InheritedProvider`存储bloc,提供provider获取的方法
  @override
  Widget build(BuildContext context) {
    return InheritedProvider<T>(
      value: delegate.value,
      child: child,
    );
  }
  
  //方便MutliBlocProvider使用
  @override
  BlocProvider<T> cloneWithChild(Widget child) {
    return BlocProvider<T>._(
      key: key,
      delegate: delegate,
      child: child,
    );
  }
}
  1. Bloc使用方式一般有三种,
  • 通过BlocBuilder来自动捕获asscent的bloc,或者直接在初始化的时候传入一个bloc,通过监听内部的bloc状态,并调用setState来更新当前的widgteTree,在BlocBuilder和它继承的BlocBuilderBase实现, 这里的条件主要是用来过滤Bloc的state,用于触发不同的child widget构建
class BlocBuilder .. extends BlocBuilderBase { ...
 
  final BlocWidgetBuilder<S> builder;

  /// {@macro blocbuilder}
  const BlocBuilder({
    Key key,
    @required this.builder,
    B bloc,
    BlocBuilderCondition<S> condition,)

  @override
  Widget build(BuildContext context, S state) => builder(context, state);
}

class _BlocBuilderBaseState<B extends Bloc<dynamic, S>, S>
    extends State<BlocBuilderBase<B, S>> { ... 
  //记录当前bloc的订阅,用于在销毁时释放订阅
  StreamSubscription<S> _subscription;
  //状态值记录,用于回调给 api调用者过滤不同的 condition,决定当前的state是否需要触发新的构建
  S _previousState;
  S _state;
  B _bloc;

  @override
  void initState() {
    super.initState();
    //可以看到bloc还可以直接重BlocBuilder传入,但这样就不支持BlocProvider便利获取了
    _bloc = widget.bloc ?? BlocProvider.of<B>(context);
     ...
    _subscribe();
  }

 //更新bloc的订阅
  @override
  void didUpdateWidget(BlocBuilderBase<B, S> oldWidget) { ...
  
  //外部widget构建的回调方法
  @override
  Widget build(BuildContext context) => widget.build(context, _state);
  
  //释放bloc的订阅
  @override
  void dispose() {
    _unsubscribe();
    super.dispose();
  }
  
  //监听bloc,并跳过intialState,因为在initial方法中已经拿到了
  void _subscribe() {
    if (_bloc != null) {
      _subscription = _bloc.skip(1).listen((S state) {
        if (widget.condition?.call(_previousState, state) ?? true) {
          setState(() { //很熟悉的setState
            _state = state;
            _previousState = state;
            ...
}
  • 通过BlocListener来监听bloc的state变化,过滤指定条件的state执行响应的逻辑
class BlocListener<B extends Bloc<dynamic, S>, S> extends BlocListenerBase<B, S>
    with SingleChildCloneableWidget {
 
  //初始化BlocListener,指定需要过滤的条件和设置listener执行的逻辑
  const BlocListener({
    Key key,
    @required BlocWidgetListener<S> listener,
    B bloc,
    BlocListenerCondition<S> condition,
    this.child,
  })  
  //拷贝child到当前的BlocListener中
  @override
  BlocListener<B, S> cloneWithChild(Widget child) { ...
  @override
  Widget build(BuildContext context) => child;
}

//Listener的逻辑实现
class _BlocListenerBaseState<B extends Bloc<dynamic, S>, S>
    extends State<BlocListenerBase<B, S>> {
  StreamSubscription<S> _subscription;
  S _previousState;
  B _bloc;

  @override
  void initState() {
    super.initState();
    //可以自带一个bloc,但不支持其它子widget通过BlocProvider便利获取
    _bloc = widget.bloc ?? BlocProvider.of<B>(context);
    _previousState = _bloc?.state;
    _subscribe();
  }
  
  //更新当前的bloc,重新订阅
  @override
  void didUpdateWidget(BlocListenerBase<B, S> oldWidget) { ...
  //构建child widget,这里只是by pass,没有做任何操作
  @override
  Widget build(BuildContext context) => widget.build(context);

  //订阅bloc state 
  void _subscribe() {
    if (_bloc != null) {
      _subscription = _bloc.skip(1).listen((S state) {
        if (widget.condition?.call(_previousState, state) ?? true) { //condition的过滤条件在这里
          widget.listener(context, state);
          _previousState = state;
        }
      });
    }
  }

  void _unsubscribe() { ...
  1. 为了便面地狱式的嵌套BlocProvider/BlocListener,引入了MultiBlocListenerMultiBlocProvider,他们属于StatelessWidget,主要是利用一个数组存储多个BlocProviderBlocListener,结合SingleChildCloneableWidget协议的实现
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: listeners,
      child: child,
    );
  }

// SingleChildCloneableWidget 协议的实现
    @override
  Widget build(BuildContext context) {
    var tree = child;
    for (final provider in providers.reversed) {
      tree = provider.cloneWithChild(tree);
    }
    return tree;
  }

  @override
  MultiProvider cloneWithChild(Widget child) {
    return MultiProvider(
      key: key,
      providers: providers,
      child: child,
    );
  }

Bloc数据缓存

主要是通过构建中间基类,对Bloc的initialState和translation事件进行传递并缓存数据到storage.可以参考HydratedBloc实现,以Bloc官方提供的HydratedBloc为例,它主要包括HydratedBloc,Your CustomBlocBloc之间插入的基本,用于管理Bloc数据的缓存.HydratedBlocDelegate它继承了BlocDelegate,用于替换Bloc框架内部默认的BlocDelegate,拦截BlocTranslation中的两个State并存储其每次变化的状态到缓存中去.

关键部分的代码实现:

abstract class HydratedBloc<Event, State> extends Bloc<Event, State> {
  //内置一个默认的Storage,
  //BlocSupervisor.delegate这个是改写之后的 BlocDelegate
  final HydratedStorage _storage =
      (BlocSupervisor.delegate as HydratedBlocDelegate).storage;
  
  //初始化时从缓存获取数据,并在子类自定义的Bloc中实现这个三个方法,
  @mustCallSuper
  @override
  State get initialState { ...
  State fromJson(Map<String, dynamic> json);
  Map<String, dynamic> toJson(State state);
  
  //HydratedBloc内部使用,用于提供BlocState的key
  String get id => ''; //可以定义扩展缓存的key名字,用于区分不同的业务逻辑数据,一般公共的bloc逻辑类可以采用此类方法处理
  Future<void> clear() => _storage.delete('${runtimeType.toString()}$id'); 
}

//Bloc state拦截,和保存

class HydratedBlocDelegate extends BlocDelegate { ...
  final HydratedStorage storage; 
  static Future<HydratedBlocDelegate> build({
    Directory storageDirectory,
  }) async {
    return HydratedBlocDelegate(
      await HydratedBlocStorage.getInstance(storageDirectory: storageDirectory),
    );
  }

  /// {@macro hydratedblocdelegate}
  HydratedBlocDelegate(this.storage);
  
  //关键步骤
  @override
  void onTransition(Bloc bloc, Transition transition) { 
        storage.write('${bloc.runtimeType.toString()}${bloc.id}',
          json.encode(stateJson)


//这个类逻辑单一,只负责文件存储,提供一个存储的Storage给上面的`HydratedBlocDelegate`
class HydratedBlocStorage implements HydratedStorage {...
   static Future<HydratedBlocStorage> getInstance({
    Directory storageDirectory,
   })
   Future<void> delete(String key) async {
   Future<void> delete(String key) async {
   Future<void> clear() async {

通过上面State部分的代码可以看出,此类bloc的设计主要是对Bloc的initialState和translation拦击来自动保存数据,默认提供了一个key,如果有多个不同的state则需使用多个state.这种写法其实对性能有一定影响。因为它是基于状态值实施存储的,所以这就需要我们做一些额外的优化。

  • 如果项目足够庞大,在初始化时则会有大量的bloc缓存加载,这样势必会加重Flutter I/O读取的速度,拖慢启动速度,而且不同的bloc之间有依赖关系,我们还需要保证他们的初始化顺序完全同步,这就要求我们从业务上尽可能的对bloc的依赖层级进行解耦,分不同的优先等级分批初始化;

  • 另外尽量不要采用多个State状态缓存,不管是从代码管理,状态传值都会显得非常长不便;

  • 此外为了缓解项目类同时过多的translation并发读写,可以对其进行扩展绑定bloc的dispose事件,标记state之后再慢慢缓存(以内存空间的部分缓存来解决磁盘空间的频繁缓存)。

Bloc获取网络数据

需要提前注册Repository,注册方式同Bloc类似,然后将Repository赋值给bloc,这样在bloc内部的mapEventToState的方法中就可以根据不同的event时间去访问网络请求了。

Bloc中的坑

  1. Bloc中所有的类型都是采用泛型推导的,这就要求我们在使用的时候不要忘记了指定我们它的类型.
BlocProvider<CustomBloc>.of(context);

BlocBuilder<CustomBloc,CustomBlocState>(builder: (context, state) { ...

BlocListener<CustomBloc,CustomBlocState>(listener: (preState, currentState){ ...
  1. BlocPovider作为Bloc载体,Bloc数据的存储会最终通过InheritedWidget存储在它当前所注册的作用阈中,如果我们在不同的作用域下注册Bloc,那么它将会被低级的作用域覆盖,因为此时WidgetTree中插入了2个不同层级的InheritedWidget<CustomBloc>,
    如果想让他们共享统一个bloc,就必须要保证InheritedWidget所关联的Bloc是同一个value.根据前面对Bloc创建提到的2个类可以看出,Bloc创建分2中方法:
  • 一种是通过SingleValueDelegateState直接传入Bloc,这种情况会将Bloc由上自下设为全部共享,但是有个弊端,会导致最上层的Widget移除后,bloc仍然会继续订阅,除非我们手动dispose,容易存在潜在的内存泄漏风险,一般只介意在整个FlutterApp生命周期全部都需要使用的单利才可使用此方法

*另一种是通过BuilderStateDelegate来创建Bloc,它通过一个构造函数create(BuildContext context) =>BlocProvider<CustomBloc>(context)`,需要注意的是后面的Bloc需要从上游已注册的Bloc中获取。

3.提到坑这里最近发掘了一个由FutureBuilder引起的bug,
在RootWidget下initialState中初始化了很多全局单例的bloc,然后在该widget的builder中使用了FutureBuilder异步构建BlocProvider对象,BlocProvider对象。但是在每次热重载的时候发现了所有的bloc全部被管理,最后排查了Bloc的作用域,自定义实现BlocProvider断点分析,bloc创建和初始化均无异常,排查FutureBuilder,它根据初始化的future生成done和其他的状态的widget,由于是启动入口,一般只会初始化一次,所以一开始没太注意,但最终发现运来是因为2个bugfix间接导致, 第一个是由于ios平台1.17渲染异常,加上了后台静止切换设置setState来避免UI渲染部分缺失的问题,所以在某些情况下这个FutureBuilder并不是真正的第一次创建,理论上来说只要这个future没变应该也不会出现另外一个rootWidget重新创建,然而恰哈就是因为之前捕捉futureBuilder传递的future(它是项目core service初始化的所有异步task的合并)在catchError的鬼使神差下,每次rootWidget构建都会触发futureBuilde的重新生成另外一个wiget重建,这样所有的BlocProvider都是移除后重建,就出现了类似的问题.
总结之后得出2点结论:
FutureBuilder作用域下面

总结

Bloc框架目前已有5.6k左右,这种简单的固定格式的写法非常适合快速构建项目架构,由于它是基于stream来实现的,所以使用中要特别注意订阅的销毁,避免出现内存的泄漏,和冗余的订阅。它的数据传递利用了InheritedWidget的局部状态刷新来完成层,相比使用常规的stateState来刷新获取数据会更搞效率,这一点上也基本上参照了Flutter官方的设计,比如ThemeData,Localization,MediaQuery等全局数据的获取.它局部Widget构建BlocBuilder依然采用的setState方式构建,因此我们需要尽量避免在BlocBuilder过多的业务逻辑处理,和不必要widget渲染,尽量把静态的widget移出去,业务逻辑我们可以采用BlocListenerBlocBuilder包裹,来优化代码结构,和减少BlocBuilder的次数.

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