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(context)获取到这个对应的Bloc。

class BlocProvider>
    extends ValueDelegateWidget implements SingleChildCloneableWidget { 
  final Widget child;

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

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

  //这里只是用`Provider`的类方法包装了一层,将通过InheritedElment获取方法`context.inheritFromWidgetOfExactType(type) as InheritedProvider`
  static T of>(BuildContext context) { ...
    
  //通过`InheritedProvider`存储bloc,提供provider获取的方法
  @override
  Widget build(BuildContext context) {
    return InheritedProvider(
      value: delegate.value,
      child: child,
    );
  }
  
  //方便MutliBlocProvider使用
  @override
  BlocProvider cloneWithChild(Widget child) {
    return BlocProvider._(
      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 builder;

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

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

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

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

 //更新bloc的订阅
  @override
  void didUpdateWidget(BlocBuilderBase 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, S> extends BlocListenerBase
    with SingleChildCloneableWidget {
 
  //初始化BlocListener,指定需要过滤的条件和设置listener执行的逻辑
  const BlocListener({
    Key key,
    @required BlocWidgetListener listener,
    B bloc,
    BlocListenerCondition condition,
    this.child,
  })  
  //拷贝child到当前的BlocListener中
  @override
  BlocListener cloneWithChild(Widget child) { ...
  @override
  Widget build(BuildContext context) => child;
}

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

  @override
  void initState() {
    super.initState();
    //可以自带一个bloc,但不支持其它子widget通过BlocProvider便利获取
    _bloc = widget.bloc ?? BlocProvider.of(context);
    _previousState = _bloc?.state;
    _subscribe();
  }
  
  //更新当前的bloc,重新订阅
  @override
  void didUpdateWidget(BlocListenerBase 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 extends Bloc {
  //内置一个默认的Storage,
  //BlocSupervisor.delegate这个是改写之后的 BlocDelegate
  final HydratedStorage _storage =
      (BlocSupervisor.delegate as HydratedBlocDelegate).storage;
  
  //初始化时从缓存获取数据,并在子类自定义的Bloc中实现这个三个方法,
  @mustCallSuper
  @override
  State get initialState { ...
  State fromJson(Map json);
  Map toJson(State state);
  
  //HydratedBloc内部使用,用于提供BlocState的key
  String get id => ''; //可以定义扩展缓存的key名字,用于区分不同的业务逻辑数据,一般公共的bloc逻辑类可以采用此类方法处理
  Future clear() => _storage.delete('${runtimeType.toString()}$id'); 
}

//Bloc state拦截,和保存

class HydratedBlocDelegate extends BlocDelegate { ...
  final HydratedStorage storage; 
  static Future 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 getInstance({
    Directory storageDirectory,
   })
   Future delete(String key) async {
   Future delete(String key) async {
   Future 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.of(context);

BlocBuilder(builder: (context, state) { ...

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

*另一种是通过BuilderStateDelegate来创建Bloc,它通过一个构造函数create(BuildContext context) =>BlocProvider(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实战应用)