[Android 从零到一] ActivityResult API 实战:替代 onActivityResult 的现代回调设计
摘要:ActivityResult API 把页面跳转、权限申请、拍照选图等结果回调从集中式的 requestCode 分发,改成生命周期感知、类型更明确的注册式回调。本文从传统 onActivityResult 的痛点讲起,逐步拆解 ActivityResultContract、registerForActivityResult、权限申请、拍照选图和组件封装的工程实践,帮助你在真实项目中把结果处理写得更清晰、更稳定。
在 Android 开发里,页面 A 打开页面 B 并拿回结果,是非常常见的需求。比如编辑资料后返回用户昵称,打开系统相册后拿到图片 Uri,申请权限后决定是否继续执行任务。早期项目通常会使用 startActivityForResult()、onActivityResult() 和 requestCode 来完成这些逻辑。
这套写法能用,但在项目变大以后会越来越难维护:回调集中在 Activity 里,多个业务共用同一个 onActivityResult(),requestCode 容易冲突,结果类型也不够清晰。Jetpack 提供的 ActivityResult API 就是为了解决这些问题。它把“发起动作”和“接收结果”绑定在一起,并且和生命周期协作,避免很多传统写法里的隐性问题。
传统写法的问题在哪里
先看一个典型的旧写法:
private val REQ_EDIT_PROFILE = 1001
fun openEditProfile() {
val intent = Intent(this, EditProfileActivity::class.java)
startActivityForResult(intent, REQ_EDIT_PROFILE)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQ_EDIT_PROFILE && resultCode == RESULT_OK) {
val name = data?.getStringExtra("name") ?: return
renderName(name)
}
}
这段代码的问题不在于语法复杂,而在于职责被拉散了。打开页面的逻辑在一个地方,处理结果的逻辑在另一个地方,中间靠一个整数常量关联。业务少的时候还能接受,一旦页面里同时有编辑资料、选择头像、拍照、权限申请、文件选择等逻辑,onActivityResult() 很快会变成一个大分发器。
更麻烦的是,requestCode 本身没有类型约束。你需要靠命名和约定保证它不会冲突,也需要手动判断 resultCode、解析 Intent、处理空值。代码越多,漏判和误判的概率越高。
ActivityResult API 的基本模型
ActivityResult API 的核心是两个概念:
ActivityResultContract<I, O>:描述输入类型和输出类型ActivityResultLauncher<I>:负责发起动作
其中 I 是输入类型,O 是结果类型。比如打开一个 Activity 并拿回 ActivityResult,输入是 Intent,输出是 ActivityResult;申请单个权限,输入是权限字符串,输出是布尔值。
最常见的写法如下:
private val editProfileLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
val name = result.data?.getStringExtra("name") ?: return@registerForActivityResult
renderName(name)
}
}
fun openEditProfile() {
val intent = Intent(this, EditProfileActivity::class.java)
editProfileLauncher.launch(intent)
}
相比旧写法,结果处理直接和 launcher 绑定,不再需要全局 requestCode。你看到 editProfileLauncher,就能知道它发起什么动作、在哪里处理结果。维护成本会明显下降。
需要注意的是,registerForActivityResult() 应该在组件初始化阶段调用,比如 Activity 或 Fragment 的成员变量初始化、onCreate() 里。不要等到点击按钮时才注册。点击时只调用 launch()。
为什么它更适合真实项目
ActivityResult API 不只是换了一个写法,它真正改善的是工程边界。
首先,它减少了集中式分发。不同业务可以拥有自己的 launcher,不再把所有结果塞进同一个回调里。业务之间的耦合降低,删除或迁移某个功能时也更容易。
其次,它让输入输出更明确。权限申请返回 Boolean,多权限申请返回 Map<String, Boolean>,选择内容返回 Uri?。你不需要每次都从原始 Intent 里猜结果结构。
再次,它对生命周期更友好。launcher 会和 ActivityResultRegistry 协作,在组件生命周期内保存和分发结果。对于屏幕旋转、进程重建等情况,它比手写 requestCode 分发更可靠。
申请权限:从回调地狱到局部处理
运行时权限是 ActivityResult API 最容易落地的场景之一。旧项目里权限请求常常散落在 requestPermissions() 和 onRequestPermissionsResult() 中。现在可以这样写:
private val cameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) {
openCamera()
} else {
showPermissionDeniedMessage()
}
}
fun requestCameraPermission() {
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
这里的结果就是一个 Boolean,语义非常直接。你不需要再解析权限数组,也不需要用 requestCode 找回业务上下文。
如果要申请多个权限,可以使用 RequestMultiplePermissions():
private val mediaPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { result ->
val cameraGranted = result[Manifest.permission.CAMERA] == true
val audioGranted = result[Manifest.permission.RECORD_AUDIO] == true
if (cameraGranted && audioGranted) {
startVideoRecord()
} else {
showPermissionGuide()
}
}
fun requestMediaPermissions() {
mediaPermissionLauncher.launch(
arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
)
)
}
实际项目里不要只判断授权结果,还要结合业务场景设计降级路径。比如拍照权限被拒绝时,可以允许用户从相册选择图片;定位权限被拒绝时,可以让用户手动选择城市。
选择图片:用系统能力减少兼容成本
选择图片也很适合用 ActivityResult API。对于普通内容选择,可以使用 GetContent():
private val pickImageLauncher = registerForActivityResult(
ActivityResultContracts.GetContent()
) { uri: Uri? ->
uri ?: return@registerForActivityResult
previewAvatar(uri)
}
fun pickImage() {
pickImageLauncher.launch("image/*")
}
这段代码的输入是 MIME 类型,输出是 Uri?。业务代码可以直接围绕 Uri 处理,不需要关心系统选择器背后的 Activity 细节。
在 Android 13 及以上,如果你的目标是选择图片或视频,也可以考虑 Photo Picker 对应的合约。它能减少存储权限依赖,让用户只授权被选择的媒体资源。对于头像、聊天图片、内容发布等场景,这是更符合现代隐私设计的方式。
拍照:处理 Uri 比处理 Bitmap 更可靠
拍照场景里,很多旧代码会尝试从返回结果中拿缩略图 Bitmap。这个做法简单,但不适合正式业务,因为缩略图质量有限,也容易遇到内存和兼容问题。更稳妥的方式是提前创建一个文件 Uri,然后让相机把图片写入这个位置。
private var pendingPhotoUri: Uri? = null
private val takePictureLauncher = registerForActivityResult(
ActivityResultContracts.TakePicture()
) { success ->
val uri = pendingPhotoUri
if (success && uri != null) {
showPhoto(uri)
} else {
clearPendingPhoto()
}
}
fun takePhoto() {
val uri = createImageUri()
pendingPhotoUri = uri
takePictureLauncher.launch(uri)
}
TakePicture() 的输入是要写入的 Uri,输出是是否拍摄成功。这里的关键点是:Uri 的创建、权限授权、失败清理都要纳入流程设计。不要只写成功路径,否则线上很容易出现空文件、重复文件或页面状态错乱。
如果使用 FileProvider 生成 Uri,记得配置 provider_paths,并确认相机应用能够写入目标位置。拍照完成后再把 Uri 交给裁剪、上传或预览模块。
在 Fragment 中使用时要注意什么
Fragment 里使用 ActivityResult API 很自然,但有两个细节需要注意。
首先,launcher 应该作为 Fragment 的成员注册,不要在点击事件里临时注册:
class ProfileFragment : Fragment(R.layout.fragment_profile) {
private val pickAvatarLauncher = registerForActivityResult(
ActivityResultContracts.GetContent()
) { uri ->
uri ?: return@registerForActivityResult
viewModel.updateAvatar(uri)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<View>(R.id.avatar).setOnClickListener {
pickAvatarLauncher.launch("image/*")
}
}
}
其次,回调里不要直接假设 View 一定存在。结果返回时,Fragment 的 View 生命周期可能已经变化。更稳的做法是把结果交给 ViewModel 或状态流,再由 UI 层根据当前生命周期渲染。
private val pickAvatarLauncher = registerForActivityResult(
ActivityResultContracts.GetContent()
) { uri ->
uri ?: return@registerForActivityResult
viewModel.onAvatarSelected(uri)
}
这样即使页面重建,也能通过 ViewModel 保留业务状态,而不是把结果强行写进已经失效的 View。
自定义 Contract:让业务输入输出更干净
内置合约覆盖了很多常见场景,但真实项目里经常需要更贴近业务的输入输出。比如打开用户编辑页,只关心返回的用户资料,而不是原始 Intent。这个时候可以自定义 ActivityResultContract。
data class EditProfileInput(val userId: String)
data class EditProfileResult(val name: String, val avatar: Uri?)
class EditProfileContract : ActivityResultContract<EditProfileInput, EditProfileResult?>() {
override fun createIntent(context: Context, input: EditProfileInput): Intent {
return Intent(context, EditProfileActivity::class.java)
.putExtra("user_id", input.userId)
}
override fun parseResult(resultCode: Int, intent: Intent?): EditProfileResult? {
if (resultCode != Activity.RESULT_OK || intent == null) return null
val name = intent.getStringExtra("name") ?: return null
val avatar = intent.getParcelableExtra<Uri>("avatar")
return EditProfileResult(name, avatar)
}
}
使用时业务代码会更清晰:
private val editProfileLauncher = registerForActivityResult(EditProfileContract()) { result ->
result ?: return@registerForActivityResult
viewModel.updateProfile(result.name, result.avatar)
}
fun editProfile(userId: String) {
editProfileLauncher.launch(EditProfileInput(userId))
}
自定义 Contract 的好处是把 Intent 拼装和结果解析封装起来。调用方只面对业务对象,不再关心 extra key、resultCode 和空值细节。
和 ViewModel 怎么配合
ActivityResultLauncher 依赖 Activity 或 Fragment 的生命周期,不应该直接放进 ViewModel。ViewModel 不应该持有 launcher,也不应该知道页面是通过哪个系统合约拿结果。
更推荐的边界是:
- Activity 或 Fragment 负责注册 launcher 和调用系统能力
- ViewModel 负责接收结果并更新业务状态
- UI 观察状态变化并渲染页面
比如选择头像:
class ProfileViewModel : ViewModel() {
private val _uiState = MutableStateFlow(ProfileUiState())
val uiState: StateFlow<ProfileUiState> = _uiState
fun onAvatarSelected(uri: Uri) {
_uiState.update { it.copy(avatarUri = uri, uploadState = UploadState.Waiting) }
}
}
Fragment 只负责把结果传给 ViewModel:
private val pickAvatarLauncher = registerForActivityResult(
ActivityResultContracts.GetContent()
) { uri ->
uri ?: return@registerForActivityResult
viewModel.onAvatarSelected(uri)
}
这样做的价值是边界清楚。系统交互留在 UI 层,业务状态留在 ViewModel。测试 ViewModel 时也不需要模拟 ActivityResultLauncher。
常见踩坑
在点击事件里注册 launcher
这是很常见的错误。launcher 注册应该发生在组件初始化阶段,点击时只负责 launch()。如果在点击时注册,可能会触发生命周期异常,也会让回调关系变得混乱。
回调里直接操作已经销毁的 View
Fragment 场景尤其要小心。结果返回时,用户可能已经离开页面,或者 View 已经重建。把结果先交给 ViewModel,再由 UI 观察状态,是更稳的方式。
忽略失败和取消路径
选择图片可能返回 null,拍照可能失败,权限可能被拒绝,打开页面也可能取消。ActivityResult API 让成功路径更简洁,但失败路径仍然要认真处理。
把 launcher 放进 ViewModel
ViewModel 不适合持有 launcher。它应该接收结果,不应该发起系统 UI 行为。否则会把生命周期对象泄漏进业务层,后续测试和复用都会变困难。
自定义 Contract 过度封装
Contract 适合封装稳定、复用频繁的业务跳转。如果只是一个页面内部使用的一次性逻辑,直接使用内置合约更简单。不要为了封装而封装。
迁移旧代码的建议
迁移时不要一次性重写所有 onActivityResult()。更稳妥的方式是按业务模块逐步替换。
可以先从权限申请、选图、拍照这些边界清晰的场景开始,因为它们有现成合约,收益明显。然后再处理页面间结果返回,把 requestCode 分支逐渐拆成独立 launcher。最后,对于多个页面复用的业务跳转,再考虑自定义 Contract。
迁移过程中要保留测试点:取消操作、权限拒绝、页面旋转、后台返回、Uri 为空、相机写入失败。这些路径比成功路径更容易暴露问题。
小结
ActivityResult API 的意义,不只是替代旧接口,而是让结果回调从“集中分发”变成“局部注册”。它把发起动作、输入类型、输出类型和结果处理放在更接近业务的位置,代码可读性和可维护性都会提升。
在项目里使用时,可以记住几个原则:launcher 在初始化阶段注册,点击时只调用 launch();Fragment 回调尽量把结果交给 ViewModel;权限、选图、拍照要完整处理取消和失败路径;复杂业务跳转可以用自定义 Contract 收敛 Intent 细节。
当这些边界建立起来以后,Activity、Fragment 和 ViewModel 的职责会更清楚,页面间结果传递也会从容易膨胀的回调分发,变成更稳定的工程结构。