跳到主要内容Jetpack Activity Results API 详解与源码分析 | 极客日志Kotlin大前端java
Jetpack Activity Results API 详解与源码分析
本文详细介绍了 Android Jetpack 中 Activity Results API 的使用方法与源码原理。文章对比了传统 startActivityForResult 方案的不足,阐述了新 API 如何通过 Launcher、Contract 和 Callback 三要素实现解耦与简化。内容涵盖核心概念、原理深度解析(包括 Activity 与 Fragment 内部机制)、配置变更处理、测试策略及 Kotlin 封装技巧,并提供最佳实践建议,帮助开发者高效迁移旧代码并提升开发体验。
晚风告白2 浏览 Activity Results API 是 Android Jetpack 组件库中用于处理 Activity 启动结果的新机制。随着 AndroidX 的演进,传统的 startActivityForResult 和 onActivityResult 方法已被标记为过时(Deprecated)。这一变化旨在解决 Fragment 生命周期管理中的复杂性,减少样板代码,并实现更好的解耦。
在重构项目 BaseFragment 时,我们常会遇到旧代码中遗留的下划线标记或 Deprecated 提示。查看源码可知,官方推荐使用 ActivityResultContract 配合 registerForActivityResult 来替代旧方案。本文将深入探讨新 API 的使用方式、核心原理以及如何在实际项目中封装应用。
传统方案的痛点
在使用旧方案跳转页面回传数据时,通常需要经历以下步骤:
- 定义 REQUEST_CODE,若同一页面有多个数据请求需避免重复;
- 调用
startActivityForResult(Intent, REQUEST_CODE);
- 重写
onActivityResult(),判断 requestCode 和 resultCode,拿到值后执行后续逻辑。
- 代码冗余:每个 Activity 都需要维护一堆 REQUEST_CODE 常量;
- 类型不安全:requestCode 是整数,容易混淆;
- 生命周期耦合:Fragment 中处理 onActivityResult 需要依赖宿主 Activity,容易导致内存泄漏或状态丢失。
新方案的优势
使用 Activity Results API,只需定义一个函数,然后 launch() 一下即可。它移除了对 onActivityResult 的重写需求,也无需手动管理 REQUEST_CODE。技术迭代飞快,建议读者使用时以官方文档为准,目前较新的 activity 和 fragment 包已内置相关功能,无需额外依赖 activity-ktx 和 fragment-ktx。
核心概念与使用
Activity Results API 的使用非常简单,它由三个核心要素组成:启动器 (Launcher) + 协定 (Contract) + 结果回调 (Callback)。
1. ActivityResultLauncher → 启动器
registerForActivityResult() 的返回值即为启动器对象,用于承载启动对象与返回对象。它负责将契约转换为 Intent 并发起跳转,同时监听结果回调。
2. ActivityResultContract → 协定/契约
这是第一个入参,协定的是所需的输入类型和结果的输出类型。ActivityResultContracts 类提供了一些常用的协定,开发者可以直接拿来即用。
- StartActivityForResult:通用协定,不做任何转换,Intent 作为输入,ActivityResult 作为输出;
- RequestMultiplePermissions:请求一组权限;
- RequestPermission:请求单个权限;
- TakePicturePreview:调用 MediaStore.ACTION_IMAGE_CAPTURE 拍照,返回 Bitmap 图片;
- TakePicture:调用 MediaStore.ACTION_IMAGE_CAPTURE 拍照,并将图片保存在给定 Uri,返回 true 表示保存成功;
- TakeVideo:调用 MediaStore.ACTION_VIDEO_CAPTURE 录制,并将视频保存在给定 Uri,返回 true 表示保存成功;
- PickContact:调用通讯录 APP 获取联系人;
- GetContent:提示选择一条内容,返回一个通过 ContentResolver#openInputStream(Uri) 访问原生数据的 Uri 地址;
- OpenDocument:提示用户选择文档,返回 Uri;
- CreateDocument:提示用户选择创建新文档的路径,返回已创建项目的 Uri。
如果需要自定义行为,可以继承 ActivityResultContract<I, O> 并重写关键方法。例如,自定义一个铃声选择器:
class PickRingtone : ActivityResultContract<Int, Uri?>() {
override fun createIntent(context: Context, ringtoneType: Int) =
Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, ringtoneType)
}
override fun parseResult(resultCode: Int, result: Intent?): Uri? {
if (resultCode != Activity.RESULT_OK) {
return null
}
return result?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
}
}
3. ActivityResultCallback → 结果回调
第二个入参,见名知意:启动 Activity 并返回当前 Activity 时的结果回调。Activity Result API 体现了模版方法模式的封装思想,开发者按需注入协定类型和结果回调即可,无需关注底层细节。
原理深度解析
在 Activity、Fragment 中可以直接使用 registerForActivityResult(),是因为 ComponentActivity 和 Fragment 都实现了 ActivityResultCaller 接口。
1. Activity 内部机制
跟下 ComponentActivity#registerForActivityResult() 可以发现,第一个参数构造了一个 key,规则为 activity_rq# + 一个自增的 AtomicInteger 值。这就是为什么不需要另外定义 REQUEST_CODE 就能进行区分的原因。
继续跟 ActivityResultRegistry#register(),其核心逻辑是添加了一个观察者,当生命周期组件状态切换到 ON_START 时执行回调。随后返回一个 ActivityResultLauncher 实例。
ComponentActivity 内部初始化了一个 ActivityResultRegistry 实例,并重写了 onLaunch();
- 开发者调用
registerForActivityResult() 最终调用 ActivityResultRegistry.register(),在此添加了一个观察者,当生命周期状态切换到 ON_START 时,执行协定 Contract.parseResult() 生成输出内容,并把结果作为参数传入回调 callback.onActivityResult() 中;
- 注意!②是要生命周期发生改变才会触发的,开发者要调用
ActivityResultLauncher.launch() 才会发起跳转,其中回调了 onLaunch() 方法,在此调用了协定 Contract.createIntent() 返回一个和 startActivityForResult() 搭配使用的 Intent 实例;
- 跳转目标 Activity 后返回此页面,生命周期发生改变,然后回调②中的相关代码。
2. Fragment 内部机制
Fragment 的实现思路也很简单,想办法拿到宿主 Activity 中的 ActivityResultRegistry 实例,调它的 register() 拿到返回的 ActivityResultLauncher 实例引用。最后返回新的 ActivityResultLauncher 实例,在 launch() 中调用前面那个 Activity 的 ActivityResultLauncher 实例引用的 launch() 方法。这实际上是一种委托代理模式。
这里有一个小细节,生命周期组件传入的是 Fragment.this,所以不用担心 Fragment 销毁没解绑导致的内存泄露问题。
3. 非 Activity/Fragment 接收 Activity 结果
如果需要在非 Activity/Fragment 的类中处理 Activity 结果,可以实现一个 LifecycleObserver 用于处理协定的注册和启动器的启动。通过绑定到特定的 LifecycleOwner,确保回调在正确的生命周期节点触发。
4. 配置改变引起 Activity 重建的处理
在 ActivityResultRegistry 中还发现了针对配置更改导致重建的场景处理。系统保存了 key(requestCode) 相关的数据、处理结果、Random 随机数实例。requestCode 和 Result 得以保留,Activity 重建后,再把它们分发给新注册的 Callback,避免了数据的丢失。这对于处理屏幕旋转等场景至关重要。
5. 测试 Activity 结果调用
默认情况下,registerForActivityResult() 会自动使用 Activity 提供的 ActivityResultRegistry,而它还提供了一个重载,支持传入自己的 ActivityResultRegistry 实例。这主要用于拦截结果调用进行测试,不会另外启动另一个 Activity。在单元测试中,可以通过注入 Mock 的 Registry 来验证业务逻辑是否正确响应了回调。
常见封装策略
虽然 Activity Results API 已经简单易用,但对于喜欢精简代码的开发者来说,还可以利用 Kotlin 相关的语法特性进一步封装。
扩展方法封装
最简单的封装就是写几个扩展方法,从 ActivityResultLauncher 生成和 launch() 调用处入手:
fun ComponentActivity.registerActResult(callback: ActivityResultCallback<ActivityResult>) =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
callback.onActivityResult(it) }
fun Fragment.registerActResult(callback: ActivityResultCallback<ActivityResult>) =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
callback.onActivityResult(it) }
fun Intent.launch(launcher: ActivityResultLauncher<Intent>) {
launcher.launch(this)
}
private val mLauncher = registerActResult {
}
Intent(this, SecondActivity::class.java).launch(mLauncher)
基于 Caller 的优化
还可以优化成基于 ActivityResultCaller 进行扩展,然后把常用的一些跳转,如权限、打开相机、录像等写成一个个扩展函数,用的时候直接调用即可。如果想代码写得更少更优雅,可以结合生命周期回调,各种简化调用的扩展,甚至弄成 DSL 调用等。
最佳实践与注意事项
1. 注册时机
registerForActivityResult() 必须在 ON_START 之前注册,通常在 onCreate() 时初始化。如果在 OnCreate() 之后才初始化可能会报错,因为此时生命周期状态可能已经越过临界点。
2. 内存泄漏防护
由于启动器持有对 Contract 和 Callback 的引用,务必确保 Callback 不会持有 Activity 或 Fragment 的强引用。建议使用弱引用或者在 onDestroy 时取消注册(虽然框架通常会自动处理)。
3. 线程安全
ActivityResultRegistry 内部使用了锁机制来保证线程安全,但在主线程外调用 launch() 可能会导致 UI 更新异常,建议在主线程中发起跳转。
4. 错误处理
在 parseResult 中应妥善处理 RESULT_CANCELED 或其他非 OK 状态,避免空指针异常。对于文件选择等操作,需检查 Uri 是否有效。
小结
借着重构 BaseFragment 的机缘巧合,深入了解了 Activity Results API 的用法,阅读源码了解到背后的实现原理,并尝试进行了封装。心里有底了,赶紧在重构项目的时候安排上!!!
该 API 不仅简化了代码结构,还增强了类型安全性,解决了 Fragment 生命周期管理的诸多痛点。理解其底层原理有助于在复杂场景下更好地调试和优化应用性能。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online