参考文章: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 ctrl = StreamController.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将:
什么是StreamBuilder
StreamBuilder其实是一个StatefulWidget,它通过监听Stream,发现有数据输出时,自动重建,调用builder方法。
StreamBuilder(
key: ...可选...
stream: ...需要监听的stream...
initialData: ...初始数据,否则为空...
builder: (BuildContext context, AsyncSnapshot 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 {
int _counter = 0;
final StreamController _streamController = StreamController();
@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( // 监听Stream,每次值改变的时候,更新Text中的内容
stream: _streamController.stream,
initialData: _counter,
builder: (BuildContext context, AsyncSnapshot 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的使用注意点和好处是:
Streams
的使用;下图是BLoC与Widget交互的简单示意图:
image.png
这么做的好处显而易见:
多亏了业务逻辑和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(
bloc: IncrementBloc(),
child: CounterPage(),
),
);
}
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final IncrementBloc bloc = BlocProvider.of(context);
return Scaffold(
appBar: AppBar(title: Text('Stream version of the Counter App')),
body: Center(
child: StreamBuilder(
// StreamBuilder控件中没有任何处理业务逻辑的代码
stream: bloc.outCounter,
initialData: 0,
builder: (BuildContext context, AsyncSnapshot 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 _counterController = StreamController();
StreamSink get _inAdd => _counterController.sink;
Stream 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);
}
}
IncrementBloc
类中进行。如果你要修改业务逻辑,只需要修改 _handleLogic()
方法就行了,无论处理过程多么复杂,CounterPage
都不需要知道,不需要关心。IncrementBloc
类即可。.incrementCounter sink
来传入即可;也可以在任何页面的任何位置来展示counter,只需要监听.outCounter stream
。StreamBuilder
放弃setState()
大大减少了build的数量,因为只需要build需要刷新的控件,从性能角度来讲是一个重大的提升。关于Bloc的可访问性
以上所有功能的实现,都依赖于一点,那就是Bloc必须是可访问的。
有很多方法都可以保证Bloc的可访问性:
dispose()
中释放它。BlocProvider
的实现:// 所有 BLoCs 的通用接口
abstract class BlocBase {
void dispose();
}
// 通用 BLoC provider
class BlocProvider extends StatefulWidget {
BlocProvider({
Key key,
@required this.child,
@required this.bloc,
}): super(key: key);
final T bloc;
final Widget child;
@override
_BlocProviderState createState() => _BlocProviderState();
static T of(BuildContext context){
final type = _typeOf>();
BlocProvider provider = context.ancestorWidgetOfExactType(type);
return provider.bloc;
}
static Type _typeOf() => T;
}
class _BlocProviderState extends State>{
@override
void dispose(){
widget.bloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context){
return widget.child;
}
}
怎么使用这个BlocProvider呢?
home: BlocProvider(
bloc: IncrementBloc(),
child: CounterPage(),
),
这段代码实例化了一个新的BlocProvider,用于处理IncrementBloc
,然后见CounterPage
渲染为一个子控件。那么,BlocProvider下的任意一个子树(sub-tree)都可以访问IncrementBloc
,访问方式为:IncrementBloc bloc = BlocProvider.of
。
多个Bloc的使用
ApplicationBloc
来处理整个App的状态。ApplicationBloc
,在CounterPage的顶层使用IncrementBloc
:void main() => runApp(
BlocProvider(
bloc: ApplicationBloc(),
child: MyApp(),
)
);
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context){
return MaterialApp(
title: 'Streams Demo',
home: BlocProvider(
bloc: IncrementBloc(),
child: CounterPage(),
),
);
}
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context){
final IncrementBloc counterBloc = BlocProvider.of(context);
final ApplicationBloc appBloc = BlocProvider.of(context);
...
}
}
为什么不使用InheritedWidget?
很多关于BLoC的文章,都将一个InheritedWidget实现为了Provider。
当然,这么做是可行的,但是:
dispose()
方法,但是,在不需要某个资源的时候及时释放它是一个好的编程实践。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的缺点:
class FiltersPage extends StatefulWidget {
@override
FiltersPageState createState() => FiltersPageState();
}
class FiltersPageState extends State {
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(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;
});
}
});
});
}
}
class ApplicationBloc implements BlocBase {
// 提供movie genres的同步Stream
StreamController> _syncController = StreamController>.broadcast();
Stream> get outMovieGenres => _syncController.stream;
// 假的数据处理
StreamController> _cmdController = StreamController>.broadcast();
StreamSink get getMovieGenres => _cmdController.sink;
ApplicationBloc() {
// 如果我们通过这个sink接收到了任意数据,我们简单地提供一个MovieGenre列表作为输出流
_cmdController.stream.listen((_){
_syncController.sink.add(UnmodifiableListView(_genresList.genres));
});
}
void dispose(){
_syncController.close();
_cmdController.close();
}
MovieGenresList _genresList;
}
// 外部调用的例子
BlocProvider.of(context).getMovieGenres.add(null);
一个实践Demo
大佬构建了一个伪应用程序来展示如何使用所有这些概念。 完整的源代码可以在Github上找到。