22、Activity Result API:告别startActivityForResult,拥抱新契约

说实话,在Android开发里,页面跳转传回结果这件事,曾经是个老大难。我记得刚入行那会儿,用startActivityForResult配合onActivityResult,代码写得又臭又长。尤其是多个请求混在一起,那个switch-case简直不忍直视。后来Google终于推出了Activity Result API,我第一时间就迁移了。今天咱们就来好好聊聊这个新玩意儿。

为什么说startActivityForResult该退休了?

你想想看,老方案有几个硬伤:

  • 耦合严重:请求代码和结果处理代码散落在Activity各处
  • 类型不安全:requestCode是int,全靠开发者自己维护
  • 生命周期混乱:Fragment和Activity的onActivityResult调用时机不同,容易踩坑
  • 无法编译时检查:写错了requestCode,运行时才崩溃

我在项目中遇到过最离谱的一次,是同事把两个Fragment的requestCode写重复了,结果A页面返回的数据被B页面拦截了。排查了一下午,最后发现是这种低级错误。从那以后,我就下定决心要换掉这套老API。

核心变化:Activity Result API将「启动请求」和「处理结果」绑定在一起,通过ActivityResultContract定义输入输出,彻底解决了上述问题。

Activity Result API基础用法

先看一个最简单的例子:打开相机拍照并获取照片。

// 注册一个结果回调
private val takePictureLauncher = registerForActivityResult(
    ActivityResultContracts.TakePicture()
) { isSuccess: Boolean ->
    if (isSuccess) {
        // 照片已经保存到uri指定的位置
        imageView.setImageURI(photoUri)
    }
}

// 在需要的地方启动
private fun takePhoto() {
    photoUri = FileProvider.getUriForFile(
        this,
        "${packageName}.fileprovider",
        File(cacheDir, "photo_${System.currentTimeMillis()}.jpg")
    )
    takePictureLauncher.launch(photoUri)
}

嗯,这里要注意:registerForActivityResult必须在Activity或Fragment初始化完成之前调用,也就是在onCreateonAttach之后。为什么?因为系统需要保证在配置变更(比如屏幕旋转)后,回调仍然有效。

自定义Contract:掌控输入输出

内置的Contract虽然方便,但很多时候不够用。比如我需要从联系人列表中选择一个,并且返回联系人的姓名和电话。这时候就得自己写Contract了。

class PickContactContract : ActivityResultContract<Void?, ContactInfo>() {

    override fun createIntent(context: Context, input: Void?): Intent {
        return Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI)
    }

    override fun parseResult(resultCode: Int, intent: Intent?): ContactInfo {
        if (resultCode != Activity.RESULT_OK || intent == null) {
            return ContactInfo.EMPTY
        }
        val uri = intent.data ?: return ContactInfo.EMPTY
        val cursor = context.contentResolver.query(uri, null, null, null, null)
        cursor?.use {
            if (it.moveToFirst()) {
                val name = it.getString(it.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME))
                val hasPhone = it.getInt(it.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER))
                if (hasPhone > 0) {
                    val phoneCursor = context.contentResolver.query(
                        ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                        null,
                        "${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?",
                        arrayOf(it.getString(it.getColumnIndex(ContactsContract.Contacts._ID))),
                        null
                    )
                    phoneCursor?.use { pc ->
                        if (pc.moveToFirst()) {
                            val phone = pc.getString(pc.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
                            return ContactInfo(name, phone)
                        }
                    }
                }
                return ContactInfo(name, "")
            }
        }
        return ContactInfo.EMPTY
    }

    override fun getSynchronousResult(context: Context, input: Void?): SynchronousResult<ContactInfo>? {
        // 这里可以返回同步结果,比如已经有缓存数据
        return null
    }
}

data class ContactInfo(val name: String, val phone: String) {
    companion object {
        val EMPTY = ContactInfo("", "")
    }
}

自定义Contract的核心就两个方法:createIntent定义怎么启动,parseResult定义怎么解析结果。我个人习惯把Contract定义成顶层类,方便跨模块复用。

小技巧getSynchronousResult方法可以返回同步结果。比如你已经有缓存数据,就不需要启动Activity了。这个特性在优化用户体验时非常有用。

跨模块的Result协议设计

项目大了以后,模块之间需要互相跳转并传回结果。这时候如果每个模块都自己写Contract,维护起来就是灾难。我建议的做法是:

  1. 定义公共Contract层:在基础模块(比如lib_common)中定义所有跨模块的Contract
  2. 使用接口抽象:把输入输出类型定义成接口,模块间只依赖接口
  3. 统一错误处理:在Contract中定义错误码或异常类型

举个例子,用户模块需要让订单模块选择地址:

// 在lib_common中定义
interface IAddressResult {
    val addressId: String
    val fullAddress: String
    val latitude: Double
    val longitude: Double
}

class SelectAddressContract : ActivityResultContract<Unit, IAddressResult?>() {
    override fun createIntent(context: Context, input: Unit): Intent {
        return Intent("com.example.action.SELECT_ADDRESS")
            .setPackage("com.example.module.order")
    }

    override fun parseResult(resultCode: Int, intent: Intent?): IAddressResult? {
        if (resultCode != Activity.RESULT_OK) return null
        return intent?.getParcelableExtra("address_result") as? IAddressResult
    }
}

这样做的好处是:订单模块可以自由修改地址选择的UI和逻辑,只要保证返回的数据实现了IAddressResult接口就行。我曾经在一个大项目里用这种方式重构了所有跨模块跳转,代码清晰了很多。

与Fragment的集成

Fragment中使用Activity Result API和Activity基本一样,但有几个坑要注意:

  • 生命周期绑定:Fragment的registerForActivityResult绑定的是Fragment的生命周期,不是Activity的
  • 父子Fragment通信:子Fragment可以通过requireParentFragment().registerForActivityResult向父Fragment传递结果
  • 结果传递链:如果FragmentA启动了FragmentB,FragmentB需要返回结果给FragmentA,可以用setFragmentResultsetFragmentResultListener
// FragmentA监听结果
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setFragmentResultListener("request_key") { requestKey, bundle ->
        val result = bundle.getString("result_data")
        // 处理结果
    }
}

// FragmentB发送结果
private fun sendResultBack() {
    setFragmentResult("request_key", bundleOf("result_data" to "hello"))
}

曾经踩过的坑:Fragment的setFragmentResultListener必须在onCreate中注册,否则可能收不到结果。另外,如果Fragment被重建,监听器会自动恢复,但bundle数据不会保留,需要自己处理。

知识体系总览

下面这张图帮你理清Activity Result API的核心脉络:

Activity Result API 核心架构 Activity Result API 基础用法 registerForActivityResult 内置Contract(拍照/选文件等) 自定义Contract createIntent / parseResult getSynchronousResult 跨模块协议 公共Contract层 接口抽象 + 统一错误处理 Fragment集成 setFragmentResult / 父子Fragment通信 核心思想:将「启动请求」与「处理结果」绑定,类型安全,生命周期感知

从这张图可以看出,Activity Result API的核心就是通过Contract将启动和结果处理绑定在一起。无论是基础用法、自定义Contract,还是跨模块协议,都围绕这个核心展开。Fragment的集成则是在此基础上增加了组件间通信的能力。

总结一下:Activity Result API不是简单的API替换,而是一种设计思想的升级。它让代码更清晰、更安全、更易维护。如果你还在用老方案,我建议尽快迁移。刚开始可能会有点不习惯,但用上之后你就回不去了。


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