Stream与StreamController以及对应的使用场景

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 的方法

  1. listen() 方法
go 复制代码
```
go 复制代码
StreamSubscription<int> subscription = stream.listen(
  (data) => print(data),           // 处理数据
  onError: (error) => print(error), // 错误处理
  onDone: () => print('完成'),      // 流关闭时回调
  cancelOnError: false,             // 是否遇错自动取消
);
```
-   `listen` 返回 `StreamSubscription`,可用于暂停、恢复、取消。
  1. forEach() 方法
go 复制代码
```
stream.forEach((data) => print(data));
```

-   当流完成时返回一个 `Future`,不能中途取消。
  1. 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('取消'),
              ),
          ],
        ),
      ],
    );
  }
}
相关推荐
心中有国也有家1 小时前
鸿蒙Flutter开发环境从零搭建教程(Windows/macOS双平台·避坑版)
学习·flutter·华为·harmonyos
心中有国也有家4 小时前
AtomGit Flutter 鸿蒙客户端:E-Brufen 架构设计
学习·flutter·华为·harmonyos
心中有国也有家6 小时前
鸿蒙 Flutter 本地存储实战:Hive CE 从入门到精讲
人工智能·hive·flutter·华为·harmonyos
程序员老刘1 天前
老刘给大家道个歉
flutter·ai编程
binbin_521 天前
Flutter 开发鸿蒙实战:Windows 环境下从 HAP 构建到四 Tab 页面运行
windows·flutter·harmonyos
阳光予你1 天前
Flutter 字体渲染与字重
flutter
恋猫de小郭1 天前
Flutter 开发怎么做 Agent ?从工程实战详细给你解读下
android·前端·flutter
binbin_521 天前
Flutter 调用鸿蒙原生组件:MethodChannel 与 PlatformView 的选择和落地
开发语言·深度学习·flutter·harmonyos
GitLqr2 天前
Flutter 3.44 插件内置 Kotlin (KGP) 双向兼容适配指南
android·flutter·dart