权限状态模型设计

权限状态模型是整个协程权限管理系统的数据基石。本节定义状态枚举、状态转换图、不可变快照以及状态合并策略,为后续的 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)规则,非法转换由编译器禁止或运行时断言拦截:

权限状态转换图 Initial Requesting Granted Denied Revoked 虚线 = 系统主动转换(非用户操作);实线 = 应用请求驱动转换 Revoked → Initial 表示重新请求;从 Denied 也可经用户操作回到 Requesting
转换约束: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] 标签,便于追踪合并路径中的发射来源。