bloc
参考文章:Reactive Programming - Streams - BLoC
(为了便于阅读,略去了原文中的一些跟Streambuilder和Bloc无关的拓展概念,比如RxDart、Demo的解释等,想要进一步了解的可以移步原文。)
先粗略讲点关于Stream的东西
Stream其实类似于Rx大家族,也是一种对于数据流的订阅管理。Stream可以接受任何类型的数据,值、事件、对象、集合、映射、错误、甚至是另一个Stream,通过Streamcontroller
中的sink
作为入口,往Stream中插入数据,然后通过你的自定义监听StreamSubscription
对象,接受数据变化的通知。如果你需要对输出数据进行处理,可以使用StreamTransformer
,它可以对输出数据进行过滤、重组、修改、将数据注入其他流等等任何类型的数据操作。
Stream有两种类型:单订阅Stream和广播Stream。单订阅Stream只允许在该Stream的整个生命周期内使用单个监听器,即使第一个subscription被取消了,你也没法在这个流上监听到第二次事件;而广播Stream允许任意个数的subscription,你可以随时随地给它添加subscription,只要新的监听开始工作流,它就能收到新的事件。
下面是一个单订阅的例子:
import 'dart:async';
void main() {
// 初始化一个单订阅的Stream controller
final StreamController ctrl = StreamController();
// 初始化一个监听
final StreamSubscription subscription = ctrl.stream.listen((data) => print('$data'));
// 往Stream中添加数据
ctrl.sink.add('my name');
ctrl.sink.add(1234);
ctrl.sink.add({'a': 'element A', 'b': 'element B'});
ctrl.sink.add(123.45);
// StreamController用完后需要释放
ctrl.close();
}
下面是添加了StreamTransformer的例子:
import 'dart:async';
void main() {
// 初始化一个int类型的广播Stream controller
final StreamController<int> ctrl = StreamController<int>.broadcast();
// 初始化一个监听,同时通过transform对数据进行简单处理
final StreamSubscription subscription = ctrl.stream
.where((value) => (value % 2 == 0))
.listen((value) => print('$value'));
// 往Stream中添加数据
for(int i=1; i<11; i++){
ctrl.sink.add(i);
}
// StreamController用完后需要释放
ctrl.close();
}
关于RxDart
之前已经说了,Stream是一种订阅者模式,所以跟Rx大家族很类似。Rx官方已经提供了对dart语言的官方支持——RxDart。两者的对应关系可以看下下表:
Dart | RxDart |
---|---|
Stream | Observable |
StreamController | subject |
对于RxDart的用法这里不多做讨论。
Flutter中Widget的状态管理和响应式编程的概念
我们都知道,Flutter中Widget的状态控制了UI的更新,比如最常见的StatefulWidget,通过调用setState({})
方法来刷新控件。那么其他类型的控件,比如StatelessWidget就不能更新状态来吗?答案当然是肯定可以的。比如Flutter的Redux
插件,就是一种在非StatefulWidget中刷新控件的机制。
我们上面已经说了,Stream的特性就是当数据源发生变化的时候,会通知订阅者,那么我们是不是可以延展一下,实现当数据源发生变化时,改变控件状态,通知控件刷新的效果呢?Flutter为我们提供了StreamBuilder
。
所以,StreamBuilder是Stream在UI方面的一种使用场景,通过它我们可以在非StatefulWidget中保存状态,同时在状态改变时及时地刷新UI。
(Flutter还有其他的一些管理状态的方法跟插件,鄙人暂时没有研究过其他,就不多说了。)
其实这种数据源改变,UI也跟着改变的方式就是一种响应式编程(Reactive Programming)。响应式编程就是使用异步数据流来编程的方式,换句话说,任何事件(比如点击事件)、变量、消息、请求等等的改变,都会触发数据流的传递。
如果使用响应式编程,那么APP将:
- 变为异步的;
- 围绕Streams和listeners的概念来构建;
- 当任意事件、变量等等发生变化时,会向Stream发送一个通知;
- Stream的监听者无论位于app中的哪个位置,都会收到这个通知。
什么是StreamBuilder
StreamBuilder其实是一个StatefulWidget,它通过监听Stream,发现有数据输出时,自动重建,调用builder方法。
StreamBuilder<T>(
key: ...可选...
stream: ...需要监听的stream...
initialData: ...初始数据,否则为空...
builder: (Buildcontext context, AsyncSnapshot<T> snapshot){
if (snapshot.hasData){
return ...基于snapshot.hasData返回的控件
}
return ...没有数据的时候返回的控件
},
)
下面是一个模仿官方自带demo“计数器”的一个例子,使用了StreamBuilder,而不需要任何setState:
import 'dart:async';
import 'package:flutter/material.dart';
class CounterPage extends StatefulWidget {
@override
_CounterPageState createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _counter = 0;
final StreamController<int> _streamController = StreamController<int>();
@override
void dispose(){
_streamController.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Stream version of the Counter App')),
body: Center(
child: StreamBuilder<int>( // 监听Stream,每次值改变的时候,更新Text中的内容
stream: _streamController.stream,
initialData: _counter,
builder: (BuildContext context, AsyncSnapshot<int> snapshot){
return Text('You hit me: ${snapshot.data} times');
}
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: (){
// 每次点击按钮,更加_counter的值,同时通过Sink将它发送给Stream;
// 每注入一个值,都会引起StreamBuilder的监听,StreamBuilder重建并刷新counter
_streamController.sink.add(++_counter);
},
),
);
}
}
这种实现方式比起setState
是一个很大的改进,因为我们不需要强行重建整个控件和它的子控件,只需要重建我们希望重建的StreamBuilder(当然它的子控件也会被重建)。我们之所以依然使用StatefulWidget的唯一原因就是:StreamController需要在控件dispose()的时候被释放。
能不能完全抛弃StatefulWidget?BLoC了解下
BLoC模式由来自Google的Paolo Soares和Cong Hui设计,并在2018年DartConf期间(2018年1月23日至24日)首次演示。 你可以在YouTube上观看此视频。
BLoC是Business Logic component(业务逻辑组建)的缩写,就是将UI与业务逻辑分离,有点MVC的味道。
简而言之,BLoC的使用注意点和好处是:
- 将业务逻辑(Business Logic)转移到一个或者多个BLoC中去;
- 尽可能地与表现层(presentation Layer)分离,换句话说就是UI组建只需要关心UI,而不需要关心业务逻辑;
- input(Sink)和output(Stream)唯一依赖于
Streams
的使用; - 保持了平台独立性;
- 保持了环境独立性。
下图是BLoC与Widget交互的简单示意图:
image.png
- Widget通过Sinks发送事件给BLoC;
- BLoC通过Streams通知Widget;
- 不需要关心BLoC实现的业务逻辑。
多亏了业务逻辑和UI的分离,使得:
1、我们可以随时更改业务逻辑,最小化对App的影响;
2、我们可以在完全不影响业务逻辑的前提下更改UI;
3、业务逻辑测试会更方便。
通过将StreamBuilder和BLoC结合,我们就可以完全放弃StatefulWidget了:
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Streams Demo',
theme: new ThemeData(
primarySwatch: colors.blue,
),
home: BlocProvider<IncrementBloc>(
bloc: IncrementBloc(),
child: CounterPage(),
),
);
}
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context);
return Scaffold(
appBar: AppBar(title: Text('Stream version of the Counter App')),
body: Center(
child: StreamBuilder<int>(
// StreamBuilder控件中没有任何处理业务逻辑的代码
stream: bloc.outCounter,
initialData: 0,
builder: (BuildContext context, AsyncSnapshot<int> snapshot){
return Text('You hit me: ${snapshot.data} times');
}
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: (){
bloc.incrementCounter.add(null);
},
),
);
}
}
class IncrementBloc implements BlocBase {
int _counter;
// 处理counter的stream
StreamController<int> _counterController = StreamController<int>();
StreamSink<int> get _inAdd => _counterController.sink;
Stream<int> get outCounter => _counterController.stream;
// 处理业务逻辑的stream
StreamController _actionController = StreamController();
StreamSink get incrementCounter => _actionController.sink;
// 构造器
IncrementBloc(){
_counter = 0;
_actionController.stream
.listen(_handleLogic);
}
void dispose(){
_actionController.close();
_counterController.close();
}
void _handleLogic(data){
_counter = _counter + 1;
_inAdd.add(_counter);
}
}
- 责任分离:StreamBuilder控件中没有任何处理业务逻辑的代码,所有的业务逻辑处理都在单独的
IncrementBloc
类中进行。如果你要修改业务逻辑,只需要修改_handleLogic()
方法就行了,无论处理过程多么复杂,CounterPage
都不需要知道,不需要关心。 - 可测试性:只需要测试
IncrementBloc
类即可。 - 自由组织布局:有了Streams,你就可以完全独立于业务逻辑地去组织你的布局了。可以在App中任何位置触发操作,只需要通过
.incrementCounter sink
来传入即可;也可以在任何页面的任何位置来展示counter,只需要监听.outCounter stream
。 - 减少了build的数量:使用
StreamBuilder
放弃setState()
大大减少了build的数量,因为只需要build需要刷新的控件,从性能角度来讲是一个重大的提升。
关于Bloc的可访问性
以上所有功能的实现,都依赖于一点,那就是Bloc必须是可访问的。
有很多方法都可以保证Bloc的可访问性:
- 全局单例(global Singleton):这种方式很简单,但是不推荐,因为Dart中对类没有析构函数(destructor)的概念,因此资源永远无法释放。
- 局部变量(local instance):你可以创建一个Bloc局部实例,在某些情况下可以完美解决问题。但是美中不足的是,你需要在StatefulWidget中初始化,并记得在
dispose()
中释放它。 - 由祖先(ancestor)来提供:这也是最常见的一种方法,通过一个实现了StatefulWidget的父控件来获取访问权。
下面的这个例子展示了一个通用的
BlocProvider
的实现:
// 所有 BLoCs 的通用接口
abstract class BlocBase {
void dispose();
}
// 通用 BLoC provider
class BlocProvider<T extends BlocBase> extends StatefulWidget {
BlocProvider({
Key key,
@required this.child,
@required this.bloc,
}): super(key: key);
final T bloc;
final Widget child;
@override
_BlocProviderState<T> createState() => _BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context){
final type = _typeof<BlocProvider<T>>();
BlocProvider<T> provider = context.ancestorWidgetOfExactType(type);
return provider.bloc;
}
static Type _typeOf<T>() => T;
}
class _BlocProviderState<T> extends State<BlocProvider<BlocBase>>{
@override
void dispose(){
widget.bloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context){
return widget.child;
}
}
怎么使用这个BlocProvider呢?
home: BlocProvider<IncrementBloc>(
bloc: IncrementBloc(),
child: CounterPage(),
),
这段代码实例化了一个新的BlocProvider,用于处理IncrementBloc
,然后见CounterPage
渲染为一个子控件。那么,BlocProvider下的任意一个子树(sub-tree)都可以访问IncrementBloc
,访问方式为:IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context);
。
多个Bloc的使用
- 每一个有业务逻辑的页面的顶层都应该有自己的BLoC;
- 每一个“足够复杂的组建(complex enough component)”都应该有相应的BLoC;
- 可以使用一个
ApplicationBloc
来处理整个App的状态。下面的例子展示了在整个App的顶层使用
ApplicationBloc
,在CounterPage的顶层使用IncrementBloc
:
void main() => runApp(
BlocProvider<ApplicationBloc>(
bloc: ApplicationBloc(),
child: MyApp(),
)
);
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context){
return MaterialApp(
title: 'Streams Demo',
home: BlocProvider<IncrementBloc>(
bloc: IncrementBloc(),
child: CounterPage(),
),
);
}
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context){
final IncrementBloc counterBloc = BlocProvider.of<IncrementBloc>(context);
final ApplicationBloc appBloc = BlocProvider.of<ApplicationBloc>(context);
...
}
}
为什么不使用InheritedWidget?
很多关于BLoC的文章,都将一个InheritedWidget实现为了Provider。
当然,这么做是可行的,但是:
- 一个InheritedWidget没有提供任何
dispose()
方法,但是,在不需要某个资源的时候及时释放它是一个好的编程实践。 - 没有谁会拦着你在一个StatefulWidget中放一个InheritedWidget,但是请考虑一下,这么做会不会增加什么负担呢?
- 如果控制的不好,InheritedWidget会产生副作用(下面会讲)。
Flutter无法实例化范型,所以我们需要将BLoC实例传递给BlocProvider。为了在每一个BLoC中执行
dispose()
,所有BLoCs都需要实现BlocBase
接口。
我们在使用InheritedWidget的context.inheritFromWidgetOfExactType(…)
方法来获取制定类型的widget时,每当InheritedWidget的父级或者子布局发生变化,这个方法会自动将当前“context”(= BuildContext)注册到要重建的widget当中。关联至BuildContext的Widget类型(Stateful还是Stateless)并不重要。
BLoC的一些缺点
BLoC模式起初是希望用在跨平台(如AngularDart)分享代码上的。BLoC没有getter/setter概念,只有sinks/streams,所以说:“rely on exclusive use of Streams for both input (Sink) and output (stream)”,BLoC只依赖于sinks和streams。
我们用两个例子来说一下BLoC的缺点:
- 我们从BLoC中获取数据,将数据作为页面的输入源,依赖于Streams异步build页面,但是有时候这种方式并不是很优雅:
class FiltersPage extends StatefulWidget {
@override
FiltersPageState createState() => FiltersPageState();
}
class FiltersPageState extends State<FiltersPage> {
MovieCatalogBloc _movieBloc;
double _minReleaseDate;
double _maxReleaseDate;
MovieGenre _movieGenre;
bool _isInit = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 在 initState() 的时候,我们是拿不到 context 的。
// 如果还没有初始化,我们通过_getFilterparameters获取参数
if (_isInit == false){
_movieBloc = BlocProvider.of<MovieCatalogBloc>(context);
_getFilterParameters();
}
}
@override
Widget build(BuildContext context) {
return _isInit == false
? container()
: Scaffold(
...
);
}
// 这么写是为了完全100%遵循 BLoC 的规则,我们所有的数据都必须是从Streams里面获取的。
// 这很不优雅,这个例子看看就好,只是一个学习性的例子展示
void _getFilterParameters() {
StreamSubscription subscriptionFilters;
subscriptionFilters = _movieBloc.outFilters.listen((MovieFilters filters) {
_minReleaseDate = filters.minReleaseDate.toDouble();
_maxReleaseDate = filters.maxReleaseDate.toDouble();
// Simply to make sure the subscriptions are released
subscriptionFilters.cancel();
// Now that we have all parameters, we may build the actual page
if (mounted){
setState((){
_isInit = true;
});
}
});
});
}
}
- 在BLoC级别,我们有时候还需要注入一些假数据,来触发stream提供你想要获得的数据:
class ApplicationBloc implements BlocBase {
// 提供movie genres的同步Stream
StreamController<List<MovieGenre>> _syncController = StreamController<List<MovieGenre>>.broadcast();
Stream<List<MovieGenre>> get outMovieGenres => _syncController.stream;
// 假的数据处理
StreamController<List<MovieGenre>> _cmdController = StreamController<List<MovieGenre>>.broadcast();
StreamSink get getMovieGenres => _cmdController.sink;
ApplicationBloc() {
// 如果我们通过这个sink接收到了任意数据,我们简单地提供一个MovieGenre列表作为输出流
_cmdController.stream.listen((_){
_syncController.sink.add(Unmodifiablelistview<MovieGenre>(_genresList.genres));
});
}
void dispose(){
_syncController.close();
_cmdController.close();
}
MovieGenresList _genresList;
}
// 外部调用的例子
BlocProvider.of<ApplicationBloc>(context).getMovieGenres.add(null);
一个实践Demo
大佬构建了一个伪应用程序来展示如何使用所有这些概念。 完整的源代码可以在Github上找到。
相关阅读
淘宝猜你喜欢是淘宝内部类似于淘金币的一种推广方式,当你购买了一件商品之后,淘宝会根据你的浏览痕迹以及下单的情况来给你推荐商品
很多场合下,产品经理都会被问到一个经典问题–什么样的产品才算是个好产品?今天我来尝试做下回答:个人觉得关于“好,坏”的判断
自从支付宝推出花呗当面花服务后,不少用户发现,竟然可以使用花呗在未开通花呗付款的线下店家进行支付了,不过每月只有500元的限额。
Word/Excel/PPT2007电脑异常导致文件丢失,如何恢复?
正在使用Office2007办公软件的过程中,突然停电或死机、蓝屏等异常现象发生,文件根本没来得及保存,怎么办?Office2007软件(包含Word、Ex
好文章是从标题开始的,没有一个好的标题,内容再好也会被人忽略,无人关注。就如一个美女,不会打扮自己,再美的容颜也会被邋遢的妆容给掩