写代码有时候就像坐过山车一样,当你在有如神助开心搬砖的时候,突然间又手足无措不知道该如何是好。这种情况还循环往复,有时候一天都这样,有时候整个你的开发生涯都差不多这样。
尤其在面对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
}
}
- 使用
async*
返回一个流,这个流的类型是Stream<int>
。 - 在这里延迟一秒钟。
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}"),
);
}
}
}));
}
- 在StatefulWidget里的成员状态
final _countStream = countStream(100);
。 - 使用
StreamBuilder
来消费_countStream
流。 StreamBuilder
的snapshot
获取流执行的状态。- 使用
snapshot
的流数据。
什么是流(Stream)
什么是流。就是一个或者多个事件,不断的发射(到一个管道里)。在(管道)另一端的监听器可以消费这些事件。在上面的例子中,countStream
会不断的发射事件,StreamBuilder
可以消费这些事件。
流和Future类似,也是dart实现异步操作的方式之一。Future可以异步地返回数据,异常之后停止执行。流也类似,可以异步发出一串数据(或者异常)。
流是怎么工作的
一般来说,流会把数据从管道的一段发到另一端。在这个管道上可以有一个、多个的监听器订阅这些数据。这些监听器根据某些条件对这些数据做一些操作。
具体如何使用流呢?
- 使用已有的流
- 新建流
流,我们有一些了解了。具体在Dart使用流也有一些条件。
- 单定流(single-subscription stream)
- 广播流
新建流
使用已有的流生成流
使用已有的流非常常见。比如,你用了第三方库、某些基于流的状态管理工具就会遇到直接生成的流。
这里没有第三方库,直接生成一个流。再用这个流。
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();
});
}
- 直接生成一个整数流。
- 使用一个已经生成的流,就是刚刚生成的整数流。使用
map
转化整数流,每个流的值翻倍。 - 订阅转化的流。
- 取消订阅。注意,在使用流的时候不用的流需要取消,否则会造成内存泄漏。
使用生成器生成流
这个类型的方法就是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
}
- 初始化一个
StreamController
。在构造这个流控制器的参数都是流控制器的回调。在监听、暂停、resume和取消的时候需要代码如何处理具体的问题都可以设置回调处理。 - 返回控制器的流。
- 在订阅流的时候控制器回调可以在
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
最终会报错!
- 初始化一个流控制器。
- 订阅流。
- 取消流的订阅。
- 再次订阅流。
- 关闭流。
单点流的这个特点对于数据完整性和顺序有要求的需求非常有用。比如解析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();
}
- 使用
StreamBuilder<T>.broadcast()
来新建一个广播流控制器,这个控制器返回的stream
就是广播流了。 - 第二、三步订阅广播流。
- 订阅广播流。
- 给流添加数据(事件)。
- 关闭流。
在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")
)
- 需要实现和
async*
方法生成流的方式一样的功能,每隔一秒发射一个数字。所以这里需要用async
。 - 给状态成员一个流,也可以在
initState
里直接赋值。或者初始化的时候直接赋值。这里主要说明,在async
事件中使用setState
的时候需要先判断mounted
属性。否则会有警告。 - 延迟一秒发射数字。
运行效果,报错: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两个数字。
- 初始化一个流控制器。
- 订阅流。
- 给流添加数据
- 添加一个异常
- 关闭流
在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('错误描述'); // 这个是在使用流控制器的时候
总之就是不要让异常把流击穿了,而是让异常变成了流要发射的值的一部分。
最后
了解了流的基础知识之后就要开始基于流的状态管理了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。