flutter Stream

767 阅读7分钟

背景

在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维护组件状态、通信方面各有其优劣势,配合使用能写出更加优雅的代码。