背景
在Flutter项目里,我们最终选择Provider作为状态管理。随着各个业务接入,组件、页面之间的通信逐渐变得复杂。更多场景涉及到模块解耦、模块复用。Provider ViewModel之间的数据通讯变的更加必要。Provider侧重于组件的状态以及刷新,虽然addListen可以做到不涉及到组件状态的通信,但addListen监听是class级别,每次调用notifyListeners都会被addListen监听到,过滤掉无用通知又过于繁琐,且notifyListeners并不能携带通信数据,增加项目维护成本。Stream是Dart响应式编程中重要部分,刚好完善了Provider通信能力较差的部分。
Stream简介
什么是Stream
Dart异步编程提供了Future、Stream两种方案。Future通常是异步函数返回的一个结果,只会执行一次,Stream是一系列异步事件的序列,当开始订阅Stream时,并不能立即获取到事件,等到Stream准备好之后才会通知订阅者。
StreamController
Stream一般使用StreamController来创建,StreamController内包含StreamSink和Stream,StreamSink为事件的入口,通过sink.add添加事件,stream.listen来订阅事件,并返回一个Subscription订阅者,subscription可以取消、暂停、回复订阅等操作。
Stream单订阅、广播订阅
Stream分为单订阅、广播订阅,单订阅只允许一个订阅者,广播订阅允许多个订阅者。广播订阅在订阅时,并不会接收到订阅之前发送的事件。
// 单订阅
StreamController<int> sc = StreamController();
sc.add(event); // 添加事件
sc.stream.listen((event) {}); // 监听事件
sc.close();
// 广播订阅
StreamController<int> broadcastSC = StreamController.broadcast(
sync: true,
onListen: () => print('onListen'),
onCancel: () => print('onCancel'),
);
broadcastSC.stream.listen((event) => print('first listener $event'));
broadcastSC.sink.add(10);
// second listener 并不会接收订阅之前的事件 10
broadcastSC.stream.listen((event) => print('second listener $event'));
broadcastSC.sink.add(20);
broadcastSC.close();
flutter: onListen
flutter: first listener 10
flutter: first listener 20
flutter: second listener 20
flutter: onCancel
StreamController源码解析
StreamController初始化方法会生成_SyncStreamController或_AsyncStreamController,以上流程图是_SyncStreamController执行过程。由于class _SyncStreamController = _StreamController with _SyncStreamControllerDispatch;,_StreamController中实现了StreamController,所以我们只需要关注_StreamController内部方法和属性就可以了。_StreamController实现了stream、sink属性,用来处理监听,添加事件。
监听
_StreamController提供了一个_ControllerStream类型的Stream,当调用StreamImpl listen监听时,通过_ControllerStream _createSubscription() ==> _StreamControllerLifecycle _subscribe() ==> _StreamController _subscribe() 流程创建一个_ControllerSubscription订阅者,并通过_varData保存了订阅者。
发送事件
_StreamSinkWrapper add()添加事件最终也是走到了_StreamController add(),两者功能是一样的,在拿到_StreamController保存的subscription之后,通过_zone执行subscription中保存的_onData监听回调。
Zone是一个代码执行的环境,在该环境中可以捕获、拦截或修改一些代码行为。
广播订阅
广播订阅与单订阅整体思路是一样的,很重要的差别在于订阅者_BroadcastSubscription是双向链表,包含_next、_previous订阅者,在获取到第一个订阅者后,遍历链表执行依次监听回调,从而实现广播订阅。
_BroadcastSubscription<T>? subscription = _firstSubscription;
while (subscription != null) {
// 执行监听回调
action(subscription)
subscription = subscription._next;
}
异步
和同步不同点是在subscription执行监听回调,异步是调用_addPending()方法,然后通过scheduleMicrotask(void Funtion() callback)异步执行监听回调任务。
Stream使用场景
属性级别的监听
假如我们只关注width属性的变化,当调用changeWidth、changeHeight时,Provider addListener都会监听到,addListener需要做额外的逻辑才能控制不处理changeHeight通知,wSC.stream.listen((event) {});只会监听到width的变化,不受changeHeight的影响。
class TestViewModel1 extends ChangeNotifier {
var w = 10;
var h = 10;
StreamController<int> wSC = StreamController();
StreamController<int> hSC = StreamController();
changeWidth(int val) {
w = val;
wSC.sink.add(val);
// notifyListeners();
}
changeHeight(int val) {
h = val;
hSC.sink.add(val);
// notifyListeners();
}
}
实现viewModel之间的通信,降低耦合度
当页面业务过于复杂时,我们通常会拆分成多个组件、viewModel,而多个组件、viewModel之间也会存在交互情况。如果直接在组件中读取其他组件的viewModel,会增加耦合度。在page组件中监听viewModel属性变化,去修改其他viewModel的状态值,这样组件、viewModel之间没有相互引用的关系,降低耦合度便于移植。
// 当对ViewModel拆分,TestViewModel2中状态又受到TestViewModel1的影响时
// 在组件中监听vm1去修改vm2
vm1.wSC.stream.listen((event) {
vm2.changePrice(event);
});
class TestViewModel2 extends ChangeNotifier {
double price = 10;
changePrice(int width) {
price = width / 10 + 10;
notifyListeners();
}
}
StreamBuilder 局部刷新组件
在不使用Provider Selector的情况下,StreamBuilder也可以做到不手动创建StatefulWidget时局部刷新组件。StreamBuilder继承自StatefulWidget,在didUpdateWidget方法中订阅stream,实现局部刷新,不过StreamBuilder并未提供同时监听多个数据流,需要手动去实现。
StreamController<int> areaSC = StreamController();
changeWidth(int val) {
w = val;
wSC.sink.add(val);
areaSC.sink.add(w * h);
}
changeHeight(int val) {
h = val;
hSC.sink.add(val);
areaSC.sink.add(w * h);
}
@override
Widget build(BuildContext context) {
return StreamBuilder(
builder: (context, sp) => Text(sp.toString()),
stream: areaSC.stream,
initialData: 10,
);
}
Stream变换
在实际业务中,Stream添加的event存在不能被直接使用情况,需要变换之后才能用使用,而且如果多处监听需要同样的处理,就要写重复代码。Stream提供了map、where、expand等一系列转换操作,从源头进行数据变换。
class A {
int val;
A(this.val);
}
class B {}
// map
StreamController<int> sc = StreamController();
sc.sink.add(10);
Stream<A> get aStream => sc.stream.map((e) => A(e));
// aStream print: A(10)
// where
StreamController sc = StreamController();
Stream<A> get aStream => sc.stream.where((e) => e is A).cast<A>();
sc.sink.add(A(10));
sc.sink.add(B());
sc.sink.add(A(20))
// aStream print:A(10)、A(20)
take、periodic、filter ...
EventBus实现
利用Stream广播订阅能力,通过where对类型的过滤,实现一个简单的全局通知。封装通用EventBus,方便其他业务快速接入Stream。
StreamController _streamController = StreamController.broadcast();
Stream<T> on<T>() {
if (T == dynamic) {
return _streamController.stream as Stream<T>;
} else {
return _streamController.stream.where((event) => event is T).cast<T>();
}
}
StreamContoller使用注意事项
Stream has already been listened to.错误
对单订阅Stream进行了多次监听 sc.stream.listen((event){})。在下面的例子中setState() StreamBuilder会重建多次,同样是多次监听,StreamBuilder内部订阅了Stream
StreamController<int> sc = StreamController();
setState(() {
isShow = !isShow;
})
isShow
? StreamBuilder(
builder: (ct, sp) => Container(), initialData: 0, stream: sc.stream)
: Container();
避免上述问题可以用广播Stream StreamController sc = StreamController.broadcast();
如果一定要使用单订阅Stream,最好在监听前使用hasListener判断,
if (sc.hasListener) {
sc.close();
sc = StreamController();
}
sc.stream.listen((event) {});
Cannot add event after closing 错误
在stream被关闭之后,依然向stream中添加事件,如果不能够保证在添加事件前StreamController没有被关闭,要在添加事件前增加isClosed判断。
// 网络请求等异步操作在dispose之后依然执行
Future.delayed(Duration(seconds: 1)).then((val) {
// sc已经被close 出现 Cannot add event after closing错误
sc.sink.add(false);
// 添加事件前判断是否被close
if (!sc.isClosed) sc.sink.add(false);
});
// 页面销毁等操作 dispose中controller被提前close
sc.close();
Cannot add event while adding a stream 错误
不能在添加stream的时候同时添加event
sc.sink.addStream(Stream.value(true));
sc.add(false);
RxDart、StreamController对比
RxDart是StreamController的基础上扩展,提供了非常好用的方法,解决了StreamController使用不方便或不好实现的功能。我们可以将Subject看成StreamController,可以从以下几个情形了解RxDart。
BehaviorSubject
在上面提到StreamController在监听之后并不能收到监听前的数据,BehaviorSubject可以收到监听前最后一次添加的事件。
BehaviorSubject subject = BehaviorSubject<int>();
subject.stream.listen((value) {
print('Observer1: $value');
});
subject.add(1);
subject.add(2);
subject.stream.listen((value) {
print('Observer2: $value');
});
subject.add(3);
print:
flutter: Observer1: 1
flutter: Observer2: 2
flutter: Observer2: 3
flutter: Observer1: 2
flutter: Observer1: 3
CombineLatest组合多个状态值
面积area值由width、height状态值决定时,使用combineLatest组合成一个stream,width、height变化都会被监听到。
late BehaviorSubject<int> _widthSubject;
late BehaviorSubject<int> _heightSubject;
Stream<int> get areaStream => Rx.combineLatest<int, int>(
[_widthSubject.stream, _heightSubject.stream],
(values) => values.fold(1, (w, h) => w * h));
TestBloc({int width = 20, int height = 20}) {
_widthSubject = BehaviorSubject.seeded(width);
_heightSubject = BehaviorSubject.seeded(height);
}
StreamBuilder<int>(
initialData: 0,
stream: _bloc.areaStream,
builder: (context, snapshot) {
return Text(
'${snapshot.data}',
style: Theme.of(context).textTheme.headline4,
);
});
debounce防抖、throttle节流
debounce在规定时间内只能触发一次,在该时间内有新事件,重新开始计算时间
throttle在规定时间呢只触发一次,不会重新计算时间
短时间内多次触发changeWidth()时,组件会渲染多次,可能会展现出抖动的效果,debounce可以避免这种情况,_increment()只会触发一次事件
Stream<int> get areaStream => Rx.combineLatest<int, int>(
[_widthSubject.stream, _heightSubject.stream],
(values) => values.fold(1, (w, h) => w * h))
.debounceTime(Duration(milliseconds: 300));
changeWidth(int width) {
_widthSubject.add(width);
}
_increment() async {
_bloc.changeWidth(40);
await Future.delayed(Duration(milliseconds: 200));
_bloc.changeWidth(50);
}
print:
flutter: area:1000
总结
Provider侧重于组件状态,组件与数据紧密的结合,Stream是响应式编程重要组成部分,
提供了单次订阅、广播订阅,stream筛选、转换等功能,Stream订阅者可以是任何类、组件,这使得类与类、类与组件等之间通信、解耦更加便利。RxDart则是对Stream功能进一步完善,让代码更加简洁。Provider、Stream维护组件状态、通信方面各有其优劣势,配合使用能写出更加优雅的代码。