权限状态模型设计
权限状态模型是整个协程权限管理系统的数据基石。本节定义状态枚举、状态转换图、不可变快照以及状态合并策略,为后续的 Flow 驱动状态机提供精确的类型约束与转换规则。
2.1 状态枚举定义
采用密封类(sealed class)表达离散、有限的权限状态,每个状态携带各自的数据载荷:
sealed class PermissionState {
object Initial : PermissionState()
data class Requesting(val attempts: Int) : PermissionState()
object Granted : PermissionState()
data class Denied(val reason: String, val canRetry: Boolean) : PermissionState()
object Revoked : PermissionState()
}
| 状态 | 语义 | 数据载荷 |
|---|---|---|
| Initial | 未请求,无缓存 | 无 |
| Requesting | 请求进行中 | 重试次数 |
| Granted | 用户授权 | 无 |
| Denied | 被拒绝,可重试或不可重试 | 拒绝原因 + 重试标志 |
| Revoked | 运行时被系统撤销 | 无 |
2.2 状态转换图
状态转换必须遵循有限状态机(FSM)规则,非法转换由编译器禁止或运行时断言拦截:
转换约束:Granted 状态不可直接回到 Requesting,必须先被系统 Revoked 或由用户进入 Denied 后再重试。此规则防止反复弹窗骚扰用户。
2.3 不可变状态快照
Flow 发射的状态必须是不可变快照,确保下游观察者无副作用竞争:
data class PermissionStateSnapshot(
val state: PermissionState,
val timestamp: Long,
val source: SnapshotSource
)
enum class SnapshotSource {
CACHE, // 来自持久化缓存
FRESH, // 来自即时探测或请求结果
COMBINED // 来自合并策略
}
每次发射都生成新的快照实例,不允许修改已有快照字段。Kotlin 的 data class 自动提供 copy(),但业务层应避免直接复制修改,始终通过状态机产生新快照。
2.4 状态合并策略
当多个权限源(运行时探测、缓存、监听回调)并发发射状态时,需要合并策略保证一致性:
- 最新胜出(Last Wins): 以时间戳最近的快照为准,适用于 Granted / Revoked 等幂等状态。
- 保守合并(Conservative Union): 如果任一源报告 Denied,合并结果为 Denied;仅当所有源一致为 Granted 时才输出 Granted。
- 降级惩罚(Degradation Penalty): 如果合并过程中出现 Requesting + Denied,优先取 Denied 并标记可重试,避免停留在中间态导致界面闪烁。
fun mergeSnapshots(
snapshots: List<PermissionStateSnapshot>
): PermissionStateSnapshot {
if (snapshots.isEmpty()) return PermissionStateSnapshot(
PermissionState.Initial, 0L, SnapshotSource.COMBINED
)
val anyDenied = snapshots.any { it.state is PermissionState.Denied }
val anyGranted = snapshots.any { it.state is PermissionState.Granted }
val anyRevoked = snapshots.any { it.state is PermissionState.Revoked }
return when {
anyRevoked -> snapshots.maxByOrNull { it.timestamp }!!
anyDenied -> snapshots.filter { it.state is PermissionState.Denied }
.maxByOrNull { it.timestamp }!!
anyGranted && snapshots.all { it.state is PermissionState.Granted } ->
snapshots.maxByOrNull { it.timestamp }!!
else -> snapshots.maxByOrNull { it.timestamp }!!
}.copy(source = SnapshotSource.COMBINED)
}
2.5 常见错误与调试
状态泄漏陷阱: 在
combine 操作符内部直接修改上一个快照会导致并发 IllegalStateException。必须始终通过不可变 data class 的构造或 copy 生成新对象。| 错误模式 | 症状 | 修复 |
|---|---|---|
| mutable 状态字段 | UI 观察到不一致值 | 改为 val + data class |
| 合并忽略时间戳 | 旧缓存覆盖新授权 | 按 timestamp 降序取最新 |
| Denied 未保留原因 | 提示文案泛化 | 合并时保留 Denied.reason |
| Revoked 后未重置 | UI 永久卡在 Revoked | 状态机允许 Revoked → Initial |
2.6 性能与稳定性优化
- 减少快照复制: 使用
@Stable注解标记合并函数,避免 Compose 重组过度。 - 去重发射: 在 Flow 链中插入
distinctUntilChanged,防止重复的快照触发 UI 刷新。 - 定时清理缓存快照: 合并时若发现时间戳小于当前时间 10 秒的旧缓存,直接丢弃并标记为 COMBINED 源重新探测。
生产建议: 在 Debug 构建中为每个快照添加
coroutineContext[CoroutineName] 标签,便于追踪合并路径中的发射来源。