2. 任务状态枚举、进度百分比定义、异常与完成回调

任务进度模型是观察者模式中数据驱动的核心骨架。一个健壮的模型必须回答三个问题:任务当前处于什么生命周期?(状态枚举)、执行到什么程度?(进度百分比)、结束时发生了什么?(异常与完成回调)。本章从这三要素出发,构建一个可被 LiveData 或 Flow 直接消费的数据模型。

2.1 任务状态枚举(TaskState)

状态枚举用于标记任务从创建到终结的当前阶段。常见的状态包括:

状态含义是否终态
IDLE任务已定义,但尚未开始执行否
RUNNING任务正在执行中否
PAUSED任务被挂起(适用于可暂停场景)否
SUCCESS任务成功完成是
FAILED任务因异常或逻辑错误终止是
CANCELLED任务被用户或系统主动取消是
设计建议:不要将 PAUSED 作为必需状态,仅在支持暂停/恢复的场景中定义它。非终态应保持可连续转化,终态则不可再变更。

Kotlin 枚举实现示例:

enum class TaskState {
    IDLE,
    RUNNING,
    PAUSED,
    SUCCESS,
    FAILED,
    CANCELLED
}

2.2 进度百分比定义

进度百分比表示任务完成的相对量。通常定义为 0~100 的整数,或 0.0~1.0 的浮点数。当进度无法计算时,使用 -1 或 null 表示“未知”。

方案类型适用场景
整数百分比Int(0~100)文件下载、批量处理等精确可数任务
浮点进度Float(0.0~1.0)视频转码、模型训练等连续进度
未知进度Int = -1 或 Float = -1f任务未开始或状态不可度量
data class TaskProgress(
    val state: TaskState,
    val percentage: Int   // -1表示未知,0~100有效
)
边界陷阱:切勿将进度更新频率设得过高(如每毫秒更新一次),会导致 LiveData 或 Flow 的 Observer 被淹没,引发 UI 卡顿。建议采用节流策略,至少间隔 50ms 再发射一次进度更新。

2.3 异常与完成回调

任务结束时需要返回执行结果或异常信息。定义统一的回调接口可以解耦调用者与任务实现。

sealed class TaskResult<out T> {
    data class Success<T>(val data: T) : TaskResult<T>()
    data class Failure(val exception: Throwable) : TaskResult<Nothing>()
    object Cancelled : TaskResult<Nothing>()
}

interface TaskCallback<T> {
    fun onProgress(percentage: Int)
    fun onComplete(result: TaskResult<T>)
}

使用 sealed class 能够确保所有分支在 when 表达式中被穷举,避免遗漏异常处理。

2.4 完整数据模型

将状态、进度、结果合并为一个不可变数据类,作为 LiveData/Flow 的发射值:

data class TaskUiState<out T>(
    val state: TaskState = TaskState.IDLE,
    val percentage: Int = -1,
    val result: TaskResult<T>? = null
) {
    val isFinished: Boolean
        get() = state == TaskState.SUCCESS ||
                state == TaskState.FAILED ||
                state == TaskState.CANCELLED
}

该模型具备以下特性:

  • 所有字段不可变,适合多线程环境下的观察者。
  • 通过 isFinished 快速判断任务是否终结。
  • 与 LiveData 和 Flow 天然兼容,发射后 UI 层直接绑定。

2.5 状态转换关系(SVG 状态机图)

下图描述了状态之间的合法转换路径,箭头标记有效转移,终态无法再回到非终态。

任务状态转换图 IDLE RUNNING PAUSED SUCCESS FAILED CANCELLED 允许的状态转移:非终态 → 非终态/终态,终态不可逆(图中省略了重复或无效箭头)
关键约束:状态转换必须从非终态出发,终态(SUCCESS/FAILED/CANCELLED)不再连接任何出边。在代码实现中,每次状态变更前应校验当前状态是否已在终态,防止非法转换。

2.6 常见错误与稳定性优化

  • 状态与进度不一致:例如状态已经是 SUCCESS,但进度显示为 50%。解决方法:将状态和进度绑定在同一个不可变对象中同时发射,避免异步拆分更新。
  • 回调泄露:在 ViewModel 或 Repository 中持有 Activity/Fragment 引用。必须使用 LiveData 或 Flow.collect() 在合适的生命周期内订阅,并且弱化对 UI 的直接引用。
  • 进度更新过密导致 UI 抖动:使用 Flow.sample(50) 或 LiveData 的 postValue 内部合并机制控制频率。
  • 异常类型丢失:将 Throwable 直接暴露给 UI 层可能会包含敏感信息。建议在 Repository 层将异常包装为业务异常,只传递错误码或用户友好的描述。
线程安全提醒:如果使用 MutableStateFlow 或 MutableLiveData 在多线程中更新同一个 TaskUiState,必须使用线程安全的写操作(如 StateFlow.update { } 或 LiveData.postValue()),以免产生竞态条件导致状态错乱。