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将:
变为异步的; 围绕Streams和listeners的概念来构建; 当任意事件、变量等等发生变化时,会向Stream发送一个通知; Stream的监听者无论位于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的使用注意点和好处是:
将业务逻辑(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( 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); } }
责任分离: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 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
static Type _typeOf() => T; }
class _BlocProviderState extends State
@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(context);。 多个Bloc的使用
每一个有业务逻辑的页面的顶层都应该有自己的BLoC; 每一个“足够复杂的组建(complex enough component)”都应该有相应的BLoC; 可以使用一个ApplicationBloc来处理整个App的状态。 下面的例子展示了在整个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。 当然,这么做是可行的,但是:
一个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 { 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;
});
}
});
});
复制代码
} }
在BLoC级别,我们有时候还需要注入一些假数据,来触发stream提供你想要获得的数据:
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上找到。