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初始化完成之前调用,也就是在onCreate或onAttach之后。为什么?因为系统需要保证在配置变更(比如屏幕旋转)后,回调仍然有效。
自定义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,维护起来就是灾难。我建议的做法是:
- 定义公共Contract层:在基础模块(比如lib_common)中定义所有跨模块的Contract
- 使用接口抽象:把输入输出类型定义成接口,模块间只依赖接口
- 统一错误处理:在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,可以用
setFragmentResult和setFragmentResultListener
// 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的核心就是通过Contract将启动和结果处理绑定在一起。无论是基础用法、自定义Contract,还是跨模块协议,都围绕这个核心展开。Fragment的集成则是在此基础上增加了组件间通信的能力。
总结一下:Activity Result API不是简单的API替换,而是一种设计思想的升级。它让代码更清晰、更安全、更易维护。如果你还在用老方案,我建议尽快迁移。刚开始可能会有点不习惯,但用上之后你就回不去了。
公众号:蓝海资料掘金营,微信deep3321