22、权限管理:运行时权限的封装与ViewModel中的权限处理
权限管理这个话题,说实话,是Android开发里绕不开的一个坎儿。尤其是从Android 6.0开始,Google搞了个运行时权限机制,咱们开发者就得跟用户玩「请求-授权-拒绝」这套游戏了。我个人习惯把权限相关的逻辑封装起来,别让它在Activity或Fragment里散得到处都是。今天咱们就聊聊,怎么用Kotlin把这套东西做得优雅一点。
运行时权限的基本套路
先回顾一下基础。运行时权限的核心流程其实不复杂:
- 检查权限是否已授予
- 如果没授予,就发起请求
- 处理用户的授权结果
- 如果用户拒绝了,还得考虑要不要解释为什么需要这个权限
嗯,这里要注意:不是所有权限都需要运行时请求。像INTERNET、ACCESS_NETWORK_STATE这些,属于正常权限,直接在Manifest里声明就行。只有危险权限(Dangerous Permissions)才需要运行时弹窗。
核心原则:权限请求的发起方应该是UI层(Activity/Fragment),但权限状态的维护和业务逻辑的触发,应该交给ViewModel。
封装一个权限请求工具
我在项目中遇到过好几次,同一个Activity里多个地方都要请求权限,代码写得又臭又长。后来我封装了一个简单的工具类,用回调的方式处理结果。
class PermissionHelper(private val activity: Activity) {
private val requestCodeMap = mutableMapOf<Int, (Boolean) -> Unit>()
fun request(permission: String, callback: (Boolean) -> Unit) {
if (ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED) {
callback(true)
return
}
val requestCode = generateRequestCode()
requestCodeMap[requestCode] = callback
ActivityCompat.requestPermissions(activity, arrayOf(permission), requestCode)
}
fun onRequestPermissionsResult(requestCode: Int, grantResults: IntArray) {
val callback = requestCodeMap.remove(requestCode) ?: return
callback(grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)
}
private fun generateRequestCode(): Int {
return System.currentTimeMillis().toInt() and 0xFFFF
}
}
说白了,这个工具就是把权限请求的样板代码藏起来了。你只需要在Activity里调一下request(),然后重写onRequestPermissionsResult把结果转发给helper就行。
小技巧:requestCode别用固定值,否则多个请求同时发起时会互相覆盖。我习惯用时间戳的低16位,基本不会冲突。
ViewModel中的权限处理
很多新手会把权限请求的逻辑直接写在ViewModel里。你想想看,ViewModel里能拿到Context吗?能。但千万别在ViewModel里直接调用requestPermissions()。为什么?因为ViewModel的生命周期比Activity长,权限弹窗是跟Activity绑定的。ViewModel里发起请求,Activity被重建了怎么办?
正确的做法是:ViewModel只负责「我需要这个权限」的状态,UI层负责「怎么去请求」。
class ProfileViewModel : ViewModel() {
private val _permissionState = MutableLiveData<PermissionState>()
val permissionState: LiveData<PermissionState> = _permissionState
fun onCameraPermissionResult(granted: Boolean) {
if (granted) {
_permissionState.value = PermissionState.Granted
// 执行拍照逻辑
} else {
_permissionState.value = PermissionState.Denied
}
}
fun onTakePhotoClicked() {
_permissionState.value = PermissionState.Requesting("android.permission.CAMERA")
}
}
sealed class PermissionState {
object Idle : PermissionState()
data class Requesting(val permission: String) : PermissionState()
object Granted : PermissionState()
object Denied : PermissionState()
}
看到没?ViewModel里只暴露了一个permissionState的LiveData。当用户点击「拍照」按钮时,ViewModel把状态变成Requesting。Activity观察到这个状态,就去发起权限请求。请求结果再通过onCameraPermissionResult()传回ViewModel。
我曾经踩过的坑:在ViewModel里直接持有Activity的引用,然后调用requestPermissions()。结果Activity旋转屏幕后,ViewModel还在,但Activity已经重建了,弹窗回调永远收不到。嗯,从那以后我再也不这么干了。
处理「拒绝并不再询问」
用户如果勾选了「不再询问」,那下次再请求权限时,系统弹窗就不会出现了。这时候你得引导用户去设置里手动开启。怎么判断这个状态?
fun shouldShowRationale(activity: Activity, permission: String): Boolean {
return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
}
fun isPermanentlyDenied(activity: Activity, permission: String): Boolean {
return !shouldShowRationale(activity, permission) &&
ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_DENIED
}
逻辑是这样的:如果权限被拒绝,而且shouldShowRequestPermissionRationale返回false,那就说明用户选了「不再询问」。这时候弹个Dialog,告诉用户「请去设置里开启权限」,然后跳转到应用详情页。
fun openAppSettings(context: Context) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.parse("package:${context.packageName}")
}
context.startActivity(intent)
}
用Flow来封装权限请求
如果你项目里用了协程和Flow,那权限请求也可以做得更「响应式」一些。我个人比较喜欢这种方式,因为它跟ViewModel的viewModelScope配合得很好。
class PermissionFlow(private val activity: Activity) {
fun request(permission: String): Flow<Boolean> = callbackFlow {
if (ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED) {
send(true)
close()
return@callbackFlow
}
val requestCode = Random.nextInt()
val callback = object : ActivityResultCallback<Map<String, Boolean>> {
override fun onActivityResult(result: Map<String, Boolean>) {
trySend(result[permission] ?: false)
close()
}
}
// 这里需要配合ActivityResultLauncher使用
// 实际项目中建议用ActivityResultContracts.RequestPermission
awaitClose { /* 清理工作 */ }
}
}
当然,上面的代码只是个示意。实际项目中,我更推荐用Google官方的ActivityResultContracts.RequestPermission,它已经帮我们封装好了。
我的建议:如果项目已经用了ActivityResultContracts,那就别自己造轮子了。直接在Activity里注册launcher,然后把结果通过ViewModel的方法传进去。简单、可靠、不易出错。
整体架构图
下面这张图展示了权限管理在MVVM架构中的流转过程。你可以看到,ViewModel只负责状态管理,真正的权限请求操作发生在UI层。
总结一下
权限管理这事儿,说白了就是「谁负责请求,谁负责处理结果」的问题。我的经验是:
- UI层:负责发起请求、处理系统回调、跳转设置页
- ViewModel:负责维护权限状态、触发后续业务逻辑
- 工具类:封装重复的检查、请求代码,让UI层更干净
另外,别忘了处理「拒绝并不再询问」的情况。我见过不少App,用户拒绝一次后就再也不弹权限请求了,也不给任何提示,用户还以为App坏了。其实加个引导Dialog就能解决,用户体验会好很多。
最后提醒一句:权限请求的时机很重要。别在App启动时一股脑把所有权限都要了,用户会很反感。等到真正需要的时候再请求,成功率会高很多。这个道理,你想想看,是不是跟现实生活里找人帮忙一样?
公众号:蓝海资料掘金营,微信deep3321