Dart 之Stream

289 阅读9分钟

Dart之Stream.png

前言

什么是Stream呢?我第一次看到这个单词的时候想到的就是流,但是感觉还是很不理解,流又是什么呢,后来我联系了一下想到了水流。带着这个联系我开始了解Dart中的Stream,发现这个命名也太符合了,和我们认为的水流差不多,只不过它是一条可由代码控制的异步水流,它既能像水流一样动态生成数据(如用户输入、网络响应),又能通过订阅主动控制数据的接收,甚至还可以暂停或关闭。是不是感到有点不可思议了?那我们一起去详细了解了解吧。

一、为什么需要Stream?

学习一门新知识时,要先明白这个新知识的重要性,让我们足够重视它,然后再建立掌握新知识的学习目标。所以下面我们就一起看看Stream的重要性吧。

  • 满足持续性异步数据流需求: Stream能够很好的解决持续、动态、按节奏生成的数据。
  • 解决单次异步操作的内存安全问题: 能够处理单次异步操作(Future)处理大规模数据带来的内存安全问题(通过分块处理)。

二、Stream定义

Stream 是Dart中表示连续异步数据序列的对象,用于处理多个按时间顺序传递的异步事件。是不是感到头大?说了等于没有说。我第一次看到的感觉也是这样,于是我尝试将它与我所知道的知识进行联系,最后发现它和水管中的水比较符合,只不过水不是异步的。下面我们就用水管中水的例子来一起了解一下。

例子:想象一下,水管中的水不就是连续的吗?它不就是按照时间顺序传递的吗?只不过它不是异步的。

Dart之Stream图片.png

三、Stream的分类

Stream使用订阅模式展开,按照订阅的个数分为单订阅流和广播流,就像这个水管只能有一个人用和多个人用。

  • 单订阅流:只允许有一个订阅者订阅。
  • 广播流:允许多个订阅者同时订阅。

按照数据生成和订阅者的关系分为冷流和热流,以水管中的水理解冷流就是只有当有人用才会有水,而热流是不管你要不要用这水管中的水它都会有水流淌。

  • 冷流:有订阅者时才生产数据。
  • 热流:无论是否有订阅者都会一直生产数据。

四、Stream的创建

Stream的创建就是水管中加水的过程,其有三种创建方式,分别是StreamController创建、九大构造函数创建和异步生成器async* 创建。下面先了解一下Stream都具有哪些属性,然后再分别介绍如何创建Stream。

3.1、Stream的属性

Stream表示连续异步数据序列的对象。看到序列?是不是和集合有点关系啊。先来看看它的属性都有哪些。

  • isBroadcast:用于标识stream是否为广播流。
  • isEmpty:判断流是否为空。
  • first:获取流的第一个数据事件,如果流为空,则报错。
  • last:获取流的最后一个数据事件,如果流为空,则报错。
  • single:判断流中是否只有一个数据事件,如果为空或有多个数据事件,则报错。
  • length:流中数据事件的个数。

注意:isBroadcast的类型为bool类型而不是Future<bool>,isBroadcast和isEmpty为原生属性,是直接定义在Stream内部的,其他则是扩展的。

3.2、StreamController创建

StreamController创建流提供了写入端(sink属性)和读取端(stream属性),下面我们先看看它都具有哪些属性。

  • sink:类型为StreamSink,其是数据的入口,用于添加数据。
  • stream:类型为Stream表示连续异步数据序列的对象,其是供外部监听数据的。
  • isClosed:标识控制器是否关闭。
  • isPaused:标识流是否暂停了。
  • hasListener:标识是否有监听者。
  • done:类型为Future<void>,用于控制器关闭时执行回调。

示例:

StreamController _streamController = StreamController();

Stream _streamWithController = _streamController.stream;
// print(_streamWithController.isEmpty);
StreamSink _sinkWithController = _streamController.sink;
_sinkWithController.add(1); // 使用Sink属性添加数据。
_sinkWithController.add(2);
_sinkWithController.add(3);
_sinkWithController.addError(Exception('错误')); // 添加错误信息

_sinkWithController.pause(); // 暂停流
print(_streamController.isPaused()); // 输出:true

_sinkWithController.resum(); // 恢复流
_sinkWithController.close(); // 关闭流

3.3、九大构造函数创建

通过构造方法可以快速创建特定类型的Stream。

  1. Stream.fromIterable(Iterable<T> elements)
  • 将集合(Iterable)中的元素按顺序转换为流。

示例: 将字符串类型的列表转换为流

Stream<String> _streamWithIterable = Stream.fromIterable(['apple','bunana','tea','peach','strawberry']);
  1. Stream.formFuture(Future<T> future)
  • 将一个Future转换为流。

示例:

Stream<String> _streamWithFutures = Stream.fromFutures(Future(()=>'第一个'));
  1. Stream.formFutures(Iterable<Future<T>> futures)
  • 将多个Future转换为流。

示例:

Stream<String> _streamWithFutures = Stream.fromFutures([Future(()=>'第一个'),Future(()=>'第二个')]);
  1. Stream.periodic(Duration period,[T Function(int)? computation])
  • 周期性生成流,其参数computation是可以为空的。

示例:

Stream _streamWithPeriodic = Stream.periodic(Duration(milliseconds: 300));
  1. Stream.value(T value)
  • 快速创建包含单个值的流,用于快速包装简单值。

示例:

Stream _streamWithValue = Stream.value('Hello Dart!');
  1. Stream.error(Object error,[StackTrace? stackTrace])
  • 创建一个包含错误值的流。

示例:

Stream _streamWithError = Stream.error(Exception('错误'));
  1. Stream.empty()
  • 创建一个不产生任何事件的空Stream,用于占位或条件分支中无数据的场景。

示例:

Stream _streamWithEmpty = Stream.empty();
  1. Stream.multi(void onListen(StreamController<T> controller))
  • 手动控制流事件,用于动态生成数据或自定义复杂逻辑,核心是通过StreamController创建流。

示例:

Stream<int> _streamWithMulti = Stream.multi((controller){
  controller.add(5);
  controller.add(120);
  controller.sink.add(23);
  controller.close();
});
  1. Stream.eventTransformed(Stream source, EventSink<T> transform(EventSink<T> sink))
  • 通过自定义EventSink转换流事件。

示例:

Stream _streamWithEventTransformed =
    Stream.eventTransformed(Stream.value(5), (sink) {
  sink.add(5);
  return sink;
});

3.4、async*创建

通过异步生成器函数创建,需要配合yield关键字。记忆方法:和使用async创建Future一样,只是Stream的创建上多个*

示例:

Stream<String> buildStream() async* {
  int count = 1;
  while (true) {
    await Future.delayed(const Duration(seconds: 2));
    yield '第$count个';
    count++;
    if (count > 10) {
      throw Exception('当前$count超出范围大于10');
    }
  }
}

五、Stream的处理

Stream的处理和生活中可能需要对水管中的水做过滤、截流等操作一致。Dart中提供了许多方法进行处理,下面我们逐步去看看都可以对其做哪些处理。

5.1、选择、跳过、转换、过滤

  • take(int count):选取前count个数据。
  • skip(int count):跳过前count个数据。
  • map<S>(S Function(T) convert):同步转换每个数据事件。
  • asyncMap<S>(S Function(T) convert):异步转换每个数据事件。
  • where(bool Function(T) test):过滤不需要的数据事件。

示例:

Stream<int> numStream = Stream.fromIterable([1,2,3,4,5]);
Stream<int> takeStream = numStream.take(4); // 获取流中前4个
Stream<int> skipStream = numStream.skip(2); // 跳过前面2个
Stream<int> addOneStream = numStream.map((num)=>num+1); // 同步对每个数据加1
Stream<int> addTwoStream = numStream.asyncMap((num)=>num+2); // 异步对每个数据加2
Stream<int> whereStream = numStream.where((num)=>num>4); // 过滤大于4的

5.2、扩展、去重、按需求选择或跳过

  • expand<S>(Iterable<S> Function(T) convert):将数据事件按需求展开为多个事件。
  • distinct([bool Function(T,T)? equals]):重复的数据事件只选择一次。
  • takeWhile(bool Function(T) test):根据自定义的条件选取数据事件。
  • skipWhile(bool Function(T) test):根据自定义的条件跳过数据事件。

示例:

Stream<int> numStream = Stream.fromIterable([1,2,2,3,4,5]);
// 扩展所有数据,num值为1时扩展后为 1,2;num值为2时扩展后为 2,4。
Stream<int> expandStream = numStream.expand((num) => [num,num*2]);
// 去重重复的数据事件2。
Stream<int> distinctStream = numStream.distinct();
// 选择数据事件中小于3的。
Stream<int> takeWhileStream = numStream.takeWhile((num)=>num<3);
// 跳过数据事件等于2的。
Stream<int> skipWhileStream = numStream.skipWhile((num)=>num==2);

5.3、转换、传输、清理

  • asBroadcastStream():将单订阅流转换为广播流。
  • cast<S>:将流的数据类型强制转换为指定的类型。
  • transform<S>(StreamTransformer<T,S> transformer):应用自定义的转换器。
  • pipe(StreamConsumer<T> consumer):将流数据传输到StreamConsumer。
  • drain<T>([T? futureValue]):资源清理,确保流完全消费。

示例: transform和pipe后续文章介绍。

Stream<int> numStream = Stream.fromIterable([1,2,2,3,4,5]);
Stream<int> asBroadcastStream = numStream.asBroadcastStream(); // 转换为广播流
Stream<double> doubleStream = numStream.cast<double>(); // 转换数据类型为double
numStream.drain(); // 确保流完全消费

5.4、检查、遍历执行、聚合、拼接

  • contains(Object? value):检查流是否包含指定值,返回类型为Future<bool>
  • every(bool Function(T) test):检查流中数据是否都满足条件,返回类型为Future<bool>
  • forEach(void Function(T) action):对流中每个执行操作,action参数为具体执行的操作,返回类型为Future<void>
  • reduce(T Function(T,T) combine):聚合所有数据为单个结果。
  • join([String separator = '']):将流中数据拼接为字符串。

示例:

Stream<int> numStream = Stream.fromIterable([1,2,2,3,4,5]);
// 检查是否包含4
Future<bool> isContainsFour = numStream.contains(4); 
// 检查流中所有数据是否都大于1
Future<bool> isValid = numStream.every((num) => num > 1); 
// 遍历流中所有数据并执行加2的操作。
Future<void> a = numStream.forEach((num)=>num+2);
// 将流中所有数据相加
Future<int> sum = numStream.reduce((a,b) => a + b);
// 以‘,’号分隔将流中数据拼接为字符串。
Future<String> joinString = numStream.join(',');

六、Stream的侦听

Stream的侦听指的是当订阅了流时,在流发出数据(data)、错误(error)或完成(done)事件时执行相应的回调函数。就像水管中的水顺利到达(流发出数据)、中途发生管子破裂(错误)、用完水(完成)时执行的相应的处理。

Dart中主要通过listen进行Stream侦听处理,其返回为StreamSubscription的对象。下面主要介绍一下流订阅对象的一些方法。

  • onData():流发出数据时的回调函数。
  • onError():发生错误时的回调处理。
  • onDone():完成时的回调处理。
  • pause():暂停订阅。
  • resume():恢复订阅。
  • cancel():取消订阅。

示例:

Stream<int> numStream = Stream.fromIterable([1,2,2,3,4,5]);
StreamSubscription<int> numStreamSubscription = numStream.listen((data)=>print);
// 出现错误时
numStreamSubscription.onError((error)=>print);
// 流发出数据时
numStreamSubscription.onData((data)=>print);
// 完成时
numStreamSubscription.onDone(()=>print('完成'));
numStreamSubscription.pause(); // 暂停订阅
numStreamSubscription.resume(); // 恢复订阅
numStreamSubscription.cancel(); // 取消订阅

除了StreamSubscription对象提供处理外,listen()也提供了参数处理相应的回调。

语法如下:

StreamSubscription<T> subscription = stream.listen(
  (T event) { /* 处理数据 */ },
  onError: (Object error) { /* 处理错误 */ },
  onDone: () { /* 流结束 */ },
  cancelOnError: false, // 是否在遇到错误时自动取消订阅
);

七、总结

本小节从生活中水管中的水流的例子出发,首先明确了Stream是一条由代码控制的异步水流,其次说明了Stream的不可或缺,然后介绍流的定义和流的分类,最后介绍了Stream的创建、处理、侦听。下面是本小节的归纳总结,以便于阅读者查缺补漏。

流的创建流的处理流的侦听
1、StreamController创建:
sink写入端、stream读取端
stream属性:
isBroadcastisEmpty
firstlastsinglelength
StreamController属性:
sinkstream
isClosedisPaused
hasListenerdone
2、构造函数创建:
fromIterable()
formFuture()fromFutures()
periodic()value()error()
empty()multi()
eventTransformed()
3、async*创建
1、选择、跳过、转换、过滤:
take()skip()map()
asyncMap()where()
2、扩展、去重、按需求选择或跳过:
expand()distinct()
takeWhile()skipWhile()
3、转换、传输、清理:
asBroadcastStream()
cast()transform()
pipe()drain()
4、检查、遍历执行、聚合、拼接:
contains()every()
forEach()reduce()join()



listen()
onData()
onError()
onDone()
pause()
resume()
cancel()