flutter StreamController 完全使用指南

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 属性。

  • 单订阅流只能有一个监听器:尝试添加第二个监听器会抛出异常,需要多监听者时使用广播流。

相关推荐
GitLqr16 小时前
玩转 Flutter 中的 Stack 与 Positioned:解决 UI 重叠问题的实战指南
flutter·面试·全栈
唔661 天前
flutter web iOS 在浏览器加载中文慢的问题
前端·flutter·ios
恋猫de小郭1 天前
Jetpack Compose 8 月版正式发布,核心模块 1.12
android·前端·flutter
梦想的颜色2 天前
AI 时代小白 VibeCoding 做 APP:UniApp(含 Uni‑X)、Flutter 与 React Native+Expo全维度技术选型对比指南
flutter·react native·app·uniapp·vibecoding·unippx·app产品
坚果的博客2 天前
Flutter-OH 3.44.9-dev 悄然上线|首个 OpenHarmony Canary 预览版上线
flutter
大龄秃头程序员2 天前
Flutter 动画随笔:业务里真正高频的几类控件
flutter
天空之城--2 天前
Android Flutter行业最新动态与实用参考(2026年8月第2周)
android·flutter
恋猫de小郭2 天前
Flutter 3.47 发布,快来看看有什么更新吧
android·前端·flutter