Flutter Bloc
英文全称(business logic),用于处理业务逻辑,其内部实现主要是对Stream的输入和输出进行了封装,它的实现原理是利用RxDart(基于Stream封装)提供的PublishSubject
和BehivorSubject
实现了Event
和State之间的转换
,以及利用Flutter提供的局部状态管理控件InheritedWidget
来传递Bloc
对象,涉及的类比较多,但基本都围绕着对上面这个对象的封装。
Usage
https://bloclibrary.dev/
WorkFlow
- Provider负责提供Bloc对象,注册到当前的context中
- Bloc接口来自service和ui的事件,并将其转换为对应的state
- service和UI根据接受的state进行更新各自的状态
- 利用hydrateBloc对bloc的信息进行缓存
主要类之间的关系
实现步骤
-
首先自定义Bloc
自定义一个继承Bloc的类,并实现
mapEventToState
的方法逻辑,完成事件输入到数据输出之间的转换, 注册Bloc
BlocProvider(
create: (context) => MyCustomBloc(),
child: YourCustomWidget(),
);
这里需要用到
BlocProvider
来包装Bloc
的构造方法和它的获取方法
BlocProvider
继承于ValueDelegateWidget
,并实现了SingleChildCloneableWidget
,包含一个ValueStateDelegate
对象ValueStateDelegate
主要是用于包装Bloc
对象,用于控制是否更新或者销毁blocValueDelegateWidget
继承于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
获取到这个对应的Bloc。(context)
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,
);
}
}
- 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() { ...
- 为了便面地狱式的嵌套
BlocProvider/BlocListener
,引入了MultiBlocListener
和MultiBlocProvider
,他们属于StatelessWidget
,主要是利用一个数组存储多个BlocProvider
或BlocListener
,结合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 CustomBloc
和Bloc
之间插入的基本,用于管理Bloc数据的缓存.HydratedBlocDelegate
它继承了BlocDelegate
,用于替换Bloc框架内部默认的BlocDelegate,拦截Bloc
的Translation
中的两个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中的坑
- Bloc中所有的类型都是采用泛型推导的,这就要求我们在使用的时候不要忘记了指定我们它的类型.
BlocProvider.of(context);
BlocBuilder(builder: (context, state) { ...
BlocListener(listener: (preState, currentState){ ...
- 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
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移出去,业务逻辑我们可以采用BlocListener
对BlocBuilder
包裹,来优化代码结构,和减少BlocBuilder的次数.