该文已受权公众号 「码个蛋」,转载请指明出处 讲了那么多的部件,这节我打算来点不太同样的,可能会没有部件那么好理解,也多是我讲的不够简单明了,总之系好安全带,咱们要准备开车了。react
在 dart
部分记得分享过 Stream
的文章连接,可是我知道大家确定没几个愿意看的,因此这里再提下。仍是得从源码开始...由于源码的注释比较长,就不贴注释了,能够本身看,我这边就提取一些关键信息。git
Stream
是 Dart
提供的一种数据流订阅管理的"工具",感受有点像 Android
中的 EventBus
或者 RxBus
,Stream
能够接收任何对象,包括是另一个 Stream
,接收的对象经过 StreamController
的 sink
进行添加,而后经过 StreamController
发送给 Stream
,经过 listen
进行监听,listen
会返回一个 StreamSubscription
对象,StreamSubscription
能够操做对数据流的监听,例如 pause
,resume
,cancel
等。github
Stream
分两种类型:安全
Single-subscription Stream
:单订阅 stream,整个生命周期只容许有一个监听,若是该监听 cancel 了,也不能再添加另外一个监听,并且只有当有监听了,才会发送数据,主要用于文件 IO
流的读取等。Broadcast Stream
:广播订阅 stream,容许有多个监听,当添加了监听后,若是流中有数据存在就能够监听到数据,这种类型,不论是否有监听,只要有数据就会发送,用于须要多个监听的状况。仍是看下例子会比较直观markdown
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(),
);
}
}
复制代码
看下控制台的输出:app
果真把全部的数据都打印出来了,前面有说过,单订阅的 stream 只有当 listen
后才会发送数据,不试试我仍是不相信的,咱们把 _sink.add
放到 listen
前面去执行,再看控制台的打印结果。竟然真的是同样的,Google 粑粑果真诚不欺我。接着试下 pause
,resume
方法,看下数据如何监听,修改代码less
_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 被堵住了,数据不继续发送了。ide
接下来看下广播订阅 stream,对代码作下修改工具
StreamController _controller = StreamController.broadcast();
Sink _sink;
StreamSubscription _subscription;
@override
void initState() {
super.initState();
_sink = _controller.sink;
_sink.add('A');
_subscription = _controller.stream.listen((data) => print('Listener: $data'));
_sink.add(11);
_subscription.pause();
_sink.add(11.16);
_subscription.resume();
_sink.add([1, 2, 3]);
_sink.add({'a': 1, 'b': 2});
}
// ...
}
复制代码
咱们再看下控制台的打印:源码分析
你猜对答案了吗,这边作下小总结:
单订阅 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
的基本属性后,就能够继续往下了~
####StreamBuilder
前面提到了 stream 经过 listen
进行监听数据的变化,Flutter
就为咱们提供了这么个部件 StreamBuilder
专门用于监听 stream 的变化,而后自动刷新重建。接着来看下源码
const StreamBuilder({
Key key,
this.initialData, // 初始数据,不传入则为 null
Stream<T> stream,
@required this.builder
}) : assert(builder != null),
super(key: key, stream: stream);
@override
AsyncSnapshot<T> initial() => AsyncSnapshot<T>.withData(ConnectionState.none, initialData);
复制代码
StreamBuilder
必须传入一个 AsyncWidgetBuilder
参数,初始值 initialData
可为空, stream
用于监听数据变化,initial
方法的调用在其父类 StremBuilderBase
中,接着看下 StreamBuilderBaseState
的源码,这里我删除一些没必要要的源码,方便查看,完整的源码可自行查看
class _StreamBuilderBaseState<T, S> extends State<StreamBuilderBase<T, S>> {
// ...
@override
void initState() {
super.initState();
_summary = widget.initial(); // 经过传入的初始值生成默认值,若是没有传入则会是 null
_subscribe(); // 注册传入的 stream,用于监听变化
}
// _summary 为监听到的数据
@override
Widget build(BuildContext context) => widget.build(context, _summary);
// ...
void _subscribe() {
if (widget.stream != null) {
// stream 经过外部传入,对数据的变化进行监听,
// 在不一样回调中,经过 setState 进行更新 _summary
// 当 _summary 更新后,因为调用了 setState,从新调用 build 方法,将最新的 _summary 传递出去
_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); //
}
}
}
复制代码
在以前更新数据都须要经过 setState
进行更新,这里了解完了 stream
,咱们就不使用 setState
更新,使用 Stream
来更新
class _StreamHomeState extends State<StreamHome> {
// 定义一个全局的 `StreamController`
StreamController<int> _controller = StreamController.broadcast();
// `sink` 用于传入新的数据
Sink<int> _sink;
int _counter = 0;
@override
void initState() {
super.initState();
_sink = _controller.sink;
}
@override
void dispose() {
super.dispose();
// 须要销毁资源
_sink.close();
_controller.close();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
alignment: Alignment.center,
child: StreamBuilder(
builder: (_, snapshot) => Text('${snapshot.data}', style: TextStyle(fontSize: 24.0)),
stream: _controller.stream, // stream 在 StreamBuilder 销毁的时候会自动销毁
initialData: _counter,
),
)),
// 经过 `sink` 传入新的数据,去通知 `stream` 更新到 builder 中
floatingActionButton: FloatingActionButton(
onPressed: () => _sink.add(_counter++),
child: Icon(Icons.add),
),
);
}
}
复制代码
那么当点击按钮的时候,就会刷新界面上的值,经过上面的源码分析,StreamBuilder
也是经过 setState
方法进行刷新,那么两种方法孰优孰劣呢,固然是经过 Stream
啦,这不是废话吗。由于经过调用 setState
刷新的话,会把整个界面都进行重构,可是经过 StreamBuilder
的话,只刷新其 builder
,这样效率就更高了,最后看小效果吧,所谓有图有真相嘛
这一步,咱们摒弃了 setState
方法,那么下一步,咱们试试把 StatefulWidget
替换成 StatelessWidget
吧,并且官方也推荐使用 StatelessWidget
替换 StatefulWidget
,这里就须要提下 BLoC
模式了。
说实话,如今 Google 下 「flutter bloc」能搜到不少文章,基本上都是经过 InheritedWidget
来实现的,例如这篇Flutter | 状态管理探索篇——BLoC(三),可是 InheritedWidget
没有提供 dispose
方法,那么就会存在 StreamController
不能及时销毁等问题,因此,参考了一篇国外的文章,Reactive Programming - Streams - BLoC 这里经过使用 StatefulWidget
来实现,当该部件销毁的时候,能够在其 dispose
方法中及时销毁 StreamController
,这里我仍是先当个搬运工,搬下大佬为咱们实现好的基类
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 的值
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
中
void main() => runApp(StreamApp());
class StreamApp 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. 实现了状态管理
由于上面的参考文章中提到了 RxDart
,我的以为有必要了解下,固然目前也有不少文章介绍 RxDart
,因此我就讲下和 BLoC
有点关系的部分吧。RxDart
须要经过引入插件的方式引入(rxdart: ^0.21.0
)
若是须要查看详细的内容,我这里提供几篇文章连接
RxDart: Magical transformations of Streams
其实 RxDart 就是对 Stream 的进一步分装,RxDart 提供了三种 Subject,其功能相似 Stream 中的单订阅 stream 和 广播 stream。
PublishSubject
/// PublishSubject is, by default, a broadcast (aka hot) controller, in order
/// to fulfill the Rx Subject contract. This means the Subject's `stream` can
/// be listened to multiple times.
复制代码
经过注释能够发现 PuslishSubject
不可被屡次订阅,尽管实现是经过 StreamController<T>.broadcast
方式实现,其实三种都是经过 broadcast
方式实现的,因此实现的功能就是相似 Single-subscription Stream
的功能。
BehaviorSubject
/// BehaviorSubject is, by default, a broadcast (aka hot) controller, in order
/// to fulfill the Rx Subject contract. This means the Subject's `stream` can
/// be listened to multiple times.
复制代码
BehaviorSubject
能够被屡次订阅,那么这个就是实现了 Broadcast Stream
功能。
ReplaySubject
/// ReplaySubject is, by default, a broadcast (aka hot) controller, in order
/// to fulfill the Rx Subject contract. This means the Subject's `stream` can
/// be listened to multiple times.
复制代码
ReplaySubject
其实也是实现 Broadcast Stream
功能,那么它和 BehaviorSubject
的区别在哪呢,别急,等我慢慢讲。
/// As items are added to the subject, the ReplaySubject will store them.
/// When the stream is listened to, those recorded items will be emitted to
/// the listener.
复制代码
当有数据添加了,可是尚未监听的时候,它会将数据存储下来,等到有监听了,再发送出去,也就是说,ReplaySubject
实现了 Brodacast Stream
的多订阅功能,同时也实现了 Single-subscription Stream
的存储数据的功能,每次添加了新的监听,都可以获取到所有的数据。固然,这还不是它的所有功能,它还能够设置最大的监听数量,会只监听最新的几个数据,在注释中,提供了这么两个例子,能够看下
/// ### Example
///
/// final subject = new ReplaySubject<int>();
///
/// subject.add(1);
/// subject.add(2);
/// subject.add(3);
///
/// subject.stream.listen(print); // prints 1, 2, 3
/// subject.stream.listen(print); // prints 1, 2, 3
/// subject.stream.listen(print); // prints 1, 2, 3
///
/// ### Example with maxSize
///
/// final subject = new ReplaySubject<int>(maxSize: 2); // 实现监听数量限制
///
/// subject.add(1);
/// subject.add(2);
/// subject.add(3);
///
/// subject.stream.listen(print); // prints 2, 3
/// subject.stream.listen(print); // prints 2, 3
/// subject.stream.listen(print); // prints 2, 3
复制代码
那么咱们可使用 RxDart
对前面使用 Stream
实现的例子进行替换,最简单的其实只须要使用 BehaviorSubject
替换 StreamController.broadcast()
就能够了,别的都不须要变化。可是 RxDart
有本身的变量,仍是按照 RxDart
的方式来
// 继承自 StreamController,因此 StreamController 拥有的属性都有
BehaviorSubject<int> _countController = BehaviorSubject();
// StreamController<int> _countController = StreamController.broadcast();
// 继承自 Stream,因此这里直接用以前 stream 的写法也没问题,可是这样就有点不 RxDart 了
Observable<int> get countStream => Observable(_countController.stream);
// Stream<int> get countStream => _countController.stream;
void dispatch(int value) {
_count = value;
// 直接提供了 add 方法,不须要经过 sink 来添加
_countController.add(_count);
// _countController.sink.add(_count);
}
复制代码
再次运行仍是能过实现相同的效果。若是说要在 RxDart
和 Stream
两种实现方式中选择一种,我的更偏向于 RxDart
,由于它对 Stream
进行了进一步的封装,提供了更多更方便的数据转换方法,并且链式的写法真的很舒服,用过了就停不下来,具体的方法介绍能够参考上面提供的连接。
说实话本身封装 BLoC
来实现分离逻辑和界面,相对仍是有点难度的,这边能够经过第三方来实现,这边推荐 Google 粑粑的库,flutter_provide,看下官方对关键部件和静态方法的介绍
Provide<T>
- Widget used to obtain values from aProviderNode
higher up in the widget tree and rebuild on change. TheProvide<T>
widget should only be used withStream
s orListenable
s. Equivalent toScopedModelDescendant
inScopedModel
Provide.value<T>
- Static method used to get a value from aProviderNode
using theBuildContext
. This will not rebuild on change. Similar to manually writing a static.of()
method for anInheritedWidget
.Provide.stream<T>
- Static method used to get aStream
from aProviderNode
. Only works if eitherT
is listenable, or if theProvider
comes from aStream
.Provider<T>
- A class that returns a typed value on demand. Stored in aProviderNode
to allow retrieval usingProvide
.ProviderNode
- The equivalent of theScopedModel
widget. ContainsProviders
which can be found as anInheritedWidget
.
Provide
这个部件主要用于从上层的 ProvideNode
中获取值,当变化的时候刷新重建,只能同 Stream
和 Listenable
一同使用,相似于 ScopeMode
中的 ScopedModelDescendant
。(这个部件放在须要状态管理的部件的上层,例若有个 Text
须要修改状态,那么就须要在外层提供一个 Provide
部件,经过内部 builder
参数返回 Text
部件)
Provide.value
是个静态方法,用于从 ProvideNode
获取值,可是当接收的值改变的时候不会重建。相似于 InheritedWidget
的静态方法 of
。(这个方法用于获取指定类型的 provide,每一个 provide 都须要提供一个数据类,该类 with ChangeNotifier
,当数据变化的时候经过 notifyListeners
通知 provide 变化,进行刷新重建)
Provide.stream
是个静态方法,用于从 ProvideNode
获取一个 stream
,仅在 T 可被监听,或者 Provide 来自 stream 的状况下有效。(这个一般结合 StreamBuilder
使用,StreamBuilder
在上面已经提到,就很少说了)
Provider
按须要的类型返回相关值的类,存储在 ProviderNode
中方便 Provide
进行检索。(这个类主要是将咱们本身建立的数据类经过 function
等方法转换成 Provider
,并在 Providers
中进行注册)
ProvideNode
相似于 ScopedModel
的一个部件,包含全部能被查找的 Providers
。(这个须要放在顶层,方便下面的容器进行查找 provider,刷新相应的部件,通常放在 MaterialApp
上层)
这边再补充一个我的以为关键的类 Providers
,这个类主要用于存储定义的 Provider
,主要是在创建 MaterialApp
的时候将须要用到的 Provider
经过 provide
方法添加进去存储起来,而后在 ProvideNode
中注册全部的 provider
方便下层容器获取值,并调用。
说那么多,还不如直接看个例子直接,代码来了~,首先须要创建一个相似 BLoC
中监听数据变化的 counter_bloc
类的数据管理类,咱们这边定义为 count_provider
须要混入 ChangeNotifier
类
class CountProvider with ChangeNotifier {
int _value = 0; // 存储的数据,也是咱们须要管理的状态值
int get value => _value; // 获取状态值
void changeValue(int value) {
_value = value;
notifyListeners(); // 当状态值发生变化的时候,经过该方法刷新重建部件
}
}
复制代码
而后须要将定义的类注册到全局的 Providers
中
void main() {
final providers = Providers()
// 将咱们建立的数据管理类,经过 Provider.function 方法转换成 Provider,
// 而后添加到 Providers 中
..provide(Provider.function((_) => CountProvider()));
// 在 App 上层,经过包裹一层 ProvideNode,并将咱们生成的 Providers 实例
// 注册到 ProvideNode 中去,这样整个 App 均可以经过 Provide.value 查找相关的 Provider
// 找到 Provider 后就能够找到咱们的数据管理类
runApp(ProviderNode(child: StreamApp(), providers: providers));
}
复制代码
接着就是替换咱们的界面实现了,前面经过 BLoC
实现,这里替换成 Provide
来实现
class StreamHome extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
alignment: Alignment.center,
// 经过指定类型,获取特定的 Provide,这个 Provide 会返回咱们的数据管理类 provider
// 经过内部定义的方法,获取到须要展现的值
child: Provide<CountProvider>(builder: (_, widget, provider) => Text('${provider.value}')),
)),
floatingActionButton: FloatingActionButton(
onPressed: () =>
// 经过 value 方法获取到咱们的数据管理类 provider,
// 经过调用改变值的方法,修改内部的值,并通知界面刷新重建
Provide.value<CountProvider>(context).changeValue(
Provide.value<CountProvider>(context).value + 1),
child: Icon(Icons.add))
);
}
}
复制代码
本文代码查看 bloc
包名下的全部文件,须要单独运行 stream_main.dart
文件
最后运行后仍是同样的效果,也摒弃了 StatefulWidget
部件和 SetState
方法,实现了逻辑和界面分离。可是 Provide
最终仍是经过 InheritedWidget
来实现,固然在资源方面 Google 的大佬们作了一些相关的处理,至于如何处理,这边就很少说了。目前 provide
的这个库还存在一点争议的地方,具体查看 issue#3,可是目前来看并无太大的影响。固然你不放心的话,可使用 Scoped_model
或者上面的 Bloc
模式,Google 在文档也有相关的注明
If you must choose a package today, it's safer to go with
package:scoped_model
than with this package.
这篇概念性的比较多,可是等理解了之后,对于之后的开发仍是很是有利的。
最后代码的地址仍是要的:
文章中涉及的代码:demos
基于郭神 cool weather
接口的一个项目,实现 BLoC
模式,实现状态管理:flutter_weather
一个课程(当时买了想看下代码规范的,代码更新会比较慢,虽然是跟着课上的一些写代码,可是仍是作了本身的修改,不少地方看着不舒服,而后就改为本身的实现方式了):flutter_shop
若是对你有帮助的话,记得给个 Star,先谢过,你的承认就是支持我继续写下去的动力~