我们都知道,Flutter中Widget的状态控制了UI的更新,比如最常见的StatefulWidget,通过调用setState({})
方法来刷新控件。那么其他类型的控件,比如StatelessWidget就不能更新状态来吗?答案当然是肯定可以的。前文已经介绍过几种状态管理
Stream
Stream
是 Dart
提供的一种数据流订阅管理的"工具",感觉有点像 Android
中的 EventBus
或者 RxBus
,Stream
可以接收任何对象,包括是另外一个 Stream
,接收的对象通过 StreamController
的 sink
进行添加,然后通过 StreamController
发送给 Stream
,通过 listen
进行监听,listen
会返回一个 StreamSubscription
对象,StreamSubscription
可以操作对数据流的监听,例如 pause
,resume
,cancel
等。
Stream
分两种类型:
-
Single-subscription Stream
:单订阅 stream,整个生命周期只允许有一个监听,如果该监听 cancel 了,也不能再添加另一个监听,而且只有当有监听了,才会发送数据,主要用于文件IO
流的读取等。 -
Broadcast Stream
:广播订阅 stream,允许有多个监听,当添加了监听后,如果流中有数据存在就可以监听到数据,这种类型,不管是否有监听,只要有数据就会发送,用于需要多个监听的情况。
class _StreamHomeState extends State<StreamHome> { StreamController _controller = StreamController(); // 创建单订阅类型 `StreamController` Sink _sink; StreamSubscription _subscription; @override void initState() { super.initState(); _sink = _controller.sink; // _sink 用于添加数据 // _controller.stream 会返回一个单订阅 stream, // 通过 listen 返回 StreamSubscription,用于操作流的监听操作 _subscription = _controller.stream.listen((data) => print('Listener: $data')); // 添加数据,stream 会通过 `listen` 方法打印 _sink.add('A'); _sink.add(11); _sink.add(11.16); _sink.add([1, 2, 3]); _sink.add({'a': 1, 'b': 2}); } @override void dispose() { super.dispose(); // 最后要释放资源... _sink.close(); _controller.close(); _subscription.cancel(); } @override Widget build(BuildContext context) { return Scaffold( body: Container(), ); } }
运行后看下控制台的输出:
果然把所有的数据都打印出来了,前面有说过,单订阅的 stream 只有当listen
后才会发送数据,不试试我还是不相信的,我们把 _sink.add
放到 listen
前面去执行,再看控制台的打印结果。居然真的是一样的,Google 粑粑果然诚不欺我。
接着试下 pause
,resume
方法,看下数据如何监听,修改代码:
_sink = _controller.sink; _subscription = _controller.stream.listen((data) => print('Listener: $data')); _sink.add('A'); _subscription.pause(); // 暂停监听 _sink.add(11); _sink.add(11.16); _subscription.resume(); // 恢复监听 _sink.add([1, 2, 3]); _sink.add({'a': 1, 'b': 2});再看控制台的打印,你们可以先猜下是什么结果,我猜大部分人都会觉得应该是不会有 11 和 11.16 打印出来了。然而事实并非这样,打印的结果并未发生变化,也就是说,调用
pause
方法后,stream 被堵住了,数据不继续发送了。
接下来看下广播订阅 stream,对代码做下修改:
StreamController _controller = StreamController.broadcast(); //广播订阅 stream Sink _sink; StreamSubscription _subscription; @override void initState() { super.initState(); _sink = _controller.sink; // _sink 用于添加数据 _sink.add('A'); _subscription = _controller.stream.listen((data) => print('Listener: $data')); // 添加数据,stream 会通过 `listen` 方法打印 _sink.add(11); _subscription.pause(); _sink.add(11.16); _subscription.resume(); _sink.add([1, 2, 3]); _sink.add({'a': 1, 'b': 2}); } @override void dispose() { super.dispose(); // 最后要释放资源... _sink.close(); _controller.close(); _subscription.cancel(); }
我们再看下控制台的打印:
总结:
单订阅 Stream 只有当存在监听的时候,才发送数据,广播订阅 Stream 则不考虑这点,有数据就发送;当监听调用 pause 以后,不管哪种类型的 stream 都会停止发送数据,当 resume 之后,把前面存着的数据都发送出去。
sink 可以接受任何类型的数据,也可以通过泛型对传入的数据进行限制,比如我们对 StreamController
进行类型指定 StreamController<int> _controller = StreamController.broadcast();
因为没有对 Sink
的类型进行限制,还是可以添加除了 int
外的类型参数,但是运行的时候就会报错,_controller
对你传入的参数做了类型判定,拒绝进入。
Stream
中还提供了很多 StremTransformer
,用于对监听到的数据进行处理,比如我们发送 0~19 的 20 个数据,只接受大于 10 的前 5 个数据,那么可以对 stream 如下操作:
_subscription = _controller.stream .where((value) => value > 10) .take(5) .listen((data) => print('Listen: $data')); List.generate(20, (index) => _sink.add(index));
那么打印出来的数据如下图:
除了where
,take
还有很多 Transformer
, 例如 map
,skip
等等,小伙伴们可以自行研究。了解了 Stream
的基本属性后,就可以继续往下了~我们上面已经说了,Stream的特性就是当数据源发生变化的时候,会通知订阅者,那么我们是不是可以延展一下,实现当数据源发生变化时,改变控件状态,通知控件刷新的效果呢?Flutter为我们提供了
StreamBuilder
。所以,StreamBuilder是Stream在UI方面的一种使用场景,通过它我们可以在非StatefulWidget中保存状态,同时在状态改变时及时地刷新UI。
StreamBuilder
StreamBuilder其实是一个StatefulWidget,它通过监听Stream,发现有数据输出时,自动重建,调用builder方法。前面提到了 stream 通过 listen
进行监听数据的变化,Flutter
就为我们提供了这么个部件 StreamBuilder
专门用于监听 stream 的变化,然后自动刷新重建。接着来看下源码
StreamBuilder<T>( key: ...可选... stream: ...需要监听的stream... initialData: ...初始数据,否则为空... builder: (BuildContext context, AsyncSnapshot<T> snapshot){ if (snapshot.hasData){ return ...基于snapshot.hasData返回的控件 } return ...没有数据的时候返回的控件 }, )
下面是一个模仿官方自带demo“计数器”的一个例子,使用了StreamBuilder,而不需要任何setState:
import 'package:flutter/material.dart'; import 'dart:async'; 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了解下
上一步,我们摒弃了setState
方法,那么下一步,我们试试把 StatefulWidget
替换成 StatelessWidget
吧,而且官方也推荐使用 StatelessWidget
替换 StatefulWidget
,这里就需要提下 BLoC
模式了。
BLoC是Business Logic Component(业务逻辑组建)的缩写,就是将UI与业务逻辑分离,有点MVC的味道。说实话,现在 Google 下 「flutter bloc」能搜到很多文章,基本上都是通过
InheritedWidget
来实现的,但是 InheritedWidget
没有提供 dispose
方法,那么就会存在 StreamController
不能及时销毁等问题,所以,参考了一篇国外的文章,Reactive Programming - Streams - BLoC 这里通过使用 StatefulWidget
来实现,当该部件销毁的时候,可以在其 dispose
方法中及时销毁 StreamController
,这里我还是先当个搬运工,搬下大佬为我们实现好的基类
import 'package:flutter/material.dart';
abstract class BaseBloc { void dispose(); // 该方法用于及时销毁资源 } class BlocProvider<T extends BaseBloc> extends StatefulWidget { final Widget child; // 这个 `widget` 在 stream 接收到通知的时候刷新 final T bloc; BlocProvider({Key key, @required this.child, @required this.bloc}) : super(key: key); @override _BlocProviderState<T> createState() => _BlocProviderState<T>(); // 该方法用于返回 Bloc 实例 static T of<T extends BaseBloc>(BuildContext context) { final type = _typeOf<BlocProvider<T>>(); // 获取当前 Bloc 的类型 // 通过类型获取相应的 Provider,再通过 Provider 获取 bloc 实例 BlocProvider<T> provider = context.ancestorWidgetOfExactType(type); return provider.bloc; } static Type _typeOf<T>() => T; } class _BlocProviderState<T> extends State<BlocProvider<BaseBloc>> { @override void dispose() { widget.bloc.dispose(); // 及时销毁资源 super.dispose(); } @override Widget build(BuildContext context) { return widget.child; } }
接着我们对前面的例子使用 BLoC
进行修改。
首先,我们需要创建一个 Bloc
类,用于修改 count 的值:
import '../widget/baseBloc.dart';
import 'dart:async';
class CounterBloc extends BaseBloc { int _count = 0; int get count => _count; // stream StreamController<int> _countController = StreamController.broadcast(); Stream<int> get countStream => _countController.stream; // 用于 StreamBuilder 的 stream void dispatch(int value) { _count = value; _countController.sink.add(_count); // 用于通知修改值 } @override void dispose() { _countController.close(); // 注销资源 } }
在使用 Bloc
前,需要在最上层的容器中进行注册,也就是 MaterialApp
中.
import 'package:flutter/material.dart'; import './widget/baseBloc.dart'; import './bloc/counter.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { // 这里对创建的 bloc 类进行注册,如果说有多个 bloc 类的话,可以通过 child 进行嵌套注册即可 // 放在最顶层,可以全局调用,当 App 关闭后,销毁所有的 Bloc 资源, // 也可以在路由跳转的时候进行注册,至于在哪里注册,完全看需求 // 例如实现主题色的切换,则需要在全局定义,当切换主题色的时候全局切换 // 又比如只有某个或者某几个特殊界面调用,那么完全可以通过在路由跳转的时候注册 return BlocProvider( child: MaterialApp( debugShowCheckedModeBanner: false, home: StreamHome(), ), bloc: CounterBloc()); } } class StreamHome extends StatelessWidget { @override Widget build(BuildContext context) { // 获取注册的 bloc,必须先注册,再去查找 final CounterBloc _bloc = BlocProvider.of<CounterBloc>(context); return Scaffold( body: SafeArea( child: Container( alignment: Alignment.center, child: StreamBuilder( initialData: _bloc.count, stream: _bloc.countStream, builder: (_, snapshot) => Text('${snapshot.data}', style: TextStyle(fontSize: 20.0)), ), )), floatingActionButton: // 通过 bloc 中的 dispatch 方法进行值的修改,通知 stream 刷新界面 FloatingActionButton(onPressed: () => _bloc.dispatch(_bloc.count + 1), child: Icon(Icons.add)), ); } }
重新运行后,查看效果还是一样的。所以我们成功的对 StatefulWidget
进行了替换。
先总结下 Bloc:
1. 成功的把页面和逻辑分离开了,页面只展示数据,逻辑通过 BLoC 进行处理
2. 减少了 setState
方法的使用,提高了性能
3. 实现了状态管理
多个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); ... } }