美文网首页Flutter
FlutterBloc实战应用

FlutterBloc实战应用

作者: 嘛尼嘛哄 | 来源:发表于2020-09-30 01:53 被阅读0次

    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的次数.

    相关文章

      网友评论

        本文标题:FlutterBloc实战应用

        本文链接:https://www.haomeiwen.com/subject/vioyuktx.html