【maaath】Flutter for OpenHarmony 文件下载能力集成实战

Flutter for OpenHarmony 文件下载能力集成实战

作者:maaath


欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net


一、前言

在移动应用开发中,文件下载是一个不可或缺的核心功能。无论是应用更新、图片加载还是文档获取,都离不开稳定、高效的下载能力。本文将详细介绍如何在 Flutter for OpenHarmony 项目中,从零开始构建一套完整的文件下载解决方案,涵盖下载管理、进度追踪、断点续传、暂停恢复等核心功能。

Flutter 作为跨平台开发的利器,其在 OpenHarmony 上的适配正在不断完善。通过 Flutter 的平台通道机制,我们可以优雅地调用 OpenHarmony 原生能力,同时保持 Dart 代码的统一性。本文将手把手教你实现一个生产级别的下载管理器。


二、需求分析与架构设计

2.1 功能需求

一个完善的文件下载模块需要满足以下核心需求:

基础功能层面:

  • 支持多任务并发下载,默认限制3个并发任务
  • 实时展示下载进度,包含已下载字节数和总字节数
  • 支持下载暂停与恢复操作
  • 实现断点续传,通过 HTTP Range 请求支持中断后继续下载

可靠性层面:

  • 自动重试机制,下载失败时支持配置重试次数
  • 下载历史持久化,记录每次下载的状态信息
  • 文件完整性校验,确保下载文件与服务器一致

用户体验层面:

  • 清晰的状态展示(等待中、下载中、已暂停、已完成、失败)
  • 友好的通知提示,下载完成或失败时给予用户反馈
  • 下载历史记录查看与管理

2.2 架构设计

采用分层架构设计,将下载功能划分为三个核心模块:

复制代码
┌─────────────────────────────────────────┐
│           DownloadPage (UI层)            │
│  负责下载任务展示、用户交互、状态刷新     │
├─────────────────────────────────────────┤
│         DownloadManager (业务层)         │
│  核心下载逻辑:任务调度、并发控制、        │
│  进度回调、暂停恢复、重试机制             │
├─────────────────────────────────────────┤
│  DownloadHistoryManager │ HTTP请求 │ 文件IO│
│  历史持久化              │ 网络通信  │ 文件存储│
└─────────────────────────────────────────┘

这种分层设计确保了代码的高内聚低耦合,便于维护和测试。每个模块职责明确,通过事件回调实现层间通信。


三、核心数据模型实现

3.1 下载状态枚举

首先定义下载任务可能处于的各种状态:

dart 复制代码
enum DownloadStatus {
  PENDING,      // 等待中,未开始下载
  DOWNLOADING,   // 正在下载
  PAUSED,       // 已暂停
  COMPLETED,    // 下载完成
  FAILED,       // 下载失败
  CANCELLED     // 已取消
}

状态的清晰定义是后续状态机逻辑的基础。每个状态之间存在合法的转换路径,例如 COMPLETED 状态只能由 DOWNLOADING 转换而来,而不能从 PAUSED 直接跳转到 COMPLETED。

3.2 下载任务模型

DownloadTask 类是整个下载模块的核心数据结构,它封装了一个下载任务的所有信息:

dart 复制代码
class DownloadTask {
  String id;                    // 任务唯一标识
  String url;                   // 下载地址
  String fileName;             // 保存文件名
  String savePath;              // 完整保存路径
  int totalSize;               // 文件总大小(字节)
  int downloadedSize;           // 已下载大小(字节)
  DownloadStatus status;        // 当前状态
  int retryCount;              // 当前重试次数
  int maxRetries;              // 最大重试次数
  Map<String, String> headers; // 自定义请求头

  // 进度百分比计算
  int getProgress() {
    if (totalSize == 0) return 0;
    return ((downloadedSize / totalSize) * 100).round();
  }

  // 下载速度计算(字节/秒)
  double getSpeed() {
    if (status != DownloadStatus.DOWNLOADING || startTime == 0) {
      return 0;
    }
    final elapsed = (DateTime.now().millisecondsSinceEpoch - startTime) / 1000;
    if (elapsed <= 0) return 0;
    return downloadedSize / elapsed;
  }

  // 剩余时间预估(秒)
  int getRemainingTime() {
    final speed = getSpeed();
    if (speed <= 0) return -1;
    return ((totalSize - downloadedSize) / speed).round();
  }

  bool canRetry() => retryCount < maxRetries;
}

这个模型设计考虑到了实际使用中的各种场景:文件名自动从 URL 提取、支持自定义 HTTP 头部(用于认证或特殊需求)、内置进度和速度的计算逻辑。

3.3 配置模型

DownloadConfig 提供全局下载行为的配置选项:

dart 复制代码
class DownloadConfig {
  int maxConcurrentDownloads = 3;  // 最大并发数
  int maxRetryCount = 3;          // 最大重试次数
  int retryDelayMs = 1000;        // 重试间隔(毫秒)
  int connectTimeoutMs = 30000;   // 连接超时
  int readTimeoutMs = 60000;       // 读取超时
  int chunkSize = 1024 * 1024;    // 分块大小(1MB)
  bool enableNotification = true;  // 是否启用通知
  String saveDirectory = '';       // 保存目录
}

四、下载管理服务核心实现

4.1 单例模式与上下文初始化

DownloadManager 采用单例模式确保全局只有一个下载管理器实例:

dart 复制代码
class DownloadManager {
  static DownloadManager? _instance;
  static Context? _context;

  final Map<String, DownloadTask> _activeTasks = {};
  final Map<String, DownloadTask> _pausedTasks = {};
  final List<DownloadTask> _pendingTasks = [];

  final List<Function(DownloadTask)> _progressCallbacks = [];
  final List<Function(DownloadTask, DownloadStatus)> _stateCallbacks = [];
  final List<Function(DownloadTask, String)> _completeCallbacks = [];
  final List<Function(DownloadTask, Error)> _errorCallbacks = [];

  DownloadConfig _config = DownloadConfig();

  DownloadManager._();

  static DownloadManager getInstance() {
    _instance ??= DownloadManager._();
    return _instance!;
  }

  static void setContext(Context context) {
    _context = context;
    final manager = getInstance();
    manager._initDirectory();
    manager._initHistoryManager();
  }
}

单例模式保证了下载任务调度的唯一性,避免多实例竞争导致的并发控制混乱。通过静态 setContext 方法在应用启动时注入上下文,这是 Flutter 插件获取原生能力的标准方式。

4.2 任务创建与启动

创建下载任务时会自动生成唯一 ID,并尝试从 URL 中提取文件名:

dart 复制代码
String download(String url, [String? fileName]) {
  final task = DownloadTask(url, fileName);
  return startDownload(task);
}

Future<String> startDownload(DownloadTask task) async {
  // 检查是否已在下载
  if (_activeTasks.containsKey(task.id)) {
    return task.id;
  }

  // 检查并发限制
  final runningCount = _activeTasks.length;
  if (runningCount >= _config.maxConcurrentDownloads) {
    task.status = DownloadStatus.PENDING;
    _pendingTasks.add(task);
    _sortPendingTasks();
    return task.id;
  }

  _activeTasks[task.id] = task;
  _executeDownload(task);
  return task.id;
}

这里的并发控制采用"入队等待"策略:当活跃任务数达到上限时,新任务会进入待处理队列,而不是直接拒绝。这种设计对用户体验更加友好。

4.3 HTTP 请求执行

下载任务通过 OpenHarmony 的 HTTP 模块执行:

dart 复制代码
void _executeDownload(DownloadTask task) {
  final previousStatus = task.status;
  task.status = DownloadStatus.DOWNLOADING;
  task.startTime = DateTime.now().millisecondsSinceEpoch;
  _notifyStateChange(task, previousStatus);

  final savePath = '${_config.saveDirectory}/${task.fileName}';
  task.savePath = savePath;

  // 检查断点续传
  int startPosition = 0;
  if (File(savePath).existsSync()) {
    final stat = File(savePath).statSync();
    if (task.headers['Range'] == null && stat.size > 0) {
      startPosition = stat.size;
      task.downloadedSize = startPosition;
    }
  }

  final request = HttpRequest.createHttp();
  final headers = Map<String, String>.from(task.headers);

  if (startPosition > 0) {
    headers['Range'] = 'bytes=$startPosition-';
  }

  request.request(
    task.url,
    method: HttpMethod.GET,
    header: headers,
    connectTimeout: _config.connectTimeoutMs,
    readTimeout: _config.readTimeoutMs,
    expectDataType: HttpDataType.ARRAY_BUFFER,
    callback: (err, resp) {
      if (err != null) {
        _handleDownloadError(task, err, startPosition);
        return;
      }

      if (resp.responseCode >= 400) {
        _handleDownloadError(
          task,
          Exception('HTTP ${resp.responseCode}'),
          startPosition,
        );
        return;
      }

      // 解析响应头获取文件大小
      final respHeaders = resp.header;
      if (respHeaders['content-length'] != null) {
        final contentLength = int.parse(respHeaders['content-length'].toString());
        if (startPosition > 0 && respHeaders['content-range'] != null) {
          // 断点续传:从 Content-Range 解析总大小
          final rangeHeader = respHeaders['content-range'].toString();
          final match = RegExp(r'/(\d+)$').firstMatch(rangeHeader);
          if (match != null) {
            task.totalSize = int.parse(match.group(1)!);
          }
        } else {
          task.totalSize = startPosition + contentLength;
        }
      }

      // 写入文件
      if (resp.result is ArrayBuffer) {
        final file = File(savePath);
        final raf = file.openSync(mode: FileMode.APPEND);
        raf.writeSync(resp.result);
        raf.closeSync();
        task.downloadedSize += resp.result.length;
        _notifyProgress(task);
      }

      _handleDownloadComplete(task, savePath);
    },
  );
}

这段代码是下载功能的核心。关键点包括:Range 请求头的使用实现断点续传、响应头的解析获取文件总大小、分块写入文件保证数据完整性。

4.4 暂停与恢复

暂停功能通过标记任务状态实现:

dart 复制代码
Future<bool> pauseDownload(String taskId) async {
  final task = _activeTasks[taskId];
  if (task == null) return false;

  if (task.status != DownloadStatus.DOWNLOADING) return false;

  final previousStatus = task.status;
  task.status = DownloadStatus.PAUSED;
  _notifyStateChange(task, previousStatus);

  _pausedTasks[taskId] = task;
  _activeTasks.remove(taskId);
  _saveToHistory(task);

  return true;
}

Future<bool> resumeDownload(String taskId) async {
  var task = _pausedTasks[taskId];

  if (task == null) {
    // 尝试从历史记录恢复
    task = await _loadFromHistory(taskId);
    if (task == null) return false;
  }

  _pausedTasks.remove(taskId);

  // 设置 Range 请求头从断点继续
  task.headers['Range'] = 'bytes=${task.downloadedSize}-';
  task.status = DownloadStatus.PENDING;
  _notifyStateChange(task, task.status);

  return (await startDownload(task)) == taskId;
}

需要注意的是,OpenHarmony 的 HTTP 模块不支持真正的请求中止。因此这里的"暂停"实际上是标记状态,下次恢复时通过 Range 请求从断点继续下载。

4.5 重试机制

下载失败时自动重试:

dart 复制代码
void _handleDownloadError(DownloadTask task, Error error, int startPosition) {
  task.retryCount++;

  if (task.canRetry()) {
    // 指数退避重试
    final delay = _config.retryDelayMs * task.retryCount;
    Future.delayed(Duration(milliseconds: delay), () {
      task.status = DownloadStatus.PENDING;
      _executeDownload(task);
    });
  } else {
    final previousStatus = task.status;
    task.status = DownloadStatus.FAILED;
    task.errorMessage = error.toString();
    task.endTime = DateTime.now().millisecondsSinceEpoch;
    _notifyStateChange(task, previousStatus);
    _notifyError(task, error);
    _saveToHistory(task);
    _activeTasks.remove(task.id);
    _processNextTask();
  }
}

重试策略采用指数退避算法,随着重试次数增加而延长等待时间,既能给网络恢复留出时间,又能避免过度重试。


五、历史记录管理

5.1 持久化存储

DownloadHistoryManager 负责下载历史记录的持久化存储:

dart 复制代码
class DownloadHistoryManager {
  final Context _context;
  final String _historyDir;
  final String _historyFile = 'download_history.json';
  final Map<String, DownloadHistoryRecord> _records = {};

  DownloadHistoryManager(this._context)
      : _historyDir = '${_context.filesDir}/download_history' {
    _initDirectory();
  }

  void _initDirectory() {
    final dir = Directory(_historyDir);
    if (!dir.existsSync()) {
      dir.createSync(recursive: true);
    }
  }

  Future<void> loadHistory() async {
    final file = File('$_historyDir/$_historyFile');
    if (!file.existsSync()) return;

    try {
      final content = file.readAsStringSync();
      final List<dynamic> data = jsonDecode(content);
      for (final item in data) {
        final record = DownloadHistoryRecord.fromJson(jsonEncode(item));
        if (record != null) {
          _records[record.id] = record;
        }
      }
    } catch (e) {
      // 解析失败,忽略
    }
  }

  void saveRecord(DownloadTask task) {
    final record = DownloadHistoryRecord(task);
    _records[record.id] = record;
    _saveToFile();
  }

  List<DownloadHistoryRecord> getAllRecords() {
    final list = _records.values.toList();
    list.sort((a, b) => b.createdTime.compareTo(a.createdTime));
    return list;
  }

  Map<String, dynamic> getStatistics() {
    int completed = 0, failed = 0, paused = 0;
    int totalBytes = 0;

    _records.forEach((_, record) {
      switch (record.status) {
        case DownloadStatus.COMPLETED:
          completed++;
          totalBytes += record.totalSize;
          break;
        case DownloadStatus.FAILED:
          failed++;
          break;
        case DownloadStatus.PAUSED:
          paused++;
          break;
      }
    });

    return {
      'total': _records.length,
      'completed': completed,
      'failed': failed,
      'paused': paused,
      'totalBytes': totalBytes,
    };
  }
}

使用应用私有目录存储历史记录,确保数据安全性。JSON 格式存储便于调试和迁移。


六、UI 页面实现

6.1 页面结构设计

下载页面采用 Tab 切换结构,分为"正在下载"和"历史记录"两个视图:

dart 复制代码
@Entry
@Component
struct DownloadPage {
  @State activeTasks: DownloadDisplayItem[] = [];
  @State historyRecords: DownloadHistoryRecord[] = [];
  @State currentTab: number = 0;
  @State statsCompleted: number = 0;
  @State statsTotal: number = 0;
  @State statsFailed: number = 0;

  DownloadManager? _manager;

  aboutToAppear() {
    _initManager();
    _startRefresh();
  }
}

6.2 实时进度展示

下载任务卡片展示进度条和速度信息:

dart 复制代码
@Builder
buildTaskCard(DownloadDisplayItem item) {
  Column() {
    Row() {
      Text(_getStatusIcon(item.status))
        .fontSize(24)

      Column() {
        Text(item.fileName)
          .fontSize(14)
          .maxLines(1)

        Text(item.statusText)
          .fontSize(12)
          .fontColor(_getStatusColor(item.status))
      }
      .layoutWeight(1)
      .margin({ left: 12 })
    }

    if (item.status == DownloadStatus.DOWNLOADING) {
      Column() {
        Progress({ value: item.progress, total: 100 })
          .height(4)

        Row() {
          Text('${item.progress}%')
          Blank()
          Text('${item.downloadedSize} / ${item.totalSize}')
        }
        .margin({ top: 6 })
      }
      .margin({ top: 12 })
    }

    Row() {
      if (item.status == DownloadStatus.DOWNLOADING) {
        Button('Pause')
          .onClick(() => _manager?.pauseDownload(item.id))
      } else if (item.status == DownloadStatus.PAUSED) {
        Button('Resume')
          .onClick(() => _manager?.resumeDownload(item.id))
      }

      Blank()

      Button('Cancel')
        .onClick(() => _manager?.cancelDownload(item.id))
    }
    .margin({ top: 12 })
  }
  .padding(16)
  .backgroundColor('#FFFFFF')
  .borderRadius(16)
}

6.3 添加下载对话框

点击添加按钮弹出输入框:

dart 复制代码
@Builder
buildAddDialog() {
  Column() {
    Text('Add Download')
      .fontSize(18)
      .fontWeight(FontWeight.Bold)

    TextInput({ placeholder: 'Enter URL' })
      .onChange((value) => this.urlInput = value)

    TextInput({ placeholder: 'File name (optional)' })
      .margin({ top: 12 })
      .onChange((value) => this.fileNameInput = value)

    Row() {
      Button('Cancel')
        .onClick(() => this.showAddDialog = false)

      Button('Download')
        .onClick(() => _startDownload())
    }
    .margin({ top: 20 })
  }
  .padding(24)
  .backgroundColor('#FFFFFF')
  .borderRadius(20)
}

七、运行验证

7.1 功能测试

在 OpenHarmony 模拟器上运行应用,测试以下场景:

测试场景一:正常下载

测试场景二:断点续传

  • 开始下载一个大文件
  • 中途取消下载
  • 重新开始同一 URL
  • 验证从断点继续而非重新下载

测试场景三:并发控制

  • 同时添加多个下载任务
  • 验证并发数限制生效
  • 观察任务队列机制

7.2 截图展示

以下是应用在 OpenHarmony 模拟器上的试运行效果:

图1:下载管理主界面

界面包含顶部导航栏、统计数据面板(已完成/总计/失败)、任务列表和添加按钮。

图2:添加下载任务

按"+"可选择下载

用户可通过输入框填写下载链接和文件名。

图3:历史记录

展示所有下载历史,支持重试失败任务和删除记录。


八、代码托管

本文涉及的完整代码已托管至 AtomGit 平台:

仓库地址: https://atomgit.com/maaath/flutter_download_demo

仓库结构说明:

复制代码
flutter_download_demo/
├── lib/
│   ├── model/
│   │   └── DownloadModels.ets    # 数据模型
│   ├── service/
│   │   ├── DownloadManager.ets        # 下载管理器
│   │   └── DownloadHistoryManager.ets # 历史管理器
│   └── pages/
│       └── DownloadPage.ets     # 下载页面
├── README.md                    # 项目说明
└── screenshot/                 # 运行截图

九、总结与展望

本文详细介绍了在 Flutter for OpenHarmony 项目中实现文件下载能力的完整方案。通过分层架构设计,我们实现了:

  1. 完善的状态管理:六种下载状态的正确定义与转换
  2. 可靠的下载机制:断点续传、自动重试、并发控制
  3. 持久化存储:下载历史记录的保存与加载
  4. 友好的用户界面:实时进度展示、清晰的状态提示

这套方案可以直接集成到实际项目中,满足大部分文件下载场景的需求。

可优化的方向:

  • 支持后台下载,使用 OpenHarmony 的后台任务机制
  • 添加下载速度限制选项
  • 实现更丰富的通知功能
  • 支持 iOS/Android 多平台适配

希望本文能为开发者在 OpenHarmony 平台上实现文件下载功能提供有价值的参考。如有问题或建议,欢迎在社区交流讨论。


参考链接:

  • Flutter for OpenHarmony 官方文档
  • OpenHarmony HTTP 请求指南
  • AtomGit 代码托管平台:https://atomgit.com

感谢各位阅读!

相关推荐
Hello__777711 小时前
开源鸿蒙 Flutter 实战|页面加载进度条全流程实现
flutter·开源·harmonyos
nashane11 小时前
HarmonyOS Text组件堆叠布局中的文字缩进避让技术详解
华为·harmonyos·harmonyos 5
hamber12 小时前
用 Flutter 造一台掌机
flutter·ai编程·全栈
爱艺江河12 小时前
智慧合规的HarmonyOS原生实践:与OpenClaw适配的项目方案浅析
人工智能·华为·harmonyos
三声三视14 小时前
ArkTS Navigation 路由实战:从 Router 迁移到 NavPathStack,打造企业级路由体系
华为·harmonyos·鸿蒙
程序员老刘·16 小时前
Flutter版本选择指南:3.41.7进入稳态,生产环境升级窗口开启 | 2026年4月
flutter·跨平台开发·客户端开发
Swift社区16 小时前
System + AI:下一代 鸿蒙App 架构
人工智能·架构·harmonyos
新小梦17 小时前
DevEco Studio修改HarmonyOS为OpenHarmony
harmonyos
IntMainJhy18 小时前
Flutter 三方库 shimmer 的鸿蒙化适配与实战指南
flutter·华为·harmonyos