3. 权限状态建模与请求流程设计
权限请求本质上是一个有限状态机(FSM)。在协程环境下,我们需要将权限的六种核心状态与四种触发动作建模为可重入、可取消的状态转换模型。本节定义状态枚举、绘制状态转换图,并实现基于协程的安全转换引擎。
3.1 权限状态枚举
每个权限在生命周期中都具有以下状态,使用 Kotlin sealed class 实现闭枚举。状态定义表如下:
| 状态名称 | 标志位 | 说明 | 是否可请求 |
|---|---|---|---|
NOT_REQUESTED |
0 | 从未发起过请求 | 是 |
REQUESTING |
1 | 请求已发出,等待用户决策 | 否 |
GRANTED |
2 | 用户已授权 | 否(终态) |
DENIED |
3 | 用户拒绝,但可再次请求 | 是 |
PERMANENTLY_DENIED |
4 | 用户勾选“不再询问”或系统拒绝 | 需引导设置 |
RETRYING |
5 | 自动重试队列中,等待退避后重试 | 协程控制 |
此外,我们还定义 COMPLETED 作为请求流程完成的哨兵状态(内部使用)。枚举示例代码:
sealed class PermissionState {
object NotRequested : PermissionState()
object Requesting : PermissionState()
object Granted : PermissionState()
data class Denied(val shouldRetry: Boolean) : PermissionState()
object PermanentlyDenied : PermissionState()
data class Retrying(val attempt: Int, val delayMs: Long) : PermissionState()
}
3.2 请求状态转换图
下图展示了权限从 NotRequested 出发,经过请求、用户决策、重试等路径的完整状态转换。每个箭头代表一个动作或事件。
3.3 协程状态机实现
使用 Channel 作为事件队列,flow 暴露状态变化,通过 stateIn 绑定到 CoroutineScope。核心状态循环:
class PermissionStateMachine(private val scope: CoroutineScope) {
private val _state = MutableStateFlow<PermissionState>(PermissionState.NotRequested)
val state: StateFlow<PermissionState> = _state.asStateFlow()
private val events = Channel<PermissionAction>(Channel.UNLIMITED)
sealed class PermissionAction {
object Request : PermissionAction()
object Grant : PermissionAction()
object Deny : PermissionAction()
data class PermanentDeny(val showSettings: Boolean) : PermissionAction()
object Retry : PermissionAction()
}
init {
scope.launch {
events.consumeAsFlow().collect { action ->
val current = _state.value
_state.value = when {
// NotRequested → Requesting
current is PermissionState.NotRequested && action is PermissionAction.Request ->
PermissionState.Requesting
// Requesting → Granted
current is PermissionState.Requesting && action is PermissionAction.Grant ->
PermissionState.Granted
// Requesting → Denied
current is PermissionState.Requesting && action is PermissionAction.Deny ->
PermissionState.Denied(shouldRetry = true)
// Denied → Requesting (手动)
current is PermissionState.Denied && action is PermissionAction.Request ->
PermissionState.Requesting
// Denied → PermanentlyDenied
current is PermissionState.Denied && action is PermissionAction.PermanentDeny ->
PermissionState.PermanentlyDenied
// Denied → Retrying (自动)
current is PermissionState.Denied && action is PermissionAction.Retry ->
PermissionState.Retrying(attempt = 1, delayMs = 1000)
// Retrying → Requesting (退避后)
current is PermissionState.Retrying && action is PermissionAction.Request ->
PermissionState.Requesting
// 其他情况保持不变
else -> current
}
}
}
}
fun send(action: PermissionAction) {
events.trySend(action)
}
}
3.4 异常处理与重试策略
生命周期安全:使用
viewModelScope 或 AndroidOwner.lifecycleScope 创建状态机实例,确保在 Fragment/Activity 重建时自动取消协程,避免内存泄漏。
重试策略采用指数退避 + 最大重试次数限制。请求失败时自动进入 Retrying 状态,并在延迟后发送 Retry 事件:
fun requestWithRetry(stateMachine: PermissionStateMachine, maxRetries: Int = 3) {
stateMachine.send(PermissionAction.Request)
// 监听状态变化,当进入 Denied 且 shouldRetry 为 true 时启动重试
stateMachine.state.filterIsInstance<PermissionState.Denied>()
.take(maxRetries)
.onEach { deniedState ->
val delay = (1L shl deniedState.hashCode()) * 500L // 指数退避:0.5s, 1s, 2s, ...
delay(delay)
stateMachine.send(PermissionAction.Retry)
}
.launchIn(scope)
}
3.5 常见错误与调试
- 状态丢失:未使用
StateFlow的重放机制,导致重建后状态重置。解决方案:在 ViewModel 中使用stateIn(scope, SharingStarted.WhileSubscribed(5000), InitialValue)保留状态。 - 事件竞争(Race Condition):同时发送
Request和用户回调的Grant导致状态跳跃。应在协程内部使用mutex或确保一次性消费。 - 永久拒绝后仍重试:状态机未对
PermanentlyDenied做守卫。在requestWithRetry入口处检查_state.value是否为PermanentlyDenied,若是则直接跳转到Settings界面。 - 协程取消后未清理事件通道:使用
Channel(UNLIMITED)且未主动关闭,可能造成背压。推荐绑定生命周期并在onCleared()或onStop()时调用events.close()。
性能注意:重试状态的退避时间建议使用
kotlinx.coroutines.delay 而非 Thread.sleep,避免阻塞调度线程。如果重试频繁,考虑使用 Flows.combine 合并多个权限状态,减少重复监听。