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 之后调用,否则会抛异常。我建议在 onCreateonViewCreated 中注册。
  • 生命周期感知:注册的 Launcher 会自动跟随宿主生命周期。如果宿主被销毁,Launcher 也会失效。所以不要在静态变量或全局单例中持有 Launcher。
  • 结果回调可能延迟:如果目标 Activity 因为配置变更(如屏幕旋转)重建,结果回调会在重建完成后才触发。这是正常行为,不要惊慌。

注意:如果你在 Fragment 中使用 Activity Result API,请确保 Fragment 的 lifecycle 状态至少为 STARTED。否则 launch() 调用会被忽略。

流程图:Activity Result API 的工作机制

下面这张图展示了从注册到启动再到结果返回的完整流程。我特意画得比较简洁,方便你理解核心逻辑。

Activity Result API 工作流程 1. 注册 Launcher registerForActivityResult 2. 调用 launch() 传入输入参数 3. 启动目标 createIntent() 4. 用户操作 setResult() 5. 解析结果 parseResult() 6. 回调执行 callback.onResult() 整个流程完全解耦,调用方只需关心输入输出类型

总结

Activity Result API 不是简单的语法糖,它从根本上解决了 startActivityForResult 的架构问题。我个人建议:新项目一律使用 Activity Result API,老项目逐步迁移。迁移成本其实很低,因为你可以用 StartActivityForResult 这个通用 Contract 先过渡,再慢慢拆成自定义 Contract。

嗯,最后说一句:自定义 Contract 是真正的“协议”思维。它让 Activity 之间的通信变得像接口调用一样规范。如果你正在设计一个多模块的 App,强烈推荐用这种方式来定义模块间的跳转协议。

小技巧:如果你需要兼容旧版本的 onActivityResult,可以同时保留两种方式。但记得不要混用同一个 requestCode,否则会冲突。


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