ActivityResult API 实战:替代 onActivityResult 的现代回调设计

简介: 本文详解 Android ActivityResult API 实战:替代传统 onActivityResult,通过生命周期感知、类型安全的注册式回调,解决 requestCode 冲突、结果解析混乱等问题。涵盖权限申请、拍照选图、Fragment 使用、自定义 Contract 及 ViewModel 协作,助你写出更清晰、稳定、可维护的代码。

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

相关文章
|
1天前
|
人工智能 JSON 安全
|
1天前
|
云安全 人工智能 安全
|
3天前
|
人工智能
Qwen3.8抢先体验!正式版即将发布并开源!
千问Qwen3.8即将开源,参数达2.4T,进化速度以“天”计,实力媲美Fable 5。预览版Qwen3.8-Max已上线阿里Token Plan等平台,限时优惠:日间Credits低至1折,夜间更优,个人/团队版月付仅35元起!
545 20
|
3天前
|
人工智能 自然语言处理 数据挖掘
Qwen3.8-Max 预览版全解析:2.4 万亿参数旗舰模型,Token Plan 限时优惠指南
Qwen3.8-Max-Preview是通义千问Qwen3系列旗舰MoE大模型,参数达2.4万亿,综合推理能力居行业第一梯队。支持思考/快速双模式,擅长大模型五大高难场景。现于阿里云百炼Token Plan、Qoder及QoderWork上线体验,个人版低至39元/月。在阿里云百炼官网:https://t.aliyun.com/U/fPVHqY 免费领取千万Tokens
443 1
Qwen3.8-Max 预览版全解析:2.4 万亿参数旗舰模型,Token Plan 限时优惠指南
|
2天前
|
人工智能 测试技术 语音技术
Qwen-Audio-3.0-TTS 正式发布!AI 语音从 “能说话” 升级到 “会带情绪表达”
阿里云发布Qwen-Audio-3.0-TTS语音合成大模型,支持细粒度标签控制(如[gasp][angry])、freestyle自由风格、16种语言及20种方言,声学鲁棒性强。含Flash(首包延时300ms)和Plus(全球榜单冠军)双版本,已在百炼平台开放调用。在阿里云百炼官网:https://t.aliyun.com/U/fPVHqY 免费领取千万Tokens
468 0
|
9天前
|
缓存 UED 开发者
Codex109天重置23次,明天还要再送一次
Codex近109天完成23次额度重置,7月14日将迎来第24次。Tibo高频响应用户反馈:优化GPT-5.6高消耗问题、补发失效福利、调整重置时间——形成“反馈→回应→修复→补偿”正向闭环,彰显以用户为中心的产品哲学。(239字)
830 12
|
1天前
|
人工智能 自然语言处理 数据挖掘
最新版通义千问(Qwen3.8-Max-Preview)功能介绍
2026年,通义千问正式推出全新旗舰级大模型 **Qwen3.8-Max-Preview 预览版**,作为首款突破万亿参数规格的新一代基座模型,该模型总参数量达到**2.4万亿**,采用全新迭代的MoE混合专家架构,综合推理性能、长文本处理、多模态理解、复杂任务规划能力全面超越前代Qwen3.7-Max版本,整体实力跻身全球第一梯队,可对标海外顶级旗舰模型,是当前面向复杂工程开发、多智能体协同、超长文档解析、专业办公自动化场景的最优国产基座模型。
545 0
|
12天前
|
存储 人工智能 JSON
Qwen 本地部署搭配 ComfyUI 生成 AI 漫剧完整实操指南(小白零基础可落地,零成本无限生成+角色一致性天花板)
2026全网最优本地漫剧流水线:零成本、离线运行、角色统一、低配(8G显卡)可跑。融合Qwen本地大模型+ComfyUI双引擎,实现剧本生成→分镜绘图→动态成片全自动,隐私安全、无审核限流,新手30分钟上手,日更无忧。(239字)