羽毛球学习 HarmonyOS 设计续篇(21):下载任务与离线缓存的可观测状态

📅 2026/7/25 13:25:09
羽毛球学习 HarmonyOS 设计续篇(21):下载任务与离线缓存的可观测状态
一、从空状态出发先限定设计范围现阶段的下载页只呈现“暂无下载内容”个人中心也没有开放下载入口。这意味着下载任务、文件缓存、状态恢复和页面联动都还不是可运行功能。与其用一张相邻页面截图包装成实战不如先把离线能力的合同写清楚用户从课程详情发起下载任务在前台可见退出页面或重启应用后仍能恢复结果网络中断、存储不足和文件损坏都有明确去向。本文只设计课程视频与配套图文的应用内离线缓存不包含系统级后台下载服务、跨设备同步、DRM 内容授权和云端进度同步。首期也不承诺应用退出后继续传输而是采用“前台下载、持久化任务、再次进入后可恢复”的保守边界。这样能先验证数据与界面闭环再决定是否引入后台任务能力。用户动作系统需要给出的反馈首期边界点击下载立即出现等待或下载中状态可取消应用前台内传输离开详情页下载页和详情页显示同一进度共享任务仓库断网后重试保留已写入字节显示可恢复原因服务端支持 Range 时续传重启应用重新读取任务索引并校验文件不自动在后台启动删除离线内容删除文件与索引失败时可再次清理不删除课程本身二、任务状态机是唯一事实来源下载按钮文字、进度条、离线徽标和错误提示不能各自保存布尔值。设计中以任务状态机作为唯一事实来源页面只把状态投影成视觉结果。任务至少经过idle、queued、downloading、paused、verifying、completed、failed和removing八种状态取消后的任务回到idle但保留最近一次错误仅用于诊断展示。以下类型是拟定的数据合同不代表当前项目中已经存在对应实现export type DownloadPhase | idle | queued | downloading | paused | verifying | completed | failed | removing export type DownloadFailure | network_unavailable | http_rejected | storage_full | permission_denied | checksum_mismatch | file_missing | unknown export interface DownloadTask { courseId: string assetId: string sourceUrl: string localUri?: string phase: DownloadPhase receivedBytes: number totalBytes?: number etag?: string checksum?: string failure?: DownloadFailure retryCount: number updatedAt: number } export interface OfflineCourseState { courseId: string tasks: DownloadTask[] readyAssetCount: number requiredAssetCount: number isOfflineReady: boolean }completed不能仅由网络请求成功决定。任务必须先进入verifying确认文件存在、大小符合预期并在服务端提供校验值时完成摘要校验才能成为可播放的离线资源。这样可避免“进度已经 100%播放器却找不到文件”的假完成状态。三、持久化索引与文件写入必须分开二进制文件负责承载内容轻量索引负责恢复任务。索引适合保存课程 ID、资源 ID、传输进度、ETag、文件 URI、失败原因和更新时间不保存大块二进制。文件写入采用临时文件校验通过后再替换正式文件应用在启动时发现残留临时文件只把任务恢复为paused或failed不能直接标记完成。export interface DownloadIndexStore { list(): PromiseDownloadTask[] get(assetId: string): PromiseDownloadTask | undefined put(task: DownloadTask): Promisevoid remove(assetId: string): Promisevoid } export interface DownloadFileStore { createTemp(assetId: string): Promisestring append(tempUri: string, bytes: ArrayBuffer): Promisenumber size(uri: string): Promisenumber verify(uri: string, checksum?: string): Promiseboolean commit(tempUri: string, assetId: string): Promisestring remove(uri: string): Promisevoid exists(uri: string): Promiseboolean } export interface DownloadTransport { open(input: { url: string offset: number etag?: string }): PromiseDownloadStream } export interface DownloadStream { totalBytes?: number etag?: string read(): PromiseArrayBuffer | undefined close(): Promisevoid }若服务端不支持范围请求暂停后只能从头开始但仍应保留课程选择和错误原因。若 ETag 改变说明远端资源已更新旧临时文件不能继续拼接。设计选择“丢弃旧分片并明确提示资源已更新”而不是冒险生成内容错位的文件。四、Repository 只暴露状态流和意图页面不应直接操作网络、Preferences 或文件。下载仓库接收用户意图串行化同一资源的动作并向所有页面提供同一份状态快照。详情页、下载页和播放器因此不需要彼此通知只要订阅相同的courseId或assetId就能得到一致结果。export interface DownloadRepository { observeCourse( courseId: string, listener: (state: OfflineCourseState) void ): () void enqueue(courseId: string, assetId: string): Promisevoid pause(assetId: string): Promisevoid resume(assetId: string): Promisevoid cancel(assetId: string): Promisevoid removeOfflineCourse(courseId: string): Promisevoid reconcileOnStartup(): Promisevoid } export class DownloadUseCase { constructor(private repository: DownloadRepository) {} async handle( intent: download | pause | resume | cancel, courseId: string, assetId: string ): Promisevoid { if (intent download) { return this.repository.enqueue(courseId, assetId) } if (intent pause) { return this.repository.pause(assetId) } if (intent resume) { return this.repository.resume(assetId) } return this.repository.cancel(assetId) } }同一资源的重复点击需要幂等处理queued或downloading状态再次收到下载意图时不创建第二个任务completed状态再次点击时跳转到离线播放或管理入口removing状态暂时禁用其他动作。跨资源可以并发但首期建议限制为两个活动任务减少存储写入和网络抖动对播放体验的影响。五、页面只消费稳定的视觉模型领域状态不应直接散落在 ArkUI 分支中。可先把任务转换为稳定的视觉模型再由组件渲染按钮、进度、说明和可用动作。进度未知时不伪造百分比而是显示不定进度失败原因必须映射成用户能理解的提示同时保留可重试与不可重试的区别。export interface DownloadViewState { label: string progress?: number showSpinner: boolean primaryAction?: download | pause | resume | play secondaryAction?: cancel | remove message?: string offlineReady: boolean } export function projectDownloadView(task: DownloadTask): DownloadViewState { switch (task.phase) { case queued: return { label: 等待下载, showSpinner: true, secondaryAction: cancel, offlineReady: false } case downloading: return { label: task.totalBytes ? 下载中 : 正在连接, progress: task.totalBytes ? task.receivedBytes / task.totalBytes : undefined, showSpinner: !task.totalBytes, primaryAction: pause, secondaryAction: cancel, offlineReady: false } case paused: return { label: 已暂停, showSpinner: false, primaryAction: resume, secondaryAction: cancel, offlineReady: false } case completed: return { label: 可离线播放, showSpinner: false, primaryAction: play, secondaryAction: remove, offlineReady: true } case failed: return failureView(task.failure) default: return idleDownloadView() } }下载页按“进行中、可离线、失败”分组详情页只展示当前课程摘要播放器在进入离线模式前再次确认文件存在。三处都消费同一仓库却使用不同视觉模型避免把完整任务对象耦合进每个页面。六、失败与降级路径要在交互中可见网络不可用时尚未开始的任务保持queued并提示联网后手动继续传输中断时转为paused不把它误写成业务失败。存储空间不足属于不可自动重试错误需要引导用户清理空间或删除其他离线内容。校验失败则删除本次临时文件并允许重新下载不能播放可能损坏的内容。失败点状态变化用户可执行动作数据处理无网络queued → paused重新连接后继续保留索引服务端拒绝downloading → failed查看提示或稍后重试保留诊断字段空间不足downloading → failed清理空间、删除离线内容删除不完整分片ETag 改变paused → queued从头下载丢弃旧分片校验不通过verifying → failed重新下载删除临时文件正式文件丢失completed → failed修复或重新下载清除失效 URI首期不做后台持续传输时应用进入后台可主动暂停并保存进度再次回到前台后由用户继续。这个降级虽然牺牲自动完成率却能避免在尚未验证生命周期和系统配额前承诺不可靠的后台能力。若后续引入系统后台任务状态合同和页面投影可以保持不变只替换传输调度层。七、实施顺序从可恢复最小闭环开始第一阶段只完成单资源前台下载定义状态机、临时文件、索引存储和详情页按钮。第二阶段加入下载页分组、暂停恢复、并发上限与启动校准。第三阶段接入播放器离线 URI、文件完整性检查和空间治理。最后才评估后台任务、批量下载和多设备同步。实施时每个阶段都必须留有降级出口。索引读取失败时进入空状态并提示修复不能删除未知文件文件目录不可写时关闭下载入口但保留在线播放课程资源缺少长度或校验值时允许传输但完成前至少核对文件存在与非零大小。Preferences 的使用方式可参考华为 HarmonyOS 数据持久化指南。八、验收条件与后续证据计划方案转入实现复盘前需要同时获得源码和运行证据。源码侧至少应出现任务模型、Repository 接口、索引存储、文件存储与页面投影运行侧要在同一构建中完成正常下载、暂停恢复、重启校准、断网、空间不足或模拟写入失败、删除重下六组验证。验收主题通过条件需要保留的证据状态一致性详情页和下载页进度一致同一时刻双页面截图或录屏重启恢复强制停止后仍显示正确状态停止前后任务快照文件完整性完成后可离线播放损坏文件被拒绝播放结果与校验日志失败可解释每类失败有明确动作不停在假进度失败态截图与错误映射删除闭环文件与索引都被清理可重新下载删除前后存储与页面状态生命周期切后台、返回、切页面不产生重复任务任务 ID 与进度变化记录在这些证据齐备前能力状态仍应明确标注为待实施结构图只能解释方案不能替代运行截图。真正落地后再用当前构建的页面、文件状态和错误回读整理实现复盘明确哪些设计被保留、哪些因平台约束而调整。九、总结课程离线能力的难点不在一个下载按钮而在状态、文件和页面是否共享同一事实。以任务状态机统一进度以临时文件和轻量索引保证恢复以 Repository 分隔用户意图与传输细节再把失败映射成可操作的视觉状态才能形成可验收的最小闭环。先验证前台下载与重启恢复再扩展后台和跨设备能力风险更可控也不会让尚未落地的能力提前变成产品承诺。