Dart StreamController 完全使用指南
StreamController 是 Dart 中管理异步数据流的核心工具,提供数据流的入口和出口。通过 StreamController,你可以手动控制数据的发送和接收,适用于处理用户交互、网络请求、传感器数据等场景。
一、StreamController 的核心组成
StreamController 包含两个关键部分:
-
stream:数据的出口,供外部监听数据
-
sink:数据的入口,用于内部添加数据
StreamController 本身直接提供了 add 方法,它只是 sink.add 的语法糖,两者完全等价。
dart
controller.add('数据');
controller.sink.add('数据'); // 与上一行完全等价
二、创建方式与使用场景
2.1 单订阅流
使用默认构造函数创建单订阅流。
dart
final controller = StreamController<String>();
适用场景:文件读取、HTTP 响应等只有一个消费者需要处理数据的场景。
使用示例:
dart
final controller = StreamController<String>();
// **==== 必须先监听再添加数据
controller.stream.listen((data) => print('收到: $data'));
controller.add('测试数据');
关键限制:单订阅流只允许一个监听器,尝试添加第二个监听器会抛出异常。
2.2 广播流
使用 broadcast 构造函数创建广播流。
dart
final controller = StreamController<String>.broadcast();
适用场景:传感器数据分发、全局状态更新、UI 事件等需要多组件同时响应的场景。
使用示例:
dart
final controller = StreamController<String>.broadcast();
// 多个监听器可以同时订阅
controller.stream.listen((data) => print('监听者A: $data'));
controller.stream.listen((data) => print('监听者B: $data'));
controller.add('广播数据');
// 两个监听者都会收到 '广播数据'
关键限制 :广播流不会缓存历史数据,后订阅的监听者收不到之前发送的数据。
三、核心操作详解
3.1 数据发送
使用 add 方法发送数据。
dart
final controller = StreamController<int>();
// **==== controller.add 与 controller.sink.add 完全等价
controller.add(1);
controller.sink.add(2); // 与上一行完全等价
使用 addError 方法发送错误信息。
dart
controller.addError(Exception('网络请求失败'));
3.2 数据接收
使用 listen 方法监听数据流。
dart
controller.stream.listen(
(data) => print('收到数据: $data'),
onError: (error) => print('发生错误: $error'),
onDone: () => print('流已关闭'),
);
3.3 关闭流
使用 close 方法关闭流。
dart
controller.close();
关闭后会触发监听器的 onDone 回调。**==== 在组件销毁时务必调用 close 释放资源 ,否则会导致内存泄漏。
3.4 合理使用 sink 控制权限
直接持有 controller 时,使用 controller.add 更简洁。sink 的主要用途是限制权限。
当需要将数据入口暴露给外部,但不希望外部拥有控制器的完整控制权时,只暴露 sink。
dart
class DataManager {
final _controller = StreamController<int>.broadcast();
// 对外暴露只读流
Stream<int> get dataStream => _controller.stream;
// 对外只暴露 sink,外部只能添加数据,无法调用 close
StreamSink<int> get dataSink => _controller.sink;
// 内部直接使用 _controller.add
void internalAdd(int value) {
_controller.add(value);
}
void dispose() {
_controller.close();
}
}
外部使用者只能通过 dataSink 添加数据,无法调用 close 或其他控制方法。
四、使用场景与对应的创建方式
场景一:UI 事件处理
Flutter 中的按钮点击、文本输入等用户交互事件适合使用广播流,因为多个组件可能需要响应同一个事件。
dart
class EventBus {
final _clickController = StreamController<void>.broadcast();
// 外部只能通过 sink 触发事件,无法关闭控制器
StreamSink<void> get clickSink => _clickController.sink;
Stream<void> get clickEvents => _clickController.stream;
void dispose() {
_clickController.close();
}
}
final eventBus = EventBus();
eventBus.clickEvents.listen((_) => print('组件A响应'));
eventBus.clickEvents.listen((_) => print('组件B响应'));
eventBus.clickSink.add(null);
场景二:实时数据流
蓝牙设备、传感器、WebSocket 等实时数据源适合使用广播流,多个 UI 组件需要同时展示这些数据。
dart
class SensorManager {
final _dataController = StreamController<Map<String, double>>.broadcast();
Stream<Map<String, double>> get sensorData => _dataController.stream;
StreamSink<Map<String, double>> get dataSink => _dataController.sink;
void dispose() {
_dataController.close();
}
}
场景三:单次数据加载
网络请求响应或数据库查询结果等一次性数据适合使用单订阅流。
dart
class DataLoader {
Stream<String> loadData() {
final controller = StreamController<String>();
Future.delayed(Duration(seconds: 1), () {
controller.add('加载完成的数据');
controller.close();
});
return controller.stream;
}
}
场景四:状态管理(BLoC 模式)
BLoC 模式中使用 StreamController 管理应用状态,通常使用广播流以便多个 UI 组件订阅状态变化。
dart
class CounterBloc {
final _stateController = StreamController<int>.broadcast();
final _eventController = StreamController<String>();
Stream<int> get state => _stateController.stream;
StreamSink<String> get eventSink => _eventController.sink;
int _currentState = 0;
CounterBloc() {
// 监听事件流,根据事件更新状态
_eventController.stream.listen((event) {
if (event == 'increment') {
_currentState++;
_stateController.add(_currentState);
}
});
}
void dispose() {
_stateController.close();
_eventController.close();
}
}
五、高级用法
5.1 使用 StreamTransformer 转换数据
StreamTransformer 可以在数据到达监听器之前进行转换处理。
dart
final controller = StreamController<int>();
final transformer = StreamTransformer<int, String>.fromHandlers(
handleData: (data, sink) {
sink.add('转换后的值: ${data * 2}');
},
);
controller.stream.transform(transformer).listen((data) => print(data));
controller.add(5); // 输出: 转换后的值: 10
5.2 链式操作
Stream 提供了丰富的操作方法,可以链式调用进行数据过滤和转换。
dart
final controller = StreamController<int>.broadcast();
controller.stream
.where((value) => value % 2 == 0) // 只保留偶数
.map((value) => '偶数: $value') // 转换为字符串
.listen((data) => print(data));
controller.add(1); // **不输出**
controller.add(2); // 输出: 偶数: 2
5.3 使用 StreamSubscription 控制监听
监听返回的 StreamSubscription 对象可以控制数据接收。
dart
final controller = StreamController<int>();
final subscription = controller.stream.listen((data) => print('收到: $data'));
controller.add(1);
subscription.pause(); // **暂停接收**
controller.add(2); // **不会输出**
subscription.resume(); // **恢复接收**
controller.add(3); // 输出: 收到: 3
subscription.cancel();
在 StatefulWidget 的 dispose 中必须调用 subscription.cancel(),避免内存泄漏。
5.4 使用 await for 异步遍历
在异步函数中可以使用 await for 遍历流中的数据。
dart
void processData() async {
final controller = StreamController<int>();
Future.delayed(Duration(seconds: 1), () => controller.add(1));
Future.delayed(Duration(seconds: 2), () => controller.add(2));
Future.delayed(Duration(seconds: 3), () => controller.close());
// **==== await for 会持续接收数据直到流关闭
await for (var data in controller.stream) {
print('处理数据: $data');
}
print('流已结束');
}
5.5 使用 onListen 和 onCancel 回调
创建 StreamController 时可以指定订阅和取消订阅的回调。
dart
final controller = StreamController<int>.broadcast(
onListen: () => print('有监听者订阅'),
onCancel: () => print('所有监听者已取消'),
);
controller.stream.listen((data) => print(data));
// 输出: 有监听者订阅
这些回调适合管理资源,在第一个监听者订阅时启动数据源,在最后一个监听者取消时释放资源。
5.6 合并多个流
使用 StreamGroup.merge 将多个流合并为一个流。
dart
import 'dart:async';
final controller1 = StreamController<int>();
final controller2 = StreamController<int>();
final mergedStream = StreamGroup.merge([controller1.stream, controller2.stream]);
mergedStream.listen((data) => print('合并收到: $data'));
controller1.add(1); // 输出: 合并收到: 1
controller2.add(2); // 输出: 合并收到: 2
六、关键注意事项
-
controller.add 与 controller.sink.add 完全等价 ,直接持有 controller 时使用 add 更简洁。sink 的主要用途是向外部暴露受限的数据入口,防止外部误调用 close 等方法。
-
先监听后添加数据:在单订阅流中,必须先调用 listen 再调用 add,否则监听器收不到之前发送的数据。
-
广播流不缓存历史数据:后订阅的监听者收不到订阅前发送的数据。如果需要缓存最新值,考虑使用 RxDart 的 BehaviorSubject。
-
必须在 dispose 中取消订阅:StatefulWidget 中监听流后,必须在 dispose 中调用 subscription.cancel(),避免内存泄漏。
-
必须在 dispose 中关闭控制器:持有 StreamController 的对象在销毁时必须调用 controller.close(),释放资源。
-
关闭后不能再添加数据:调用 close 后再次调用 add 会抛出异常,添加数据前可检查 isClosed 属性。
-
单订阅流只能有一个监听器:尝试添加第二个监听器会抛出异常,需要多监听者时使用广播流。