有这么一段代码:
dart
Stream<int> countStream(int max) async* {
for (int i = 0; i < max; i++) {
yield i;
}
}
这段代码定义了一个异步生成器函数,用来产生一个整数序列的流(Stream)。我会从基础到核心,帮你把这段代码拆解清楚。
代码整体功能
这段代码定义了一个名为 countStream 的函数,它接收一个整数参数 max,返回一个 Stream<int>(整数类型的流)。这个流会异步地、逐个地生成从 0 到 max-1 的整数序列(比如传入 max=3,会依次产生 0、1、2)。
逐行拆解解释
dart
// 函数返回值是 Stream<int>:表示这是一个能产生整数的"数据流"
Stream<int> countStream(int max) async* {
// 普通的 for 循环,从 0 开始,到 max-1 结束
for (int i = 0; i < max; i++) {
// 核心关键字:yield,作用是"产出"一个值到流中
yield i;
}
}
核心概念解释
Stream<int>:流
你可以把 Stream 理解成一个异步的、可迭代的 "管道":
- 普通的 List 是一次性把所有数据(比如 [0,1,2])放在一个容器里给你;
- 而 Stream 是把数据逐个、异步地通过管道传给你(先传 0,再传 1,再传 2),适合处理需要 "边产生边消费" 的场景(比如网络请求、文件读取、实时数据)。
async*:异步生成器函数
async:标记函数是异步的,但async*是 Dart 专门用于生成 Stream的异步生成器语法(区别于普通异步函数的 async);*:表示这是一个 "生成器" 函数,作用是持续产生值,而不是只返回一个值。
yield:产出值到流中
yield 是生成器的核心关键字:
- 当执行到
yield i时,函数会把当前的i值 "发送" 到 Stream 中,供监听者接收; - 发送后函数不会结束,而是暂停在这里,等下一次被请求时继续执行循环(直到循环结束,Stream 才会关闭)。
实际使用示例
为了让你更直观理解,我写一个完整的使用示例,你可以直接运行:
dart
void main() async {
// 调用函数,得到一个流(此时流还没开始产生数据)
Stream<int> stream = countStream(3);
// 监听流,接收每一个产出的值
await for (int value in stream) {
print('收到值:$value');
}
print('流已结束');
}
// 你提供的原函数
Stream<int> countStream(int max) async* {
for (int i = 0; i < max; i++) {
yield i;
}
}
运行结果:
dart
收到值:0
收到值:1
收到值:2
流已结束
countStream是一个异步生成器函数(async*),返回一个整数类型的流(Stream<int>);yield关键字负责把循环中的每个整数逐个 "发送" 到流中,而非一次性返回所有值;- 流的特点是异步、逐个消费,需要通过
await for或listen来监听和接收值。
优点
用Dart 异步生成器(async* + yield)返回 Stream 的方式来生成数据序列,相比直接返回列表(List)等其他方式,具体有哪些优势。我会结合实际场景,把这些好处讲得通俗易懂。
极致的内存效率:避免一次性加载所有数据
dart
// 危险:max=1亿时会直接卡崩/内存溢出
List<int> countList(int max) {
return List.generate(max, (i) => i);
}
// 安全:max=1亿也能正常运行(只要消费端能处理)
Stream<int> countStream(int max) async* {
for (int i = 0; i < max; i++) {
yield i;
}
}
这是最核心的好处。
- 如果用
List<int> countList(int max),当max非常大(比如 100 万、1 亿)时,会一次性在内存中创建包含所有元素的列表,瞬间占用大量内存,甚至可能导致内存溢出(OOM)。 - 而
Stream+yield的方式:每次只生成一个值并发送,内存中始终只保留当前的i(单个整数),无论max多大,内存占用几乎可以忽略。
异步非阻塞:不卡主线程,支持耗时操作
Stream 是异步的,生成数据的过程可以包含耗时操作(比如网络请求、文件读取),且不会阻塞主线程;而普通列表是同步生成,耗时操作会卡住整个程序。
实时响应:边生产边消费,提升交互体验
Stream 是「流式处理」,生成一个值就可以立即消费一个值,无需等待所有数据生成完毕。
- 比如做一个「实时计数展示」的功能:用
Stream可以每生成一个数就立刻更新UI,用户能看到计数逐步增加; - 用列表的话,必须等所有数生成完,才能一次性展示,用户会看到长时间的空白,体验极差。
缺点
手动管理成本极高,极易引发内存泄漏
这是最致命的缺点,也是实际项目中最容易踩的坑。
- 问题本质:
async*生成的Stream订阅后,必须手动调用subscription.cancel()取消订阅(比如在页面dispose生命周期中);如果忘记取消,async*函数会持续执行(比如倒计时循环、分页请求),导致内存泄漏(页面销毁后仍占用资源),甚至引发空指针异常(订阅回调中操作已销毁的Widget)。 - 对比主流方案:
Bloc/Riverpod/Provider会自动绑定Widget生命周期,页面销毁时自动终止状态更新;ValueNotifier也无需手动取消监听,GC会自动处理。
示例:
dart
class BadStreamPage extends StatefulWidget {
const BadStreamPage({super.key});
@override
State<BadStreamPage> createState() => _BadStreamPageState();
}
class _BadStreamPageState extends State<BadStreamPage> {
StreamSubscription<int>? _subscription;
@override
void initState() {
super.initState();
// 订阅倒计时流,但忘记在 dispose 中取消
_subscription = countStream(60).listen((i) {
print('倒计时:$i'); // 页面销毁后仍会打印,内存泄漏
});
}
// 忘记重写 dispose 取消订阅 → 内存泄漏!
// @override
// void dispose() {
// _subscription?.cancel(); // 必须手动取消
// super.dispose();
// }
@override
Widget build(BuildContext context) {
return const Center(child: Text('倒计时页面'));
}
}
调试体验差,难以追踪数据流转
原生 Stream + async* 缺乏配套的调试工具,排查问题成本极高。
- 问题 1:无法直观看到
yield的调用时机、数据值,只能靠print日志调试; - 问题 2:无法追踪
Stream的订阅 / 取消状态,排查内存泄漏时只能靠猜; - 对比主流方案:
Bloc有BlocObserver可全局监控所有状态emit操作,Flutter DevTools能可视化Riverpod/Provider的状态变化,调试效率提升 10 倍。
认知与协作成本高,团队易出分歧
async*、yield、Stream 属于 Dart 进阶语法,而非基础语法:
- 新手理解成本高(比如分不清
asyncvsasync*、yieldvsreturn),容易写出逻辑错误的代码; - 团队协作时,部分开发者用原生
Stream,部分用Bloc/Riverpod,代码风格不统一,维护成本飙升; - 对比主流方案:
setState、Future、Provider是Flutter入门必学内容,所有开发者都能快速上手。
这些缺点决定了它只适合「底层流式处理、大数据量、可中断序列」等小众高需求场景。
典型场景
实时、连续的「原生数据流」处理(不可替代)
这是最核心的高需求场景 ------ 当你需要处理持续产生、无固定终点的实时数据时,async* + Stream 是唯一简洁且高效的选择。
典型业务场景:
- 蓝牙 / BLE / 串口通信(实时接收设备数据,比如手环心率、智能家居传感器);
- 传感器数据(手机加速度计、陀螺仪、GPS 实时定位);
- WebSocket/SSE 推送(实时聊天、行情刷新、消息通知);
- 实时日志监控(APP 运行日志、服务器日志实时展示)。
这类场景的核心是「数据持续产生,需要逐次消费」,Future 只能处理单次异步操作,状态管理库(Bloc/Riverpod)是「状态封装层」,而底层的数据流生成必须依赖 Stream;async* + yield 则是生成这类流式数据最简洁的原生方式。
示例:蓝牙实时数据读取
dart
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
// 用 async* + yield 实时读取蓝牙设备的特征值数据
Stream<List<int>> readBleDataStream(BluetoothCharacteristic characteristic) async* {
// 持续监听蓝牙数据,有新数据就 yield 出去
await for (final value in characteristic.onValueReceived) {
yield value; // 实时产出蓝牙设备发来的字节数据
}
}
// 消费端使用
void listenBleData(BluetoothCharacteristic characteristic) {
readBleDataStream(characteristic).listen((data) {
print('实时蓝牙数据:$data'); // 每收到一次数据就处理一次
});
}
其他方案的不足:
- 用
Future:无法持续监听,只能单次读取,完全不适用; - 用
Bloc:Bloc的emit本质是封装了Stream,但底层仍需用async*生成数据流,只是上层封装,而非替代。
大数据量「流式处理」(内存敏感场景)
当处理超大文件 / 超大数据集时,async* + yield 是避免 OOM(内存溢出)的最优解,属于「必须用」的高需求场景。
典型业务场景:
- 读取 / 解析超大本地文件(比如
100MB+的CSV/JSON日志文件、Excel报表); - 大批量数据导出(比如导出
10万条订单数据为CSV,逐行生成避免内存爆炸); - 批量数据库查询(逐批读取数据,而非一次性加载所有结果)。
这类场景的核心是「内存可控」------ 一次性加载所有数据会直接导致 App 崩溃,而 yield 能逐行/逐块产出数据,内存占用始终保持在极低水平。
实战示例:解析超大 CSV 文件
dart
import 'dart:io';
import 'dart:convert';
// 用 async* + yield 逐行解析超大 CSV 文件,避免 OOM
Stream<Map<String, String>> parseLargeCsvStream(String filePath) async* {
final file = File(filePath);
if (!await file.exists()) throw Exception('文件不存在');
final lines = file.openRead()
.transform(utf8.decoder)
.transform(const LineSplitter());
// 读取表头
final headerLine = await lines.first;
final headers = headerLine.split(',');
// 逐行解析数据并 yield
await for (final line in lines) {
if (line.isEmpty) continue;
final values = line.split(',');
final row = <String, String>{};
for (int i = 0; i < headers.length; i++) {
row[headers[i]] = values[i];
}
yield row; // 逐行产出解析后的行数据,内存只存当前行
}
}
// 消费端:逐行处理,不卡内存
void processLargeCsv(String filePath) async {
await for (final row in parseLargeCsvStream(filePath)) {
print('处理行数据:$row'); // 处理完当前行就释放内存
}
}
自定义「可中断的异步序列」生成
当你需要生成有固定序列、但可能中途取消的异步数据时,async* + Stream 是最灵活的选择。
典型业务场景:
- 分页加载(带「取消加载」功能,比如用户退出页面时终止后续请求);
- 批量任务处理(比如批量上传 100 张图片,支持中途暂停 / 取消);
- 精准控制的倒计时 / 定时器(支持中途停止,且不残留定时器)。
这类场景的核心是「可中断」------Stream 的订阅取消能直接终止 async* 函数的执行,而其他方案(如 Future 循环 + 定时器)中断逻辑复杂,易残留资源。
实战示例:可中断的批量图片上传
dart
// 用 async* + yield 生成批量上传进度流,支持中途取消
Stream<double> uploadImagesStream(List<String> imagePaths) async* {
int uploadedCount = 0;
for (final path in imagePaths) {
// 模拟单张图片上传(耗时操作)
await Future.delayed(const Duration(seconds: 1));
uploadedCount++;
// 产出上传进度(0.0 ~ 1.0)
yield uploadedCount / imagePaths.length;
}
}
// 消费端:支持中途取消上传
void startUpload(List<String> imagePaths) {
late StreamSubscription<double> subscription;
subscription = uploadImagesStream(imagePaths).listen((progress) {
print('上传进度:${(progress * 100).toStringAsFixed(1)}%');
// 模拟:进度到50%时取消上传
if (progress >= 0.5) {
subscription.cancel(); // 取消订阅,uploadImagesStream 会立即停止循环
print('上传已取消');
}
});
}
封装「底层流式工具 / SDK」
当你需要开发通用工具类、SDK 或底层库时,async* + Stream 是对外暴露流式接口的标准方式
- 自定义网络请求库(对外暴露下载进度流);
- 日志工具(对外暴露实时日志流);
- 数据同步工具(对外暴露同步进度流)。
作为底层工具,需要提供「通用、灵活、低耦合」的接口,Stream 是 Dart/Flutter 生态的标准流式接口,而 async* 是生成这类接口的最简洁方式。
实战示例:自定义下载进度工具
dart
import 'dart:io';
import 'package:http/http.dart' as http;
// 封装下载工具,用 async* + yield 暴露下载进度流
Stream<double> downloadFileStream(String url, String savePath) async* {
final request = http.Request('GET', Uri.parse(url));
final response = await http.Client().send(request);
final totalLength = response.contentLength ?? -1;
int downloadedLength = 0;
final file = File(savePath);
final sink = file.openWrite();
await for (final chunk in response.stream) {
downloadedLength += chunk.length;
sink.add(chunk);
// 产出下载进度(-1 表示长度未知)
if (totalLength > 0) {
yield downloadedLength / totalLength;
}
}
await sink.flush();
await sink.close();
yield 1.0; // 最后产出 100% 进度
}