首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >[Android 从零到一] ActivityResult API 实战:替代 onActivityResult 的现代回调设计

[Android 从零到一] ActivityResult API 实战:替代 onActivityResult 的现代回调设计

原创
作者头像
hunter android
发布2026-07-23 11:09:08
发布2026-07-23 11:09:08
290
举报

[Android 从零到一] ActivityResult API 实战:替代 onActivityResult 的现代回调设计

摘要:ActivityResult API 把页面跳转、权限申请、拍照选图等结果回调从集中式的 requestCode 分发,改成生命周期感知、类型更明确的注册式回调。本文从传统 onActivityResult 的痛点讲起,逐步拆解 ActivityResultContract、registerForActivityResult、权限申请、拍照选图和组件封装的工程实践,帮助你在真实项目中把结果处理写得更清晰、更稳定。

在 Android 开发里,页面 A 打开页面 B 并拿回结果,是非常常见的需求。比如编辑资料后返回用户昵称,打开系统相册后拿到图片 Uri,申请权限后决定是否继续执行任务。早期项目通常会使用 `startActivityForResult()`、`onActivityResult()` 和 `requestCode` 来完成这些逻辑。

这套写法能用,但在项目变大以后会越来越难维护:回调集中在 Activity 里,多个业务共用同一个 `onActivityResult()`,requestCode 容易冲突,结果类型也不够清晰。Jetpack 提供的 ActivityResult API 就是为了解决这些问题。它把“发起动作”和“接收结果”绑定在一起,并且和生命周期协作,避免很多传统写法里的隐性问题。

传统写法的问题在哪里

先看一个典型的旧写法:

代码语言:javascript
复制
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` 是输入类型,`O` 是结果类型。比如打开一个 Activity 并拿回 `ActivityResult`,输入是 `Intent`,输出是 `ActivityResult`;申请单个权限,输入是权限字符串,输出是布尔值。

最常见的写法如下:

代码语言:javascript
复制
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()` 中。现在可以这样写:

代码语言:javascript
复制
private val cameraPermissionLauncher = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) { granted ->
    if (granted) {
        openCamera()
    } else {
        showPermissionDeniedMessage()
    }
}

fun requestCameraPermission() {
    cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}

这里的结果就是一个 `Boolean`,语义非常直接。你不需要再解析权限数组,也不需要用 requestCode 找回业务上下文。

如果要申请多个权限,可以使用 `RequestMultiplePermissions()`:

代码语言:javascript
复制
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()`:

代码语言:javascript
复制
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,然后让相机把图片写入这个位置。

代码语言:javascript
复制
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 的成员注册,不要在点击事件里临时注册:

代码语言:javascript
复制
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 层根据当前生命周期渲染。

代码语言:javascript
复制
private val pickAvatarLauncher = registerForActivityResult(
    ActivityResultContracts.GetContent()
) { uri ->
    uri ?: return@registerForActivityResult
    viewModel.onAvatarSelected(uri)
}

这样即使页面重建,也能通过 ViewModel 保留业务状态,而不是把结果强行写进已经失效的 View。

自定义 Contract:让业务输入输出更干净

内置合约覆盖了很多常见场景,但真实项目里经常需要更贴近业务的输入输出。比如打开用户编辑页,只关心返回的用户资料,而不是原始 Intent。这个时候可以自定义 `ActivityResultContract`。

代码语言:javascript
复制
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)
    }
}

使用时业务代码会更清晰:

代码语言:javascript
复制
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 观察状态变化并渲染页面

比如选择头像:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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 的职责会更清楚,页面间结果传递也会从容易膨胀的回调分发,变成更稳定的工程结构。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • [Android 从零到一] ActivityResult API 实战:替代 onActivityResult 的现代回调设计
    • 传统写法的问题在哪里
    • ActivityResult API 的基本模型
    • 为什么它更适合真实项目
    • 申请权限:从回调地狱到局部处理
    • 选择图片:用系统能力减少兼容成本
    • 拍照:处理 Uri 比处理 Bitmap 更可靠
    • 在 Fragment 中使用时要注意什么
    • 自定义 Contract:让业务输入输出更干净
    • 和 ViewModel 怎么配合
    • 常见踩坑
      • 在点击事件里注册 launcher
      • 回调里直接操作已经销毁的 View
      • 忽略失败和取消路径
      • 把 launcher 放进 ViewModel
      • 自定义 Contract 过度封装
    • 迁移旧代码的建议
    • 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档