写代码有时候就像坐过山车一样,当你在有如神助开心搬砖的时候,突然间又手足无措不知道该如何是好。这种情况还循环往复,有时候一天都这样,有时候整个你的开发生涯都差不多这样。

尤其在面对Stream的时候这样的情况更加明显。Stream的很多概念会让你觉得很简单,有些有会让你抓不到要点,尤其对于Dart或者Flutter的新手的时候。为什么会这样的呢?这是因为Strem实在是太过基础,比如很多感知设备发出来的信号,一些状态管理工具甚至于Dart的isolate等都是以Stream为基础。

但是,Stream也没有难到“蜀道难”的程度,只需要稍加留心和多练习就可以掌握它。

我们先来看一个简单的例子:

Stream<int> countStream(int to) async* { // 1
  for (int i = 1; i <= to; i++) {
    await Future.delayed(const Duration(seconds: 1)); // 2
    yield i; // 3
  }
}
  1. 使用async*返回一个流,这个流的类型是Stream<int>
  2. 在这里延迟一秒钟。
  3. yield在流里面发出一个值。

在这个简单的例子里面制造了一个流,这个流每隔一秒钟发出一个数值。在StreamBuilder中可以消费这个方法返回的流,具体是这样的:

  // 在StatefulWidget里
  final _countStream = countStream(100); // 1

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text("STREAM"),
        ),
        body: StreamBuilder(    // 2
            stream: _countStream,
            builder: (context, snapshot) {
              if (snapshot.hasError) {  // 3
                return Center(
                  child: Text("error ${snapshot.error}"),
                );
              } else {
                switch (snapshot.connectionState) {  // 3
                  case ConnectionState.none:
                    return const Center(
                      child: Text("None"),
                    );
                  case ConnectionState.waiting:
                    return const Center(
                      child: Text("Waiting..."),
                    );
                  case ConnectionState.active:
                    return Center(
                      child: Text("Active: ${snapshot.data}"),  // 4
                    );
                  case ConnectionState.done:
                    return Center(
                      child: Text("Active: ${snapshot.data}"),
                    );
                }
              }
            }));
  }
  1. 在StatefulWidget里的成员状态final _countStream = countStream(100);
  2. 使用StreamBuilder来消费_countStream流。
  3. StreamBuildersnapshot获取流执行的状态。
  4. 使用snapshot的流数据。

什么是流(Stream)

alt text

什么是流。就是一个或者多个事件,不断的发射(到一个管道里)。在(管道)另一端的监听器可以消费这些事件。在上面的例子中,countStream会不断的发射事件,StreamBuilder可以消费这些事件。

流和Future类似,也是dart实现异步操作的方式之一。Future可以异步地返回数据,异常之后停止执行。流也类似,可以异步发出一串数据(或者异常)。

流是怎么工作的

一般来说,流会把数据从管道的一段发到另一端。在这个管道上可以有一个、多个的监听器订阅这些数据。这些监听器根据某些条件对这些数据做一些操作。

具体如何使用流呢?

  1. 使用已有的流
  2. 新建流

流,我们有一些了解了。具体在Dart使用流也有一些条件。

  1. 单定流(single-subscription stream)
  2. 广播流

新建流

使用已有的流生成流

使用已有的流非常常见。比如,你用了第三方库、某些基于流的状态管理工具就会遇到直接生成的流。

这里没有第三方库,直接生成一个流。再用这个流。

import 'dart:async';

void main() {
  // 新建一个整数流
  final Stream<int> originalStream = Stream<int>.fromIterable([1, 2, 3, 4, 5]); // 1

  // 使用map转化流
  final Stream<int> transformedStream = originalStream.map((int value) { // 2
    return value * 2; // Double each integer
  });

  // 订阅监听刚刚转化的流
  final StreamSubscription<int> subscription = // 3
      transformedStream.listen((int value) {
    print('Transformed value: $value');
  });

  // 取消订阅
  Future.delayed(Duration(seconds: 1), () { // 4
    subscription.cancel();
  });
}
  1. 直接生成一个整数流。
  2. 使用一个已经生成的流,就是刚刚生成的整数流。使用map转化整数流,每个流的值翻倍。
  3. 订阅转化的流。
  4. 取消订阅。注意,在使用流的时候不用的流需要取消,否则会造成内存泄漏。

使用生成器生成流

这个类型的方法就是countStream所使用的的方法。使用async*表明这个方法返回的是一个流。在方法的内部使用yield向流内部发射值。

Stream<int> countStream(int to) async* {
  for (int i = 1; i <= to; i++) {
    await Future.delayed(const Duration(seconds: 1));
    yield i;
  }
}

使用StreamController生成流

相比之下,StreamController比生成器更加好用。

Stream<int> timedCounter(Duration interval, [int? maxCount]) {
  late 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); // 3
  }

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

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

  return controller.stream; // 2
}
  1. 初始化一个StreamController。在构造这个流控制器的参数都是流控制器的回调。在监听、暂停、resume和取消的时候需要代码如何处理具体的问题都可以设置回调处理。
  2. 返回控制器的流。
  3. 在订阅流的时候控制器回调可以在onListen中开始向流发射数据。注意: 流的数据可以包括具体的业务数据,也可以包含异常。

StreamController提供了创建流的很多便利,但是也有一个问题需要注意。如果StreamController的准备好发射了,没有订阅者,那么这些数据会缓存,从而导致内存泄露。所以,没订阅,不发射是流使用的一条黄金规则。

使用流

使用流,就是订阅流。订阅了之后就会收到流发射出来的各种数据。

对于Dart使用流的限制条件需要详细了解,否则就算在StreamBuilder这么简单是使用环境也会报错。

Dart默认的流是单定流

单定流(single-subscription stream)在其生命周期中,至允许存在一个订阅者,或者是监听器。就算取消了一个已经存在的订阅者,也不能再订阅了。如果强行订阅,报错:Bad State

  StreamController<int> streamController = StreamController<int>(); // 1

  StreamSubscription<int> subscription = streamController.stream.listen( // 2
    (int data) {
      print('Received data: $data');
    },
  );

  subscription.cancel(); // 3

  subscription = streamController.stream.listen( // 4
    (int data) {
      print('Received data again: $data');
    },
  );

  streamController.close(); // 5

最终会报错!

  1. 初始化一个流控制器。
  2. 订阅流。
  3. 取消流的订阅。
  4. 再次订阅流。
  5. 关闭流。

单点流的这个特点对于数据完整性和顺序有要求的需求非常有用。比如解析Http的请求,读一个文件,或者处理聊天app的消息。

单点流只有在订阅之后才会开始发出数据,在订阅者取消点阅之后数据就不会再发出。即使还有数据需要发出。

但是,如果需要多个订阅者呢?

如果你需要在app的多个部分都订阅一个流,如果你的app多个部分需要在一个事件发生之后同时做出反应呢?这就需要广播流了!

广播流

广播流可以有多个订阅者。而且不管有没有订阅者,只要准备就绪就会开始不断的发出数据。广播流发出的数据没有顺序的要求。订阅者完全可以在收到数据之后就做出对应的处理。比如,头条新闻、球赛比分或者天气预报之类。这样似乎看起来多少有点浪费资源。所以在使用广播流的时候要加些小心,否则可能会导致内存泄漏。

所以,在订阅者收到了done事件之后会好取消订阅。

import 'dart:async';

void main() {
  // 1
  StreamController<int> broadcastController = StreamController<int>.broadcast();

  // 2
  StreamSubscription<int> subscription1 = broadcastController.stream.listen(
    (data) {
      print('Listener 1 Received: $data');
    },
  );

  // 3
  StreamSubscription<int> subscription2 = broadcastController.stream.listen(
    (data) {
      print('Listener 2 Received: $data');
    },
  );

  // 4
  broadcastController.add(1);
  broadcastController.add(2);
  broadcastController.add(3);

  // 5
  broadcastController.close();
}
  1. 使用StreamBuilder<T>.broadcast()来新建一个广播流控制器,这个控制器返回的stream就是广播流了。
  2. 第二、三步订阅广播流。
  3. 订阅广播流。
  4. 给流添加数据(事件)。
  5. 关闭流。

在Flutter的实例。在stream_page.dart文件中,给组件添加一个控制器成员。可以控制这个控制器出的初始化方法,用构造函数就是单定流,用broadcast就是广播流。

给这个流控制器两个StreamBuilder,让这两个builder订阅流。这样可以对比单定流和广播流在多个订阅下的反应是什么。

class StreamPage extends StatefulWidget {
  StreamPage({super.key});

  final controller = StreamController<int>(); // *

  @override
  State<StatefulWidget> createState() => _StreamPageState();
}

在widget成员里加流控制器。现在这个流控制器使用的是单定流。下面看看单定流在StreamBuilder的使用情况:

不过首先需要在这个按钮中给这个扣控制器添加数据(事件):

ElevatedButton(
  onPressed: () async { // 1
    if (mounted) {
      setState(() {
        _countStream2 = widget.controller.stream; // 2
      });
    }

    for (int i = 1; i <= 10; i++) {
      await Future.delayed(const Duration(seconds: 1)); // 3
      widget.controller.add(i);
    }
  },
  child: const Text("Start stream")
)
  1. 需要实现和async*方法生成流的方式一样的功能,每隔一秒发射一个数字。所以这里需要用async
  2. 给状态成员一个流,也可以在initState里直接赋值。或者初始化的时候直接赋值。这里主要说明,在async事件中使用setState的时候需要先判断mounted属性。否则会有警告。
  3. 延迟一秒发射数字。

运行效果,报错:Bad state。这也说明单定流只能有一个订阅。这里的效果可能会受到hot reload的影响。

final controller = StreamController<int>();换成final controller = StreamController<int>.broadcast();。再次运行代码,一切正常运行。

不要忘记在dispose方法中关闭流。

  @override
  void dispose() {
    widget.controller.close();
    super.dispose();
  }

异常处理

await for来说,流数据会不断的发射数据,一直到全部的数据都发射完毕。但是,不巧遇到了问题,比如网络下载的文件突然就断了之类的。这时候流也会停止。Dart的流也很类似,在遇到第一个错误的时候就停止执行。当然也有例外,稍后讨论。

import 'dart:async';

void main() {
  // 1
  StreamController<int> streamController = StreamController<int>();

  // 2
  StreamSubscription<int> subscription = streamController.stream.listen(
    (int data) {
      print('Received data: $data');
    },
    onError: (error) {
      print('Error occurred: $error');
    },
    onDone: () {
      print('Stream is done.');
    },
  );

  // 3
  streamController.add(1);
  streamController.add(2);
  throw Exception("Error");
  streamController.add(3);

  // 5
  streamController.close();
}

运行结果只会有1和2两个数字。

  1. 初始化一个流控制器。
  2. 订阅流。
  3. 给流添加数据
  4. 添加一个异常
  5. 关闭流

在Widget里执行:

  for (int i = 1; i <= 10; i++) {
    await Future.delayed(const Duration(seconds: 1));
    if (i == 5) {
      // widget.controller.addError('Error with num $i');
      throw Exception("Number is $i"); // *
    } else {
      widget.controller.add(i);
    }
  }

抛出异常的时候,流停止发射值。

如果发生了异常的时候,还想要流继续执行的话可以这样:

 if (i == 3) yield* () async* { throw Exception(); }();
  // 或者:      yield* Stream.fromFuture(Future.error(Exception());
  // 或者:      yield* Stream.error(Exception()); 
  // 或者:     controller.addError('错误描述'); // 这个是在使用流控制器的时候

总之就是不要让异常把流击穿了,而是让异常变成了流要发射的值的一部分。

最后

了解了流的基础知识之后就要开始基于流的状态管理了。


小红星闪啊闪
914 声望1.9k 粉丝

时不我待