一个简单的Flutter应用程序|显示来自流的数据

127 阅读7分钟

一个简单的Flutter应用程序,显示来自流的数据

这篇文章涵盖了反应式编程的基本原理之一:流,它是Stream类型的对象。

如果你读过我们以前关于期货的文章,你可能记得每个期货代表 一个异步交付的单一值 (错误或数据)。流工作原理类似,只是不是一个单一的东西,而是可以一段时间交付零个或多个值和错误

这篇文章首次发表于2020年2月。这个版本将包含的代码更新为null安全。

本文是基于Flutter in Focus视频系列《Dart中的异步编程》的第三篇文章。第一篇文章,隔离器和事件循环,涵盖了Dart支持后台工作的基础。第二篇,Futures,讨论了Future类。

如果你喜欢通过看或听来学习,这篇文章中的所有内容在下面的视频中都有涉及。

medium.com/media/19f75…

注意:本文中的代码已经更新,以反映最佳实践和Dart语言的变化(包括空值安全),这些变化发生在2019年6月28日视频发布之后。

如果你想一想单个值与同一类型的迭代器的关系,这就是future与流的关系:future代表单个请求与单个响应,而流则代表单个请求与多个响应。

就像期货一样,关键是提前决定1)当一块数据准备好时,2)当出现错误时,3)当流完成时,该做什么。与期货一样,在这个过程中,Dart事件循环仍然在运行。

流与Dart事件循环一起工作。

事件循环掐头去尾

如果你使用文件类的openRead()方法从一个文件中读取数据,例如,这个方法返回一个流。

数据块被从磁盘上读取并到达事件循环。Dart库看着它们说:"啊,有人在等这个,"把数据添加到流中,并把它发送给你的应用程序。

当另一个数据到达时--它进去了,又出来了。基于定时器的流和来自网络套接字的流数据也与事件循环一起工作,使用时钟和网络事件。

事件循环会对数据进行排序。

倾听流的声音

接下来要了解的是如何处理流提供的数据。

假设你有一个类,它给你一个流,每秒发出一个新的整数(1、2、3、4、5...)。你可以使用listen()方法来订阅这个流。唯一需要的参数是一个函数。

final myStream = NumberCreator().stream;
final subscription = myStream.listen(
    (data) => print(‘Data: $data’),
);

每当流发出一个新的值时,该函数就会被调用并打印出该值。

Data: 1
Data: 2
Data: 3
Data: 4
...

这就是listen()的工作方式。

重要提示:默认情况下,流被设置为单一订阅。在有人订阅之前,它们会保留自己的值,而且在整个生命周期内只允许一个监听器。如果你试图听一个流两次,你会得到一个异常。

幸运的是,Dart也提供广播流。你可以使用asBroadcastStream()方法,从一个单一的订阅流中制作一个广播流。广播流的工作原理与单一订阅流相同,但它们可以有多个听众。

广播流的另一个区别是:如果在一个数据准备好的时候没有人在听,那么这个数据就会被扔掉。

final myStream = NumberCreator().stream;
final subscription = myStream.listen(
  (data) => print(‘Data: $data’),
);
final subscription2 = myStream.listen(
  (data) => print(‘Data again: $data’),
);

让我们回到第一个listen()调用,因为还有几件事情要谈。

如前所述,流可以产生错误,就像期货一样。通过在listen()调用中添加onError函数,你可以捕捉和处理任何错误。

还有一个cancelOnError 属性,默认为真,但可以设置为假,以便在出错后仍然保持订阅。

你可以添加onDone函数,以便在流完成发送数据时执行一些代码,例如当一个文件被完全读取时。

有了所有这四个参数的组合--onError、onDone、cancelOnError和所需的参数(onData)--你可以提前为发生的任何事情做好准备。

final myStream = NumberCreator().stream;
final subscription = myStream.listen(
  (data){
    print(‘Data: $data’);
},
onError: (err) {
  print(‘Error!’);
},
cancelOnError: false,
onDone: () {
  print(‘Done!’):
 },
);

提示:listen()返回的对象有它自己的一些有用的方法。它被称为StreamSubscription,你可以用它来暂停、恢复、甚至取消数据流。

final subscription = myStream.listen(…);
subscription.pause();
subscription.resume();
subscription.cancel();

使用和操作流

现在你知道了如何使用listen()来订阅一个流并接收数据事件,让我们来谈谈是什么让流变得真正酷:操纵它们。

一旦你在一个流中得到了数据,有很多操作突然变得流畅和优雅。

让我们回到刚才的数字流。

使用一个叫做map()的方法*,*你可以从流中获取每个值,并将其快速转换为其他东西。给map()一个函数来做转换,它就会返回一个新的流,其类型与函数的返回值一致。

现在不是一个整数流,而是一个字符串流。在最后抛出一个listen()调用,把print()函数传给它,现在它直接从流中打印出字符串--异步地,因为它们到达了。

NumberCreator().stream
    .map((i) =>String $i’)
    .listen(print) ;
String 1
String 2
String 3
String 4
*/

有很多方法可以像这样连锁起来。例如,如果你只想打印偶数,你可以使用where()来过滤流。给它一个测试函数,为每个元素返回一个布尔值,然后它返回一个新的流,只包括通过测试的值。

NumberCreator().stream
    .where((i) => i % 2 == 0)
    .map((i) =>String $i’)
    .listen(print) ;
String 2
String 4
String 6
String 8

distinct()方法是另一个好方法。对于使用Redux商店的应用程序,该商店会在onChange流中发出新的应用程序状态对象。

你可以使用map()将状态对象流转换为应用程序的一个部分的视图模型流。然后你可以使用distinct()方法来获得一个流,过滤掉连续的相同的值(以防商店踢出一个不影响视图模型中的数据子集的变化)。

然后,每当你得到一个新的视图模型时,你可以监听并更新用户界面。

myReduxStore.onChange
    .map((s) => MyViewModel(s))
    .distinct()
    .listen( /* update UI */ )

Dart中还内置了其他方法,你可以用来塑造和修改你的数据流。另外,当你准备好使用更高级的功能时,还有Dart团队维护的async包,可在pub.dev上使用。它有一些类,可以将两个流合并在一起,缓存结果,并执行其他类型的基于流的向导。

试试async包,了解更多基于流的魔法。

对于更多的流魔法,看看stream_transform包

创建流

最后,一个更高级的话题值得一提,那就是如何创建你自己的流。

就像期货一样,大多数时候你会使用由网络库、文件库、状态管理等为你创建的流,但你也可以使用StreamController创建自己的流。

让我们回到到目前为止我们一直在使用的NumberCreator例子。下面是它的实际代码。

class NumberCreator {
  NumberCreator() {
    Timer.periodic(const Duration(seconds: 1), (timer) {
      _controller.sink.add(_count);
     _count += 1;
   });
 }
 final _controller = StreamController<int>();
 var _count = 0;
 Stream<int> get stream => _controller.stream;
}

正如你所看到的,它保持一个运行中的计数,并使用一个定时器每秒递增该计数。不过,有趣的是,流控制器。

StreamController从头开始创建一个全新的流,并让你访问它的两端。这就是流的一端,也就是数据到达的地方。(我们在本文中一直在使用这个控制器)。

Stream get stream => _controller.stream;

还有一个sink端,是新数据被添加到流中的地方。

_controller.sink.add(_count)。

NumberCreator同时使用它们。当计时器关闭时,它将最新的计数添加到控制器的汇中,然后它用一个公共属性公开控制器的流,以便其他对象可以订阅它。

使用流构建Flutter小部件

现在我们已经涵盖了创建、操作和监听流的内容,让我们谈谈如何在Flutter中使用它们来构建小部件。

如果你读了之前关于Futures的文章,你可能记得FutureBuilder。你给它一个未来和一个构建方法,它就会根据未来的状态来构建小部件。

对于流,有一个类似的小部件,叫做StreamBuilder。给它一个像数字创造者那样的流和一个构建器方法,每当流发出一个新的值时,它就会重建它的孩子。

StreamBuilder<String>(
  stream: NumberCreator().stream.map((i) =>String $i’),
  builder: (context, snapshot) {
    // Build some widgets
    throw UnimplementedError(“Case not handled yet”);
  },
);

快照参数是一个AsyncSnapshot,就像FutureBuilder一样。

StreamBuilder<String>(
  stream: NumberCreator().stream.map((i) =>String $i’),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Text(‘No data yet.’);
  }
  throw UnimplementedError(“Case not handled yet”);
},
);

你可以检查它的connectionState属性,看看流是否还没有发送任何数据,或者是否已经完全完成。

StreamBuilder<String>(
   stream: NumberCreator().stream.map((i) => 'String $i'),
   builder: (context, snapshot) {
      if (snapshot.connectionState == ConnectionState.waiting) {      
        return const Text('No data yet.');
      } else if (snapshot.connectionState == ConnectionState.done){
        return const Text('Done!');
      }
      throw UnimplementedError("Case not handled yet");
    },
 );

你可以使用hasError属性来处理数据值,看看最新的值是否是一个错误。

StreamBuilder<String>(
  stream: NumberCreator().stream.map((i) => ‘String $i’),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Text(‘No data yet.’);
    } else if (snapshot.connectionState == ConnectionState.done) {
      return const Text(‘Done!’);
    } else if (snapshot.hasError) {
      return const Text(‘Error!’);
    } else {
      return Text(snapshot.data ?? ‘’);
    } 
  },
);

最主要的是要确保你的构建器知道如何处理流的所有可能状态。一旦你得到了这些,它就可以对流的任何行为作出反应。(更多信息,包括你可以使用的DartPad实例,请参见StreamBuilderAPI页面)。

总结

本文介绍了流代表什么,如何从流中获取值,操作这些值的方法,以及StreamBuilder如何帮助您在Flutter应用程序中使用流值。

您可以从Dart和Flutter文档中了解更多关于流的信息。

请继续关注本系列中的更多文章。接下来,我们将讨论async和await。这是Dart提供的两个关键字,可以帮助你保持异步代码的简洁和易读。

同时,您可以在我们的YouTube频道观看下一个关于Dart异步编程的 系列视频,或者前往我们的网站了解更多关于DartFlutter的信息。

medium.com/media/924c4…


Dart异步编程 Streams最初发表于Medium上的Dart,人们通过强调和回应这个故事来继续对话。