Stream
概念
go
一次性得出多个结果。而`Future`为一个结果。
创建方式
arduino
1. 利用`async*`
```
Stream<int> countStream(int max) async* {
for (int i = 1; i <= max; i++) {
await Future.delayed(Duration(seconds: 1));
yield i; // 产生一个数据
}
}
```
- `yield`:产生一个数据;`yield*`:可产生另一个 Stream 的数据。
2. 使用 `Stream.fromFuture` / `fromFutures`
```
Stream<int> stream1 = Stream.fromFuture(Future.value(42));
Stream<int> stream2 = Stream.fromFutures([Future.value(1), Future.value(2)]);
```
3. 使用 `Stream.fromIterable`
```
Stream<int> stream = Stream.fromIterable([1, 2, 3]);
```
4. 使用 `Stream.periodic`
```
/// 隔一秒产生一个值
Stream<int> timerStream = Stream.periodic(Duration(seconds: 1), (count) => count);
```
5. 使用 `StreamController`(手动控制)
```
final controller = StreamController<int>();
// 添加数据
controller.sink.add(42);
// 获取流
Stream<int> stream = controller.stream;
```
监听 Stream 的方法
listen()方法
go
```
go
StreamSubscription<int> subscription = stream.listen(
(data) => print(data), // 处理数据
onError: (error) => print(error), // 错误处理
onDone: () => print('完成'), // 流关闭时回调
cancelOnError: false, // 是否遇错自动取消
);
```
- `listen` 返回 `StreamSubscription`,可用于暂停、恢复、取消。
forEach()方法
go
```
stream.forEach((data) => print(data));
```
- 当流完成时返回一个 `Future`,不能中途取消。
StreamBuilder(UI 集成)
kotlin
```
StreamBuilder<int>(
stream: stream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
return Text('Data: ${snapshot.data}');
}
},
)
```
核心API
go
操作符 | 作用 | 示例 |
| -------- | ----------------- | ------------------------------- |
| `map` | 将每个数据映射为新值 | `stream.map((x) => x * 2)` |
| `where` | 过滤满足条件的数据 | `stream.where((x) => x.isEven)` |
| `expand` | 将一个数据展开为多个数据(1对多) | `stream.expand((x) => [x, x])` |
| `take` | 只取前 n 个数据 | `stream.take(5)` |
| `skip` | 跳过前 n 个数据 | `stream.skip(5)`
使用场景
边下边保存、断点续传
kotlin
/// 核心模型
/// models/download_task.dart
enum DownloadStatus {
idle,
loading,
paused,
done,
error,
}
class DownloadProgress {
final int received;
final int total;
double get percent => total > 0 ? received / total : 0.0;
DownloadProgress(this.received, this.total);
}
class DownloadTask {
final String url;
final String savePath; // 最终文件名
final String tempPath; // 临时文件名(续传用)
final DownloadStatus status;
final DownloadProgress? progress;
final Object? error;
DownloadTask({
required this.url,
required this.savePath,
String? tempPath,
this.status = DownloadStatus.idle,
this.progress,
this.error,
}) : tempPath = tempPath ?? '$savePath.temp';
DownloadTask copyWith({
DownloadStatus? status,
DownloadProgress? progress,
Object? error,
}) {
return DownloadTask(
url: url,
savePath: savePath,
tempPath: tempPath,
status: status ?? this.status,
progress: progress ?? this.progress,
error: error ?? this.error,
);
}
}
-
下载管理器
DioDownloadManager-
职责
- 管理单个下载任务的完整生命周期(开始、暂停、取消、恢复)。
- 维护进度
StreamController以广播DownloadTask状态变化。 - 支持断点续传(通过
Range头 + 临时文件追加写入)。 - 支持外部传入解析器(
StreamTransformer)实现边下载边解析。
-
实现代码
dart// managers/dio_download_manager.dart import 'dart:io'; import 'package:dio/dio.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; class DioDownloadManager { final Dio _dio; final String _url; final String _savePath; final String _tempPath; final StreamController<DownloadTask> _taskController = StreamController.broadcast(); CancelToken? _cancelToken; DownloadTask _currentTask; bool _isPaused = false; final SharedPreferences _prefs; // 解析器(可选),用于将字节流转换为业务对象 StreamTransformer<List<int>, dynamic>? _parser; DioDownloadManager._({ required Dio dio, required String url, required String savePath, required SharedPreferences prefs, StreamTransformer<List<int>, dynamic>? parser, }) : _dio = dio, _url = url, _savePath = savePath, _tempPath = '$savePath.temp', _prefs = prefs, _parser = parser, _currentTask = DownloadTask(url: url, savePath: savePath) { _initTask(); } /// 工厂方法:创建并初始化管理器 static Future<DioDownloadManager> create({ required Dio dio, required String url, required SharedPreferences prefs, StreamTransformer<List<int>, dynamic>? parser, }) async { // 确定保存路径(使用应用文档目录) final dir = await getApplicationDocumentsDirectory(); final filename = Uri.parse(url).pathSegments.last; final savePath = '${dir.path}/$filename'; return DioDownloadManager._( dio: dio, url: url, savePath: savePath, prefs: prefs, parser: parser, ); } /// 对外暴露的任务状态 Stream Stream<DownloadTask> get taskStream => _taskController.stream; /// 初始化:从 SharedPreferences 恢复元数据 Future<void> _initTask() async { final key = 'download_meta_$_url'; final saved = _prefs.getString(key); if (saved != null) { final parts = saved.split('|'); if (parts.length == 2) { final received = int.tryParse(parts[0]) ?? 0; final total = int.tryParse(parts[1]) ?? 0; // 检查临时文件是否存在 final tempFile = File(_tempPath); if (await tempFile.exists() && received > 0) { _currentTask = _currentTask.copyWith( status: DownloadStatus.paused, progress: DownloadProgress(received, total), ); _taskController.add(_currentTask); return; } } } // 无有效元数据,初始状态 idle _currentTask = _currentTask.copyWith(status: DownloadStatus.idle); _taskController.add(_currentTask); } /// 保存元数据到 SharedPreferences void _saveMeta(int received, int total) { final key = 'download_meta_$_url'; _prefs.setString(key, '$received|$total'); } /// 清除元数据 void _clearMeta() { final key = 'download_meta_$_url'; _prefs.remove(key); } /// 开始或恢复下载 Future<void> start() async { if (_currentTask.status == DownloadStatus.done) return; // 如果已有 cancelToken,先取消旧的 _cancelToken?.cancel(); _cancelToken = CancelToken(); final tempFile = File(_tempPath); int startByte = 0; int totalBytes = 0; // 读取已下载字节数(从元数据或文件实际大小) if (await tempFile.exists()) { startByte = await tempFile.length(); } // 尝试从 _currentTask.progress 中获取 total if (_currentTask.progress != null && _currentTask.progress!.total > 0) { totalBytes = _currentTask.progress!.total; } _isPaused = false; _currentTask = _currentTask.copyWith( status: DownloadStatus.loading, error: null, ); _taskController.add(_currentTask); try { final response = await _dio.download( _url, tempFile.path, options: Options( headers: { if (startByte > 0) 'range': 'bytes=$startByte-', }, responseType: ResponseType.stream, ), cancelToken: _cancelToken, onReceiveProgress: (received, total) { if (_isPaused) return; // 注意:total 可能是 -1(如果服务器未返回 Content-Length) final totalBytes = total > 0 ? total + startByte : total; final receivedBytes = received + startByte; final progress = DownloadProgress(receivedBytes, totalBytes); _currentTask = _currentTask.copyWith(progress: progress); _taskController.add(_currentTask); // 保存元数据(每收到一定量保存一次,或每次保存) _saveMeta(receivedBytes, totalBytes); }, ); // 下载完成 if (response.statusCode == 200 || response.statusCode == 206) { // 如果有解析器,应用解析器并输出解析结果(可在外部监听) if (_parser != null) { final fileStream = tempFile.openRead(); final parsedStream = fileStream.transform(_parser!); // 将解析结果通过另一个 StreamController 对外暴露(此处略) } // 将临时文件重命名为最终文件 final finalFile = File(_savePath); if (await finalFile.exists()) { await finalFile.delete(); } await tempFile.rename(_savePath); _clearMeta(); final finalSize = await finalFile.length(); _currentTask = _currentTask.copyWith( status: DownloadStatus.done, progress: DownloadProgress(finalSize, finalSize), ); _taskController.add(_currentTask); } } on DioException catch (e) { if (CancelToken.isCancel(e)) { if (_isPaused) { // 暂停:保存元数据,状态置为 paused _currentTask = _currentTask.copyWith( status: DownloadStatus.paused, ); _taskController.add(_currentTask); } else { // 其他取消(如手动 cancel) await tempFile.delete(); _clearMeta(); _currentTask = _currentTask.copyWith( status: DownloadStatus.idle, progress: null, ); _taskController.add(_currentTask); } } else { _currentTask = _currentTask.copyWith( status: DownloadStatus.error, error: e, ); _taskController.add(_currentTask); } } catch (e) { _currentTask = _currentTask.copyWith( status: DownloadStatus.error, error: e, ); _taskController.add(_currentTask); } } /// 暂停下载 void pause() { if (_currentTask.status != DownloadStatus.loading) return; _isPaused = true; _cancelToken?.cancel('pause'); } /// 取消下载并删除临时文件 Future<void> cancel() async { _cancelToken?.cancel('cancel'); final tempFile = File(_tempPath); if (await tempFile.exists()) { await tempFile.delete(); } _clearMeta(); _currentTask = _currentTask.copyWith( status: DownloadStatus.idle, progress: null, ); _taskController.add(_currentTask); } /// 释放资源 void dispose() { _cancelToken?.cancel(); _taskController.close(); } } -
-
引入
riverpod
dart
// providers/download_providers.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dio_download_manager.dart';
// 提供 Dio 单例
final dioProvider = Provider<Dio>((ref) => Dio());
// 提供 SharedPreferences 单例
final sharedPreferencesProvider = FutureProvider<SharedPreferences>(
(ref) async => await SharedPreferences.getInstance(),
);
// 通过参数创建 DownloadManager(使用 FutureProvider 因为需要异步初始化)
final downloadManagerProvider = FutureProvider.family<DioDownloadManager, String>(
(ref, url) async {
final dio = ref.watch(dioProvider);
final prefs = await ref.watch(sharedPreferencesProvider.future);
// 可以在这里传入解析器
final manager = await DioDownloadManager.create(
dio: dio,
url: url,
prefs: prefs,
parser: null, // 可扩展
);
ref.onDispose(() => manager.dispose());
return manager;
},
);
// 暴露任务状态 Stream
final downloadTaskStreamProvider = StreamProvider.family<DownloadTask, String>(
(ref, url) {
final managerAsync = ref.watch(downloadManagerProvider(url));
return managerAsync.when(
data: (manager) => manager.taskStream,
error: (_, __) => Stream.value(
DownloadTask(url: url, savePath: '').copyWith(
status: DownloadStatus.error,
error: _,
),
),
loading: () => const Stream.empty(),
);
},
);
// 提供控制器方法(方便 UI 调用 start/pause/cancel)
final downloadControllerProvider = Provider.family<DioDownloadManager?, String>(
(ref, url) {
final managerAsync = ref.watch(downloadManagerProvider(url));
return managerAsync.when(
data: (manager) => manager,
error: (_, __) => null,
loading: () => null,
);
},
);
- UI使用
less
// download_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'providers/download_providers.dart';
class DownloadScreen extends ConsumerStatefulWidget {
final String url;
const DownloadScreen({super.key, required this.url});
@override
ConsumerState<DownloadScreen> createState() => _DownloadScreenState();
}
class _DownloadScreenState extends ConsumerState<DownloadScreen> {
@override
void initState() {
super.initState();
// 进入页面自动开始下载(或恢复)
WidgetsBinding.instance.addPostFrameCallback((_) {
final controller = ref.read(downloadControllerProvider(widget.url));
controller?.start();
});
}
@override
Widget build(BuildContext context) {
final taskAsync = ref.watch(downloadTaskStreamProvider(widget.url));
return Scaffold(
appBar: AppBar(title: Text('下载')),
body: Center(
child: taskAsync.when(
data: (task) => _buildContent(task),
error: (err, stack) => Text('错误: $err'),
loading: () => CircularProgressIndicator(),
),
),
);
}
Widget _buildContent(DownloadTask task) {
final controller = ref.read(downloadControllerProvider(widget.url));
if (controller == null) return Text('控制器未就绪');
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('状态: ${task.status.name}'),
if (task.progress != null) ...[
SizedBox(height: 16),
LinearProgressIndicator(value: task.progress!.percent),
SizedBox(height: 8),
Text(
'${(task.progress!.percent * 100).toStringAsFixed(1)}% '
'(${task.progress!.received}/${task.progress!.total} bytes)',
),
],
SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (task.status == DownloadStatus.loading)
ElevatedButton(
onPressed: controller.pause,
child: Text('暂停'),
),
if (task.status == DownloadStatus.paused ||
task.status == DownloadStatus.idle ||
task.status == DownloadStatus.error)
ElevatedButton(
onPressed: controller.start,
child: Text('开始/恢复'),
),
if (task.status != DownloadStatus.idle)
ElevatedButton(
onPressed: controller.cancel,
child: Text('取消'),
),
],
),
],
);
}
}