Compose下的权限申请

2,887 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 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("授权")
                }
            }
        }
    }
}

依然简单。首次启动时,因从未申请权限,如下

image.png

这时候点击授权Button,将调起系统权限申请弹窗:

image.png

我们先拒绝一下试试:

image.png

拒绝后,界面马上更新了 —— 因为拒绝了,shouldShowRationale标志变为true,提醒信息即显示出来。这时候再点击授权:

image.png

可以看到,权限弹窗已经有区别(测试机是Xiaomi10,没有给询问的checkbox,相当于直接勾选了不再询问),点击“拒绝且不再询问”:

image.png

哈,现在和首次进来的时候一样了,但是,再次点击“授权”,是没有任何效果的 —— 因为我们已经完全禁止申请了

源码浅析

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)

由以上源码可以有以下结论:

  1. 因为hasPermission和permissionRequested均是MutableState对象,它们的更新都将触发界面刷新
  2. 初始状态,permissionRequested固定为false,试图请求权限后,无论是否授权,都更新为true —— 即为单次生命周期标记
  3. 因为refreshShouldShowRationale的调用,所以一次权限申请,会刷新hasPermission,permissionRequested和shouldShowRationale三个的值,然后界面重刷

有关PermissionState的核心,如此简单

总结

Accompanist工具类将权限申请简化了不少,但是还是无法解决Android权限申请的尴尬和麻烦。如前文例子就能发现这个存在多年的问题:“从未申请权限”和“完全禁止申请”时的各参数状态居然是完全一样的!实际开发过程中想要区分,就得额外加控制,实在不明白这是怎么个设计理念?

另外,还有一次申请多个权限的API rememberMultiplePermissionsState(permissions: List<String>),和单个的基本一样的实现逻辑,有兴趣的自行研究吧