8、权限与协程:使用协程封装权限请求、suspend函数设计、超时处理
说实话,权限请求这块,以前我写起来挺烦的。
每次都要写个回调,onRequestPermissionsResult 里塞一堆逻辑。代码一多,自己都绕晕了。后来协程普及了,我试着把权限请求封装成 suspend 函数,体验一下子清爽了很多。
这一章,我就跟你聊聊我是怎么用协程来封装权限请求的。包括 suspend 函数怎么设计、超时怎么处理、还有那些我踩过的坑。
8.1 为什么用协程处理权限?
你想想看,权限请求本质上是一个异步操作。用户点“允许”或“拒绝”,系统回调结果。这个过程天然适合用协程来表达。
用协程的好处很明显:
- 代码线性化:不用嵌套回调,逻辑从上往下读就行。
- 生命周期感知:配合 ViewModelScope 或 lifecycleScope,自动取消。
- 超时控制:用户一直不点?协程可以轻松设置超时。
- 错误处理:try-catch 一把梭,比回调里的错误分支清晰多了。
我在项目中遇到过一种情况:用户打开权限弹窗后,切到别的应用,过了几分钟才回来。这时候回调早就超时了,但代码还在等。用协程加超时,这个问题就解决了。
8.2 核心思路:用 suspendCancellableCoroutine 封装
Kotlin 协程提供了一个神器——suspendCancellableCoroutine。它能把基于回调的 API 转成 suspend 函数。
基本模式是这样的:
suspend fun requestPermission(context: Context, permission: String): Boolean {
return suspendCancellableCoroutine { continuation ->
// 1. 检查是否已有权限
if (ContextCompat.checkSelfPermission(context, permission) ==
PackageManager.PERMISSION_GRANTED
) {
continuation.resume(true)
return@suspendCancellableCoroutine
}
// 2. 注册回调
val callback = object : ActivityResultCallback<Map<String, Boolean>> {
override fun onActivityResult(result: Map<String, Boolean>) {
val granted = result[permission] ?: false
continuation.resume(granted)
}
}
// 3. 发起请求(这里用 ActivityResultLauncher)
launcher.launch(arrayOf(permission))
// 4. 取消时清理
continuation.invokeOnCancellation {
// 清理资源,比如移除回调
}
}
}
嗯,这里要注意:suspendCancellableCoroutine 和 suspendCoroutine 的区别。前者支持取消,后者不支持。权限请求这种场景,用户可能随时离开页面,取消能力是必须的。
8.3 设计一个通用的权限请求 suspend 函数
光一个权限不够,实际项目中经常要请求多个权限。我设计了一个通用函数:
suspend fun requestPermissions(
activity: ComponentActivity,
permissions: Array<String>
): Map<String, Boolean> {
return suspendCancellableCoroutine { continuation ->
// 过滤已授权的权限
val needRequest = permissions.filter {
ContextCompat.checkSelfPermission(activity, it) !=
PackageManager.PERMISSION_GRANTED
}.toTypedArray()
if (needRequest.isEmpty()) {
// 全部已授权,直接返回
continuation.resume(permissions.associateWith { true })
return@suspendCancellableCoroutine
}
// 注册 ActivityResult 回调
val launcher = activity.registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { result ->
// 合并结果:已授权的 + 刚请求的
val fullResult = mutableMapOf<String, Boolean>()
permissions.forEach { perm ->
fullResult[perm] = result[perm] ?: true
}
continuation.resume(fullResult)
}
launcher.launch(needRequest)
continuation.invokeOnCancellation {
launcher.unregister()
}
}
}
这个函数有几个设计点:
- 自动过滤已授权权限:避免重复弹窗,用户体验更好。
- 返回完整结果:调用方拿到所有权限的状态,不用自己拼。
- 取消时清理:防止内存泄漏。
8.4 超时处理:用户一直不点怎么办?
这个问题我遇到过好几次。用户打开权限弹窗,然后去接电话了,或者干脆把手机放一边。你的协程就一直挂在那里,不超时也不取消。
解决方案很简单:用 withTimeout 或 withTimeoutOrNull 包裹请求逻辑。
suspend fun requestPermissionWithTimeout(
activity: ComponentActivity,
permission: String,
timeoutMillis: Long = 10_000L
): Boolean? {
return withTimeoutOrNull(timeoutMillis) {
requestPermission(activity, permission)
}
}
调用方可以这样用:
val result = requestPermissionWithTimeout(activity, Manifest.permission.CAMERA)
when (result) {
null -> {
// 超时了,用户没操作
showToast("操作超时,请重试")
}
true -> {
// 授权成功
openCamera()
}
false -> {
// 被拒绝了
showRationaleDialog()
}
}
withTimeout 超时后会抛出 TimeoutCancellationException,是 CancellationException 的子类。如果你用 try-catch 捕获,记得别把它吞掉,否则协程的取消机制会受影响。
我曾经踩过这个坑:在 try-catch 里写了 catch (e: Exception) { },结果协程取消时静默失败了,导致后续的权限请求全部乱套。后来改成 catch (e: TimeoutCancellationException) { } 才解决。
8.5 完整的封装示例
把上面这些整合起来,一个完整的权限请求工具类大概长这样:
class PermissionManager(private val activity: ComponentActivity) {
private val requestMap = mutableMapOf<String, ActivityResultLauncher<Array<String>>>()
suspend fun request(
vararg permissions: String,
timeoutMillis: Long = 15_000L
): Map<String, Boolean> {
return withTimeout(timeoutMillis) {
val needRequest = permissions.filter { !isGranted(it) }.toTypedArray()
if (needRequest.isEmpty()) {
return@withTimeout permissions.associateWith { true }
}
suspendCancellableCoroutine { continuation ->
val launcher = activity.registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { result ->
val fullResult = permissions.associateWith { perm ->
result[perm] ?: isGranted(perm)
}
continuation.resume(fullResult)
}
requestMap[permissions.joinToString()] = launcher
launcher.launch(needRequest)
continuation.invokeOnCancellation {
launcher.unregister()
requestMap.remove(permissions.joinToString())
}
}
}
}
private fun isGranted(permission: String): Boolean {
return ContextCompat.checkSelfPermission(activity, permission) ==
PackageManager.PERMISSION_GRANTED
}
fun cleanup() {
requestMap.values.forEach { it.unregister() }
requestMap.clear()
}
}
8.6 知识体系总览
下面这张图,我把这一章的核心逻辑串起来了:
8.7 避坑指南
最后,分享几个我实际踩过的坑:
- 不要在非 UI 线程启动权限请求:ActivityResultLauncher 必须在 Activity 的生命周期内使用。在后台协程里启动,会直接崩溃。
- 重复注册问题:同一个 launcher 不要多次注册。我习惯在请求前先 unregister 旧的。
- 权限被永久拒绝:用户勾选了“不再询问”后,弹窗不会再出现。这时候要引导用户去设置页手动开启。这个逻辑也要在协程里处理。
- Fragment 场景:如果在 Fragment 里请求权限,记得用
registerForActivityResult时传入 Fragment 本身,而不是 Activity。否则生命周期会错乱。
核心总结:用协程封装权限请求,本质上是把异步回调转成同步写法。核心工具是 suspendCancellableCoroutine,核心保障是 withTimeout。设计时注意生命周期绑定和资源清理,就能写出既优雅又健壮的权限请求代码。
公众号:蓝海资料掘金营,微信deep3321