10、权限与Jetpack:ActivityResult API、RegisterForActivityResult、权限启动器
说实话,Android 的运行时权限从 Marshmallow 开始,折腾了好几个版本。早期我们用 onRequestPermissionsResult,那代码写得我头皮发麻——回调里套回调,Activity 和 Fragment 还得各自处理一遍。后来 Jetpack 出了 ActivityResult API,我才觉得这事儿终于清爽了。
今天我们就聊聊这套 API。说白了,它就是官方帮我们把权限请求的「启动」和「结果回调」封装成了一个统一的模式。你不再需要手动覆写 onActivityResult 或 onRequestPermissionsResult,而是注册一个 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 在权限控制中的完整流程:
10.7 最佳实践总结
最后,我把自己在多个项目里沉淀下来的几条经验列出来:
- 尽早注册:
registerForActivityResult放在onCreate或onAttach里,别放在点击事件中。 - 封装复用:把权限启动器封装成工具类或 ViewModel,避免每个页面重复写注册逻辑。
- 区分拒绝类型:用
shouldShowRequestPermissionRationale判断是「可再次提示」还是「永久拒绝」,给用户不同的反馈。 - 不要滥用:只在真正需要的时候请求权限。比如用户没点拍照按钮,就别提前弹相机权限。
- 测试覆盖:用模拟器测试「授予」「拒绝」「永久拒绝」三种情况,特别是配置变更后的行为。
小技巧:如果你用 Fragment 作为宿主,记得在 onDestroy 里清理 Launcher 的引用,避免内存泄漏。虽然 Jetpack 内部做了处理,但养成好习惯总没错。
好了,关于 ActivityResult API 和权限启动器,就聊这么多。这套 API 其实不限于权限,它还能处理拍照、选文件、打开设置页面等场景。你只要记住一个核心模式:注册 Launcher → 调用 launch() → 在 Lambda 里处理结果。剩下的,交给 Jetpack 就好。