20、Activity Result API:告别startActivityForResult,拥抱新协议
说实话,startActivityForResult 这个老伙计,陪伴了我们 Android 开发者很多年。我记得刚入行那会儿,几乎每个需要返回数据的页面都得用它。但用久了,痛点就出来了——代码耦合严重、回调满天飞、处理结果还得在 onActivityResult 里写一堆 if-else。嗯,我自己就曾在项目里见过一个 onActivityResult 方法超过 200 行的“奇观”。
好在 Jetpack 带来了 Activity Result API,彻底解决了这些历史遗留问题。今天我们就来聊聊它怎么用,以及如何用它替代自定义协议。
为什么非要换掉 startActivityForResult?
你想想看,startActivityForResult 最大的问题是什么?
- 耦合严重:发起方和接收方必须约定好 requestCode 和 resultCode,一旦写错,数据就丢了。
- 生命周期不安全:如果 Activity 被销毁重建,
onActivityResult可能不会按预期调用。 - 代码分散:启动逻辑在 A 处,处理逻辑在 B 处,维护起来很痛苦。
我在一个旧项目重构时,就踩过这个坑。当时有个拍照返回头像的功能,因为 requestCode 冲突,导致两个模块互相覆盖结果,排查了整整一个下午。从那以后,我下定决心要换掉它。
Activity Result API 的核心用法
Activity Result API 的核心思想很简单:把“启动”和“结果处理”绑定在一起。你不再需要全局的 requestCode,也不需要重写 onActivityResult。
1. 注册一个结果回调
在 Activity 或 Fragment 初始化时,用 registerForActivityResult 注册一个回调。它接收两个参数:一个 contract(协议),一个 callback(回调)。
// 在 Activity 中
private val pickContactLauncher = registerForActivityResult(
ActivityResultContracts.PickContact()
) { uri: Uri? ->
// 处理返回的数据
uri?.let { showContactInfo(it) }
}
你看,代码变得非常紧凑。启动和结果处理写在一起,一目了然。
2. 启动目标 Activity
需要启动时,直接调用 launch() 方法即可:
// 点击按钮时启动
button.setOnClickListener {
pickContactLauncher.launch(null) // 不需要传 Intent 时传 null
}
不需要再拼 Intent、不需要传 requestCode。干净利落。
3. 内置的 Contracts
Jetpack 提供了很多现成的 ActivityResultContract,覆盖了常见场景:
| Contract 名称 | 用途 |
|---|---|
StartActivityForResult |
通用启动,返回 Intent 数据 |
TakePicture |
拍照并返回图片 Uri |
RequestPermission |
请求单个权限 |
RequestMultiplePermissions |
请求多个权限 |
PickContact |
选择联系人 |
OpenDocument |
打开文档 |
我个人习惯优先使用这些内置 Contract,因为它们已经处理好了各种边界情况。比如 TakePicture 会自动处理 FileProvider 的 Uri 授权,省心不少。
自定义协议:打造你自己的 Contract
内置 Contract 虽然方便,但总有不够用的时候。比如你需要启动一个自定义的拍照界面,或者一个复杂的表单填写页面。这时候就需要自定义 ActivityResultContract 了。
说白了,自定义 Contract 就是定义好“输入”和“输出”的格式。我举个例子:
class CustomFormContract : ActivityResultContract<String, String?>() {
override fun createIntent(context: Context, input: String): Intent {
// input 是传入的参数,比如表单标题
return Intent(context, FormActivity::class.java).apply {
putExtra("title", input)
}
}
override fun parseResult(resultCode: Int, intent: Intent?): String? {
// 解析返回的结果
return when (resultCode) {
Activity.RESULT_OK -> intent?.getStringExtra("result_data")
else -> null
}
}
}
然后注册时直接使用:
private val formLauncher = registerForActivityResult(CustomFormContract()) { result ->
result?.let { showResult(it) }
}
// 启动
formLauncher.launch("填写用户信息")
这样做的好处是:调用方完全不需要知道 FormActivity 内部怎么传 Intent。所有协议细节都封装在 Contract 里。我曾经在一个多团队协作的项目中推广这种做法,不同模块之间只需要约定好 Contract 的输入输出类型,就能无缝对接,再也不用互相看对方的 Intent 代码了。
核心思想:自定义 Contract 就是定义了一个“协议接口”。它让 Activity 之间的通信变得像函数调用一样清晰。
避坑指南:我曾经踩过的坑
虽然 Activity Result API 很优雅,但也不是完全没有坑。我分享几个亲身经历:
- 不要在 Fragment 的 onCreate 之前注册:
registerForActivityResult必须在 Fragment 的onAttach之后调用,否则会抛异常。我建议在onCreate或onViewCreated中注册。 - 生命周期感知:注册的 Launcher 会自动跟随宿主生命周期。如果宿主被销毁,Launcher 也会失效。所以不要在静态变量或全局单例中持有 Launcher。
- 结果回调可能延迟:如果目标 Activity 因为配置变更(如屏幕旋转)重建,结果回调会在重建完成后才触发。这是正常行为,不要惊慌。
注意:如果你在 Fragment 中使用 Activity Result API,请确保 Fragment 的 lifecycle 状态至少为 STARTED。否则 launch() 调用会被忽略。
流程图:Activity Result API 的工作机制
下面这张图展示了从注册到启动再到结果返回的完整流程。我特意画得比较简洁,方便你理解核心逻辑。
总结
Activity Result API 不是简单的语法糖,它从根本上解决了 startActivityForResult 的架构问题。我个人建议:新项目一律使用 Activity Result API,老项目逐步迁移。迁移成本其实很低,因为你可以用 StartActivityForResult 这个通用 Contract 先过渡,再慢慢拆成自定义 Contract。
嗯,最后说一句:自定义 Contract 是真正的“协议”思维。它让 Activity 之间的通信变得像接口调用一样规范。如果你正在设计一个多模块的 App,强烈推荐用这种方式来定义模块间的跳转协议。
小技巧:如果你需要兼容旧版本的 onActivityResult,可以同时保留两种方式。但记得不要混用同一个 requestCode,否则会冲突。
公众号:蓝海资料掘金营,微信deep3321