Dart中创建Stream

Dart中创建Stream

可以从以下几个方法来创建流

  • 转变已有的Stream
  • 使用async* 关键字创建流
  • 使用StreamController来创建流

1.转变已有的Stream

在已经有了一个流,想基于原始流事件创建一个新的流。例如,希望通过UTF-8对输入进行解码,将一个字节流转换为字符串流。最常用的方法是创建一个新流,该流等待原始流上的事件,然后输出新的事件。

Stream<String> lines(Stream<String> source) async* {
  var partial = '';
  await for (var chunk in source) {
    var lines = chunk.split('\n');
    lines[0] = partial + lines[0];
    partial = lines.removeLast();
    for (var line in lines) {
      yield line;
    }
  }
  if (partial.isNotEmpty) yield partial;
}

例如,假设你有一个流,counterStream,每秒释放一个递增的计数器。下面是如何实现的:

var counterStram =
      Stream<int>.periodic(Duration(seconds: 1), (x) => x).take(15);
  counterStram.forEach(print);

要转换流事件,您可以在监听流之前在流上调用转换方法,如map()。该方法返回一个新的流。

var counterStram =
      Stream<int>.periodic(Duration(seconds: 1), (x) => x).take(15);
var doubleCounterStream = counterStram.map((event) => event * 2);
doubleCounterStream.forEach(print);

通常,你只需要一个转换方法。但是,如果您需要对转换进行更多的控制,您可以使用Stream 的 transform()方法指定StreamTransformer。平台库为许多常见任务提供流转换器。例如,下面的代码使用了由dart:convert库提供的utf8.decoder和LineSplitter转换器。

main(List<String> args) async {
  Stream<List<int>> content = File(r'.\t6.dart').openRead();
  List<String> lines =
      await content.transform(utf8.decoder).transform(LineSplitter()).toList();
  lines.forEach((element) {
    print(element);
  });
}

2.从头开始创建流

创建新流的一种方法是使用异步生成器(async*)函数。流是在调用函数时创建的,当侦听流时,函数的主体开始运行。当函数返回时,流关闭。在函数返回之前,它可以使用yieldyield*语句在流上发出事件。
下面是一个基本的示例,它定期发出数字.

Stream<int> timedCounter(Duration interval, [int maxCount]) async* {
  int i = 0;
  while (true) {
    await Future.delayed(interval);
    yield i++;
    if (i == maxCount) break;
  }
}

main(List<String> args) async{
  var stream = timedCounter(Duration(seconds: 2), 5);
  await for (var i in stream) {
    print(i);
  }
}

当侦听器取消(通过在listen()方法返回的StreamSubscription对象上调用cancel())时,那么下一次主体到达yield语句时,yield将充当返回语句。执行任何封闭的finally块,函数退出。如果函数试图在退出前生成一个值,则会失败。
当该函数最终退出时,cancel()方法返回的future就完成了。如果函数因错误而退出,则将来会因该错误而结束;否则,它以null结束。
以下示例是将Future序列转化为一个Stream

Stream<T> streamFromFutures<T>(Iterable<Future<T>> futures) async* {
	for(var future in futures){
		var result = await future;
		yield result;
	}
}

这个函数请求可迭代的future来获得一个新的future,等待那个future,发出结果值,然后进行循环。如果一个future完成时出现了错误,那么流也会随着该错误完成。
用一个async函数从零开始构建流是很少见的。它需要从某个地方获取数据,而这个地方通常是另一个流。在某些情况下,如上面的Futures序列,数据来自其他异步事件源。然而,在许多情况下,async函数过于简单,无法轻松处理多个数据源, 这就是StreamController(流控制器)类存在的原因。

3.流控制器

如果流的事件来自程序的不同部分,而不只是来自可以由异步函数遍历的Stream或Future,则使用StreamController创建和填充流。
StreamController提供了一个新流,以及在任何点和任何地方向流中添加事件的方法。流具有处理侦听器和暂停所需的所有逻辑。返回Stream或者保持Controller完全可以自己控制。
下面的示例(来自流控制器bad.dart)展示了流控制器的基本用法(尽管有缺陷),以实现前面示例中的timedCounter()函数。这段代码创建一个要返回的流,然后根据计时器事件(既不是Future事件也不是Stream
事件)将数据提供给它。

import 'dart:async';

Stream<int> timedCounter(Duration interval, [int maxCount]) {
  var controller = StreamController<int>();
  int counter = 0;
  void tick(Timer timer) {
    counter++;
    controller.add(counter);
    if (maxCount != null && counter >= maxCount) {
      timer.cancel();
      controller.close();
    }
  }

  Timer.periodic(interval, tick);
  return controller.stream;
}

main(List<String> args) {
  var countStream = timedCounter(Duration(seconds: 1), 10);
  countStream.listen(print);
}

以上代码存的timedCounter()存在两个问题。

  • 在订阅者接收之前已经开始生产事件
  • 当订阅者暂停,它仍然持续生产事件

在接下来的内容中,将使用onListenonPause来解决上述问题。

3.1等待订阅

作为一个规则,流应该等待订阅者开工作才开始生产事件。

import 'dart:async';

Stream<int> timedCounter(Duration interval, [int maxCount]) {
  var controller = StreamController<int>();
  int counter = 0;
  void tick(Timer timer) {
    counter++;
    controller.add(counter);
    if (maxCount != null && counter >= maxCount) {
      timer.cancel();
      controller.close();
    }
  }

  Timer.periodic(interval, tick);
  return controller.stream;
}

void listenAfterDelay() async {
  var counterStream = timedCounter(const Duration(seconds: 1), 15);
  await Future.delayed(const Duration(seconds: 5));
  await for (int n in counterStream) {
    print(n);
  }
}

main(List<String> args) {
  listenAfterDelay();
}

当这段代码运行时,在前5秒内没有打印任何东西,但是流已经在工作中。所以当暂停5秒结束,流中已经缓存了5个事件。所以观察输出结果可以发现前5个结果几乎同时输出。

3.2遵守暂停状态

为了避免在侦听器请求暂停时产生事件,async* 函数自动在yield 处暂停。但是StreamController在订阅者暂停时也持续生产事件,这样可能会导致流的缓冲区无限增长。

void listenWithPause() {
  var counterStream = timedCounter(const Duration(seconds: 1), 15);
  StreamSubscription<int> subscription;
  subscription = counterStream.listen((counter) {
    print(counter);
    if (counter == 5) {
      subscription.pause(Future.delayed(const Duration(seconds: 5)));
    }
  });
}

以上例子中,当subscription暂停5秒时,Stream仍然在持续生产。
以下版本的timedCounter()通过使用StreamController上的onListen、onPause、onResume和onCancel回调来实现暂停。

import 'dart:async';

Stream<int> timedCounter(Duration interval, [int maxCount]) {
  StreamController<int> controller;
  Timer timer;
  int counter = 0;
  void tick(_) {
    counter++;
    controller.add(counter);
    if (counter == maxCount) {
      timer.cancel();
      controller.close();
    }
  }

  void startTimer() {
    timer = Timer.periodic(interval, tick);
  }

  void stopTimer() {
    if (timer != null) {
      timer.cancel();
      timer = null;
    }
  }

  controller = StreamController<int>(
      onListen: startTimer,
      onPause: stopTimer,
      onResume: startTimer,
      onCancel: stopTimer);

  return controller.stream;
}

void listenWithPause() {
  var counterStream = timedCounter(const Duration(seconds: 1), 15);
  StreamSubscription<int> subscription;
  subscription = counterStream.listen((counter) {
    print(counter);
    if (counter == 5) {
      subscription.pause(Future.delayed(const Duration(seconds: 5)));
    }
  });
}

main(List<String> args) {
  listenWithPause();
}

上述例子中,当订阅者执行pause时,Stream同时也会执行onPause方法实现暂停生产事件。

总结

当不使用async*函数来创建Stream时,需要记住以下几点:

  • 避免使用同步controllerStreamController(sync: true)。当向未暂停的同步控制器上发送事件时,改事件会立即送到所有的流侦听器上。在添加侦听器代码完全返回之前是不能调用侦听器的,但是同步控制器可能会破坏这一规则。
  • 如果使用StreamController,则在listen调用返回StreamSubscription之前调用onListen回调。不要让onListen回调依赖于已经存在的订阅。
  • StreamController定义的onListen、onPause、onResume和onCancel回调在流侦听器状态改变时被流调用,但不会在别的改变状态的回调函数执行期间调用。在这些情况下,状态更改回调被延迟,直到前一个回调完成。
  • 不要尝试自己实现流接口。在事件、回调以及添加和删除侦听器之间进行交互很容易出错。始终使用现有流(可能来自StreamController)来实现新流的listen调用。
  • 尽管可以通过扩展Stream类并在上面实现listen方法和额外功能来创建具有更多功能的扩展Stream的类,但通常不建议这样做,因为它引入了用户必须考虑的新类型。通常的做法是创建一个类,该类中又一个stream成员变量。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值