什么是BLoC
BLoC(Business Logic Component)是谷歌提出的一种设计模式,利用Flutter 响应式结构的特点,通过Stream 的方式实现了异步渲染界面,实现布局与业务分离的效果.
这句话中有2个重点,一个是异步渲染,另一个就是业务与布局分离,
以传统的计数器代码为例,我们想要实现这个功能其实是非常简单的
class TsmSimpleBLoCPage extends StatefulWidget {
@override
State<StatefulWidget> createState() => _TsmSimpleBLoCState();
}
class _TsmSimpleBLoCState extends State<TsmSimpleBLoCPage> {
int count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Simple BLoC 学习'),
centerTitle: true,
),
body: Container(
alignment: Alignment.center,
child: Text(count.toString()),
),
floatingActionButton: FloatingActionButton(
child: Text('+'),
onPressed: () {
setState(() {
++count;
});
},
),
);
}
}
使用了BLoC后的代码结构如下
class TsmSimpleBLoCPage extends StatefulWidget{
@override
State<StatefulWidget> createState()=>_SimpleBLoCState();
}
class _SimpleBLoCState extends State<TsmSimpleBLoCPage>{
_SimpleBLoC _bloc;
@override
void initState() {
super.initState();
_bloc=_SimpleBLoC.of();
}
@override
void dispose() {
_bloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Simple BLoC 学习'),
centerTitle: true,
),
body: StreamBuilder(
stream:_bloc.outStream ,
initialData: _bloc.data,
builder: (context,snap){
return Container(
alignment: Alignment.center,
child: Text(snap.data.toString(),style: TextStyle(fontSize: 18),),
);
},
),
floatingActionButton: FloatingActionButton(
child: Text('+'),
onPressed: (){
_bloc.add();
},
),
);
}
}
class _SimpleBLoC{
int _data;
StreamController _streamController;
_SimpleBLoC.of(){
_streamController=StreamController<int>();
_data=0;
}
dispose(){
_streamController.close();
}
get outStream=>_streamController.stream;
get data=>_data;
add(){
_streamController.sink.add(++_data);
}
}
使用了BLoC 后,代码变得更多了,那么为什么还要使用他呢,如果你是一个android 开发的话,看起来是不是和MVP结构有点类似, 将逻辑代码与UI分离开,在state 的 build Widget 的方法内没有一行是关于业务逻辑的,在bloc 模块没有一行是关于布局的,这点主要是为了避免Widget 中嵌套了太多关于业务逻辑的判断,导致随着业务发展难以维护,虽然看到有网上说这么做是减少build 的次数,但是看了StreamBuilder 的源码发现,他用来更新widget的方法就是setState 的方法,并没有减少build的次数,只是通过观察者模式减少了业务与数据之间的关系
在使用bloc的过程中尽量使用StatefulWidget,这样你可以使用 dispose 方法来关闭stream ,减少内存泄露
我们先来看一下StreamBuilder 的源码,再来自己封装一个BLoC形式的StatefulWidget,以便于加深印象
class StreamBuilder<T> extends StreamBuilderBase<T, AsyncSnapshot<T>> {
const StreamBuilder({
Key key,
this.initialData,
Stream<T> stream,
@required this.builder,
}) : assert(builder != null),
super(key: key, stream: stream);
final T initialData;
@override
AsyncSnapshot<T> initial() => AsyncSnapshot<T>.withData(ConnectionState.none, initialData);
@override
AsyncSnapshot<T> afterConnected(AsyncSnapshot<T> current) => current.inState(ConnectionState.waiting);
@override
AsyncSnapshot<T> afterData(AsyncSnapshot<T> current, T data) {
return AsyncSnapshot<T>.withData(ConnectionState.active, data);
}
@override
AsyncSnapshot<T> afterError(AsyncSnapshot<T> current, Object error) {
return AsyncSnapshot<T>.withError(ConnectionState.active, error);
}
@override
AsyncSnapshot<T> afterDone(AsyncSnapshot<T> current) => current.inState(ConnectionState.done);
@override
AsyncSnapshot<T> afterDisconnected(AsyncSnapshot<T> current) => current.inState(ConnectionState.none);
@override
Widget build(BuildContext context, AsyncSnapshot<T> currentSummary) => builder(context, currentSummary);
}
入参接收了一个initialData,在初始化AsyncSnapshot 的时候将这个initialData 添加进去,其他方便并没有什么逻辑,继续看他的父类
abstract class StreamBuilderBase<T, S> extends StatefulWidget {
const StreamBuilderBase({ Key key, this.stream }) : super(key: key);
final Stream<T> stream;
S initial();
S afterConnected(S current) => current;
S afterData(S current, T data);
S afterError(S current, Object error) => current;
S afterDone(S current) => current;
S afterDisconnected(S current) => current;
Widget build(BuildContext context, S currentSummary);
@override
State<StreamBuilderBase<T, S>> createState() => _StreamBuilderBaseState<T, S>();
}
class _StreamBuilderBaseState<T, S> extends State<StreamBuilderBase<T, S>> {
StreamSubscription<T> _subscription;
S _summary;
@override
void initState() {
super.initState();
_summary = widget.initial();
_subscribe();
}
@override
void didUpdateWidget(StreamBuilderBase<T, S> oldWidget) {
super.didUpdateWidget(oldWidget);
///前后的数据源不同,先取消注册,再重新注册,
if (oldWidget.stream != widget.stream) {
if (_subscription != null) {
_unsubscribe();
_summary = widget.afterDisconnected(_summary);
}
_subscribe();
}
}
@override
Widget build(BuildContext context) => widget.build(context, _summary);
///在控件移出的时候取消注册
@override
void dispose() {
_unsubscribe();
super.dispose();
}
///添加各种监听
void _subscribe() {
if (widget.stream != null) {
_subscription = widget.stream.listen((T data) {
setState(() {
_summary = widget.afterData(_summary, data);
});
}, onError: (Object error) {
setState(() {
_summary = widget.afterError(_summary, error);
});
}, onDone: () {
setState(() {
_summary = widget.afterDone(_summary);
});
});
_summary = widget.afterConnected(_summary);
}
}
//取消注册
void _unsubscribe() {
if (_subscription != null) {
_subscription.cancel();
_subscription = null;
}
}
}
和包裹inheritedwidget 的statefulwidget 的使用方式基本是保持一致的,在initState 和dispose 方法中分别添加和移除监听,在didUpdateWidget方法中如果有必要则重置监听,在build 的方法内向下暴露当前的data
接下来我们利用StatefulWidget 与 StreamBuilder 封装一个简单的bloc 的base类,方便使用,
abstract class TsmBaseBLoC{
/**
* 用来调用 streamcontroller.close()
*/
void dispose();
}
class TsmBaseBLoCWidget<T extends TsmBaseBLoC > extends StatefulWidget{
final Widget child;
final T bloc;
TsmBaseBLoCWidget({Key key, @required this.child, @required this.bloc}):super(key:key);
@override
State<StatefulWidget> createState() =>_TsmBaseBLoCState<T>();
/**
* 便于子Widget 通过此方法向上
* 此方法不能在 TsmBaseBLoCWidget 的child直接使用,需要使用StatelessWidget 或者StatefulWidget
* 包裹一层,原因是 在findAncestorWidgetOfExactType 这个方法是直接查找他们_parent ,并没有比对自身
* 他的直接 包裹的子Widget 的bloc 直接使用初始化的就可以了,
*
*/
static T of<T extends TsmBaseBLoC>(BuildContext context){
TsmBaseBLoCWidget<T> provider = context.findAncestorWidgetOfExactType<TsmBaseBLoCWidget<T>>();
return provider.bloc;
}
}
class _TsmBaseBLoCState<T> extends State<TsmBaseBLoCWidget>{
@override
Widget build(BuildContext context) {
return widget.child;
}
@override
void dispose() {
widget.bloc.dispose();
super.dispose();
}
}
封装后自动实现自动streamcontroller.close 和 在TsmBaseBLoCWidget 的子 StatelessWidget 与StatefulWidget中可以使用var_bloc=TsmBaseBLoCWidget.of<T>(context);来获取该 bloc,
关于findAncestorWidgetOfExactType 方法引起的血案
这个方法的意思向上查找祖先widget,找到第一个符合条件的widget为止,既然是这样,正常情况下A页面启动B页面,是不是会通过 A页面加载B页面呢,这样B页面也就继承了A页面的某些特性,答案肯定不是这样的,先来分析一下如果我们这么做会出现什么问题
1> widget 树结构太复杂,打开的widget 的深度越深 则这个widget中包含所有widget的特性,数据量太大,这个太容易导致oom了,
2> 如果从InheritedWidget 方面来说,如果A启动B,B就会继承A的特性,设计这个InheritedWidget 就完全没有必须要了,
再来从代码上面来讲,打开一个页面使用的Navigator,我看了一下Navigator.of的方法,看一下他是如何实现的,
static NavigatorState of(
BuildContext context, {
bool rootNavigator = false,
bool nullOk = false,
}) {
// Handles the case where the input context is a navigator element.
NavigatorState navigator;
if (context is StatefulElement && context.state is NavigatorState) {
navigator = context.state as NavigatorState;
}
if (rootNavigator) {///找到最顶层NavigatorState ,即MaterialApp 的 navigatorKey
navigator = context.findRootAncestorStateOfType<NavigatorState>() ?? navigator;
} else {
///找到最近NavigatorState
navigator = navigator ?? context.findAncestorStateOfType<NavigatorState>();
}
return navigator;
}
在Navitagor.of的方法我们发现他需要一个rootNavigator ,来控制他是获取整个App根NavigatorState 还是向上查找最近一个NavigatorState,
那也就可以理解在整个app中是可以存在多个 NavigatorState, 这样就实现了根NavigatorState 控制了整个app的风格, 最近的NavigatorState 作为模块的根NavigatorState 控制着必要的数据,实现在一个模块共享一些必须的信息,为什么要这么设计,我先来说一下我的需求,以一个机票改期为例, 改期的第一步需要获取到需要改期的行程,根据这个原始行程你需要经过选择日历的界面,选择新航班的页面,选择新仓位的页面,新的确认订单页面,如果你使用的是app 的根NavigatorState,所有的页面是平级的,他们之间不能使用InheritedWidget共享数据 ,将这个数据放在根NavigatorState共享给其他模块又没有什么意义,你如果在选择需要改期的行程页面创建这个NavigatorState , 这个NavigatorState 承接了根NavigatorState 的theme数据,同时又可以利用InheritedWidget 在这个NavigatorState 下面共享这个原始行程的数据,这样实现起来就非常完美了,
这些都是一些理论知识,由于Navigator和NavigatorState 他们是一个控件的整体,就像StatefulWidget 和State 一样,利用Navigator来创建页面我还没有看到,只是一些猜想,而这个灵感就是从findAncestorWidgetOfExactType 这样一个方法而来的,虽然因此思前想后的3个多小时,但是知识的进步真是一件令人愉快的事情
这个里面关于如何实现一个简单的Navigator,希望大家和我多多交流,在后续我也会多加关注这方面的源码,
我学习flutter的整个过程都记录在里面了
https://www.jianshu.com/c/36554cb4c804
最后附上demo 地址