10、权限与Jetpack:ActivityResult API、RegisterForActivityResult、权限启动器

说实话,Android 的运行时权限从 Marshmallow 开始,折腾了好几个版本。早期我们用 onRequestPermissionsResult,那代码写得我头皮发麻——回调里套回调,Activity 和 Fragment 还得各自处理一遍。后来 Jetpack 出了 ActivityResult API,我才觉得这事儿终于清爽了。

今天我们就聊聊这套 API。说白了,它就是官方帮我们把权限请求的「启动」和「结果回调」封装成了一个统一的模式。你不再需要手动覆写 onActivityResultonRequestPermissionsResult,而是注册一个 Launcher,然后调用 launch() 就行。

10.1 为什么需要 ActivityResult API?

我记得刚做权限适配那会儿,项目里同时有 Activity 和 Fragment,每个页面都得写一遍回调。更坑的是,如果权限被用户永久拒绝,你还得手动判断 shouldShowRequestPermissionRationale。代码量翻倍不说,逻辑还容易漏。

ActivityResult API 解决了三个核心痛点:

  • 解耦:启动和结果处理绑定在一起,不再依赖 Activity 的生命周期回调。
  • 统一:Activity、Fragment、甚至自定义组件都能用同一套 API。
  • 安全:自动处理配置变更(比如屏幕旋转),不会丢结果。

核心思想:把「我要做什么」和「做完后怎么办」写在一起,而不是分散在多个方法里。

10.2 RegisterForActivityResult 基础用法

先看一个最简单的例子。假设我们要请求 CAMERA 权限:

// 在 Activity 或 Fragment 的初始化阶段注册
private val requestCameraLauncher =
    registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
        if (isGranted) {
            // 权限已授予,打开相机
            openCamera()
        } else {
            // 权限被拒绝
            showPermissionDeniedDialog()
        }
    }

// 在需要请求权限的地方调用
fun onClickCamera() {
    requestCameraLauncher.launch(Manifest.permission.CAMERA)
}

嗯,这里要注意:registerForActivityResult 必须在 onCreate 或 Fragment 的 onAttach 之前调用。为什么?因为 Jetpack 需要保证 Launcher 在生命周期重建时能恢复。我早期踩过这个坑,在按钮点击时才注册,结果屏幕旋转后回调就丢了。

10.3 权限启动器:RequestMultiplePermissions

实际项目中,我们经常需要同时请求多个权限。比如拍照+存储,或者定位+后台定位。这时候用 RequestMultiplePermissions

private val multiPermissionLauncher =
    registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
        // permissions 是一个 Map<String, Boolean>
        val allGranted = permissions.values.all { it }
        if (allGranted) {
            startLocationService()
        } else {
            // 找出被拒绝的权限
            val deniedList = permissions.filter { !it.value }.keys
            showDeniedRationale(deniedList)
        }
    }

// 启动时传入多个权限
multiPermissionLauncher.launch(
    arrayOf(
        Manifest.permission.ACCESS_FINE_LOCATION,
        Manifest.permission.ACCESS_BACKGROUND_LOCATION
    )
)

我个人习惯把权限启动器封装成一个工具类。比如这样:

class PermissionHelper(private val fragment: Fragment) {

    private val launcher = fragment.registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { result ->
        callback?.invoke(result)
    }

    private var callback: ((Map<String, Boolean>) -> Unit)? = null

    fun request(permissions: Array<String>, onResult: (Map<String, Boolean>) -> Unit) {
        callback = onResult
        launcher.launch(permissions)
    }
}

这样 Fragment 里的代码就干净多了。你想想看,每个页面只需要一行 permissionHelper.request(...) 就搞定。

10.4 处理「不再询问」的情况

这是权限控制里最头疼的部分。用户勾选了「不再询问」后,你再调用 launch() 也不会弹出系统对话框,直接返回拒绝。

我曾经在一个社交 App 里遇到过:用户拒绝相机权限并勾了「不再询问」,结果每次点拍照按钮都静默失败,用户以为 App 坏了。后来我加了一个前置检查:

fun checkAndRequestCamera() {
    when {
        // 权限已授予
        ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) ==
            PackageManager.PERMISSION_GRANTED -> {
            openCamera()
        }
        // 需要显示 rationale(用户之前拒绝过,但没勾「不再询问」)
        shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
            showRationaleDialog {
                requestCameraLauncher.launch(Manifest.permission.CAMERA)
            }
        }
        // 已被永久拒绝
        else -> {
            showSettingsDialog() // 引导用户去设置页手动开启
        }
    }
}

这里的关键是 shouldShowRequestPermissionRationale。如果返回 true,说明用户之前拒绝过,但还有机会再弹一次。如果返回 false,那就是被永久拒绝了,你得引导用户去系统设置页。

注意shouldShowRequestPermissionRationale 在第一次请求前也会返回 false。所以判断逻辑一定要按「已授予 → 可再次提示 → 永久拒绝」的顺序来。

10.5 与旧版 API 的对比

我整理了一个表格,方便你对比新旧两种方式:

对比项 旧版 onRequestPermissionsResult 新版 ActivityResult API
注册方式 覆写 Activity/Fragment 方法 registerForActivityResult 注册 Launcher
结果回调 通过 requestCode 区分 Lambda 直接绑定
配置变更 结果丢失,需手动保存 自动恢复
多权限请求 需自行合并结果 返回 Map<String, Boolean>
代码可读性 差,回调分散 好,逻辑集中

说白了,旧版 API 是「面向过程」的,你得自己管理状态。新版 API 是「声明式」的,你只管说「我要什么权限,结果怎么处理」,剩下的框架帮你搞定。

10.6 知识体系图

下面这张 SVG 图展示了 ActivityResult API 在权限控制中的完整流程:

ActivityResult API 权限请求流程 注册 Launcher 检查权限状态 已授予 → 执行操作 未授予 → 调用 launch() 永久拒绝 → 引导设置 结果回调 Lambda

10.7 最佳实践总结

最后,我把自己在多个项目里沉淀下来的几条经验列出来:

  • 尽早注册registerForActivityResult 放在 onCreateonAttach 里,别放在点击事件中。
  • 封装复用:把权限启动器封装成工具类或 ViewModel,避免每个页面重复写注册逻辑。
  • 区分拒绝类型:用 shouldShowRequestPermissionRationale 判断是「可再次提示」还是「永久拒绝」,给用户不同的反馈。
  • 不要滥用:只在真正需要的时候请求权限。比如用户没点拍照按钮,就别提前弹相机权限。
  • 测试覆盖:用模拟器测试「授予」「拒绝」「永久拒绝」三种情况,特别是配置变更后的行为。

小技巧:如果你用 Fragment 作为宿主,记得在 onDestroy 里清理 Launcher 的引用,避免内存泄漏。虽然 Jetpack 内部做了处理,但养成好习惯总没错。

好了,关于 ActivityResult API 和权限启动器,就聊这么多。这套 API 其实不限于权限,它还能处理拍照、选文件、打开设置页面等场景。你只要记住一个核心模式:注册 Launcher → 调用 launch() → 在 Lambda 里处理结果。剩下的,交给 Jetpack 就好。

公众号:蓝海资料掘金营,微信deep3321