携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情
Android的权限申请一直是一个老大麻烦的事情,至少本人是一直深受其害。而在 Compose 框架下的权限申请,没法利用Activity,onRequestPermissionsResult()的回调,应该怎么做呢?
Accompanist
直接上硬菜:Accompanist,这是官方提供的一套十分有用的工具,包含了很多常用的开发需求,比如Pager、Navigation等,当然,也包含这里的主角:权限控制
权限控制
Accompanist的权限控制库为accompanist-permissions,gradle这样引用:
repositories {
mavenCentral()
}
dependencies {
implementation "com.google.accompanist:accompanist-permissions:<version>"
}
使用到的核心API为rememberPermissionState(permission: String)。来看看官方给的基本使用例子:
@Composable
private fun FeatureThatRequiresCameraPermission() {
// Camera permission state
val cameraPermissionState = rememberPermissionState(
android.Manifest.permission.CAMERA
)
when (cameraPermissionState.status) {
// If the camera permission is granted, then show screen with the feature enabled
PermissionStatus.Granted -> {
Text("Camera permission Granted")
}
is PermissionStatus.Denied -> {
Column {
val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
// If the user has denied the permission but the rationale can be shown,
// then gently explain why the app requires this permission
"The camera is important for this app. Please grant the permission."
} else {
// If it's the first time the user lands on this feature, or the user
// doesn't want to be asked again for this permission, explain that the
// permission is required
"Camera permission required for this feature to be available. " +
"Please grant the permission"
}
Text(textToShow)
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Request permission")
}
}
}
}
}
很简单哇?
嗯,对于使用 0.18.0 版本的我,一点不简单 —— cameraPermissionState哪有status字段啊喂???
看来啊,SDK早已更新了多代,但是文档却没跟上步伐。好吧,原谅之,程序员何苦为难程序员…… 咱还是亲自来探究学习吧……
权限控制 plus
rememberPermissionState方法的参数仅一个,即需要申请的权限,并获取到一个PermissionState类型:
interface PermissionState {
/**
* The permission to control and observe.
*/
val permission: String
/**
* When `true`, the user has granted the [permission].
*/
val hasPermission: Boolean
/**
* When `true`, the user should be presented with a rationale.
*/
val shouldShowRationale: Boolean
/**
* When `true`, the [permission] request has been done previously.
*/
val permissionRequested: Boolean
/**
* Request the [permission] to the user.
*
* This should always be triggered from non-composable scope, for example, from a side-effect
* or a non-composable callback. Otherwise, this will result in an IllegalStateException.
*
* This triggers a system dialog that asks the user to grant or revoke the permission.
* Note that this dialog might not appear on the screen if the user doesn't want to be asked
* again or has denied the permission multiple times.
* This behavior varies depending on the Android level API.
*/
fun launchPermissionRequest(): Unit
}
它是一个很简单的接口,四个字段加一个方法:
- permission:权限
- hasPermission:标志是否已获取到权限
- shouldShowRationale:权限申请的理由标志,标志是否需要显示理由描述
- permissionRequested:是否尝试申请过权限
- launchPermissionRequest方法:权限申请调用 —— 相当于Activity.requestPermissions()
据此并对照着老的官方例子,我们可以修改代码如下:
@Composable
private fun FeatureThatRequiresCameraPermission() {
val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
when {
cameraPermissionState.hasPermission -> {
Text("相机已授权")
}
else -> {
Column {
val textToShow = if (cameraPermissionState.shouldShowRationale) {
"欲用相机,必先授权"
} else {
"请授权"
}
Text(textToShow)
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("授权")
}
}
}
}
}
依然简单。首次启动时,因从未申请权限,如下
这时候点击授权Button,将调起系统权限申请弹窗:
我们先拒绝一下试试:
拒绝后,界面马上更新了 —— 因为拒绝了,shouldShowRationale标志变为true,提醒信息即显示出来。这时候再点击授权:
可以看到,权限弹窗已经有区别(测试机是Xiaomi10,没有给询问的checkbox,相当于直接勾选了不再询问),点击“拒绝且不再询问”:
哈,现在和首次进来的时候一样了,但是,再次点击“授权”,是没有任何效果的 —— 因为我们已经完全禁止申请了
源码浅析
rememberPermissionState是调用了rememberMutablePermissionState()方法获取到 PermissionState对象
internal fun rememberMutablePermissionState(
permission: String
): MutablePermissionState {
val context = LocalContext.current
val permissionState = remember(permission) {
MutablePermissionState(permission, context, context.findActivity())
}
// Refresh the permission status when the lifecycle is resumed
PermissionLifecycleCheckerEffect(permissionState)
// Remember RequestPermission launcher and assign it to permissionState
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {
permissionState.hasPermission = it
permissionState.permissionRequested = true
}
DisposableEffect(permissionState, launcher) {
permissionState.launcher = launcher
onDispose {
permissionState.launcher = null
}
}
return permissionState
}
这里赫然出现了rememberLauncherForActivityResult,对,没错,这就是ActivityResultContract的工具方法(这里有介绍),原来就是用它简化权限申请的。申请后结果会更新到state中:
permissionState.hasPermission = it
permissionState.permissionRequested = true
这两位的初始值为:
// 初始化为权限检查值,是一个MutableState对象
private var _hasPermission by mutableStateOf(context.checkPermission(permission))
// 更新时,会同时刷新shouldShowRationale
override var hasPermission: Boolean
internal set(value) {
_hasPermission = value
refreshShouldShowRationale()
}
// permissionRequested是一个MutableState对象
override var permissionRequested: Boolean by mutableStateOf(false)
由以上源码可以有以下结论:
- 因为hasPermission和permissionRequested均是MutableState对象,它们的更新都将触发界面刷新
- 初始状态,permissionRequested固定为false,试图请求权限后,无论是否授权,都更新为true —— 即为单次生命周期标记
- 因为refreshShouldShowRationale的调用,所以一次权限申请,会刷新hasPermission,permissionRequested和shouldShowRationale三个的值,然后界面重刷
有关PermissionState的核心,如此简单
总结
Accompanist工具类将权限申请简化了不少,但是还是无法解决Android权限申请的尴尬和麻烦。如前文例子就能发现这个存在多年的问题:“从未申请权限”和“完全禁止申请”时的各参数状态居然是完全一样的!实际开发过程中想要区分,就得额外加控制,实在不明白这是怎么个设计理念?
另外,还有一次申请多个权限的API rememberMultiplePermissionsState(permissions: List<String>),和单个的基本一样的实现逻辑,有兴趣的自行研究吧