第11周:Activity 跳转与传值 + 跳转优化

第 11 周继续围绕 Activity,但重点从"页面自己怎么活着",切到"页面之间怎么来回"。很多刚入门的 Demo 会把跳转写成一行:startActivity(Intent(this, DetailActivity::class.java))。这当然能跑,可真实业务里,页面跳转从来不只是一行代码。

你会遇到这些问题:这个页面应该显式跳还是隐式跳?参数放 Intent 还是 Bundle?为什么不能把大对象直接塞进 Intent?旧的 startActivityForResult() 还能不能用?共享元素动画到底是体验优化,还是又一个容易掉帧的坑?

所以这一周我没有只做"点按钮打开新页面",而是做了一个 Activity 跳转实验室:同一页里观察显式跳转、隐式跳转、Bundle 小参数、ActivityResult API、共享元素动画和大数据传输替代方案。

一、相关资料

来源 相关
Android Developers:Intent 和 Intent Filter 显式 / 隐式 Intentaction / data / categoryintent-filter
Android Developers:Parcelable 和 Bundle Bundle 小参数、Binder 事务大小、TransactionTooLargeException 边界依据
Android Developers:获取 Activity 的结果 registerForActivityResult()ActivityResultContracts 的实践依据
AndroidX API:ActivityResultContracts 用于确认 StartActivityForResult 通用 Contract 的 API 边界
Android Developers:使用动画启动 Activity 共享元素动画、ActivityOptionsCompattransitionName 的实践依据

二、先看第11周 Demo 主结构

第 11 周主页面叫 Week11ActivityNavigationActivity。它的第一段关键代码不是 startActivity(),而是 ActivityResult 的注册位置:

kotlin 复制代码
class Week11ActivityNavigationActivity : AppCompatActivity() {
​
    private lateinit var binding: ActivityWeek11ActivityNavigationBinding
​
    private val editProfileLauncher = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->
        if (result.resultCode == Activity.RESULT_OK) {
            val message = result.data?.getStringExtra(Week11ResultActivity.EXTRA_RESULT_MESSAGE)
                ?: "目标页没有返回内容"
            binding.tvResult.text = "最近返回结果:$message"
        } else {
            binding.tvResult.text = "最近返回结果:用户取消或目标页未返回数据。"
        }
    }
}

这段代码最值得注意的是:registerForActivityResult() 没有写在按钮点击里,而是作为 Activity 的成员提前注册。官方文档强调,每次创建 Activity 或 Fragment 时,都应该无条件注册结果回调。原因很现实:你启动相册、相机、联系人页以后,自己的页面可能被系统回收;页面重建后,系统还要能把结果分发回来。

ActivityResultContracts.StartActivityForResult() 是一个通用 Contract(契约,定义输入和输出类型)。它的输入是一个 Intent,输出是 ActivityResult,适合迁移老式 startActivityForResult() 流程。新手先用它理解结果返回没问题,但如果是选图片、拍照、请求权限,后面更推荐用更具体的 Contract。

如果少了提前注册,而是在点击按钮时临时注册,短 Demo 可能看不出问题,但一旦遇到配置变化、系统回收、多结果请求顺序变化,结果就可能找不到正确回调。

实践

资料编辑页、地址选择页、优惠券选择页、城市选择页,都很适合用 ActivityResult API。成熟团队通常会把"启动选择页"和"接收选择结果"封装在页面边界里,而不是到处散落 requestCode。这样页面重构时,不用到一个巨大的 onActivityResult() 里猜是谁返回的。

相关技术
  • API / 类:registerForActivityResult()ActivityResultContracts.StartActivityForResultActivityResultLauncherActivity.RESULT_OK
  • 系统机制:Activity 重建、结果分发、Lifecycle 到达 CREATED 后才能 launch
  • Jetpack / 三方库:AndroidX Activity Result API、AppCompat
  • 性能工具:Logcat、页面跳转耗时打点
  • 优化关键词:结果回调解耦、提前注册、类型边界
  • 常见坑:点击时才注册回调、多 launcher 注册顺序不稳定、结果回来后业务状态已经丢失

三、显式跳转:应用内部页面优先用它

显式 Intent(明确指定目标组件的消息对象)最适合应用内部页面跳转。它不需要系统去猜谁能处理,而是直接说:我要打开 Week11ResultActivity

Demo 里的写法是这样的:

kotlin 复制代码
fun createIntent(
    context: Context,
    source: String,
    requestId: String,
    payloadId: String? = null
): Intent {
    return Intent(context, Week11ResultActivity::class.java).apply {
        putExtra(EXTRA_SOURCE, source)
        putExtra(EXTRA_REQUEST_ID, requestId)
        payloadId?.let { putExtra(EXTRA_PAYLOAD_ID, it) }
    }
}

我特意把构造 Intent 的逻辑放到了目标页的 createIntent() 里。这样做不是形式主义,而是为了把参数协议集中起来:目标页需要哪些 key、哪些参数可选、默认值是什么,都在一个地方能看到。

如果每个调用方都手写:

kotlin 复制代码
Intent(this, Week11ResultActivity::class.java).putExtra("xxx", value)

项目大了以后,key 很容易写错,参数也很容易漏传。页面还能打开,但目标页拿不到数据,最后变成一堆空判断。

目标页读取参数也很简单:

ini 复制代码
val source = intent.getStringExtra(EXTRA_SOURCE).orEmpty()
val requestId = intent.getStringExtra(EXTRA_REQUEST_ID).orEmpty()

putExtra()getStringExtra() 适合传小参数,比如 id、标题、来源、筛选条件、布尔开关。它不适合传完整对象图,这个边界后面会单独讲。

实践

电商详情、内容详情、订单详情、个人主页这类内部页面,大部分都应该用显式跳转。成熟团队常见做法是给每个页面提供 createIntent() 或路由参数对象,调用方只传业务 id,目标页自己加载数据。这样页面入口清楚,也更容易做埋点、权限拦截和参数校验。

相关技术
  • API / 类:Intent(context, TargetActivity::class.java)putExtra()getStringExtra()Companion object
  • 系统机制:显式组件启动、Activity 栈入栈、参数打包和解包
  • Jetpack / 三方库:AppCompat;后续 Navigation 周会补导航框架边界
  • 性能工具:启动耗时日志、StrictMode
  • 优化关键词:参数协议集中、内部页面可控跳转、参数校验
  • 常见坑:到处手写 extra key、传参协议散落、目标页默认值不清楚

四、隐式跳转:不是打开别的 App 才叫隐式

隐式 Intent(不指定具体组件,只描述"我要做什么")依靠 actiondatacategory 和 Manifest 里的 <intent-filter> 匹配目标。

本周 Demo 做了一个应用内隐式接收页:

kotlin 复制代码
private fun openImplicitReceiver() {
    val intent = Intent(ACTION_WEEK11_VIEW_DEMO).apply {
        data = Uri.parse("study://week11/navigation?from=implicit")
        putExtra(EXTRA_IMPLICIT_MESSAGE, "隐式 Intent 由 action + data 匹配到本应用接收页。")
    }
​
    try {
        startActivity(intent)
        addLog("隐式跳转:通过 action/data 找到匹配的 Activity。")
    } catch (e: ActivityNotFoundException) {
        addLog("隐式跳转失败:没有 Activity 能处理这个 Intent。")
    }
}

Manifest 里对应的接收方是:

ini 复制代码
<activity
    android:name=".Week11ImplicitReceiverActivity"
    android:exported="false"
    android:label="第11周隐式 Intent 接收页">
    <intent-filter>
        <action android:name="com.study.all.action.WEEK11_VIEW_DEMO" />
        <category android:name="android.intent.category.DEFAULT" />
        <data
            android:host="week11"
            android:pathPrefix="/navigation"
            android:scheme="study" />
    </intent-filter>
</activity>

这里有几个细节不能省。

action 表示要做什么,Demo 里是 com.study.all.action.WEEK11_VIEW_DEMO。自定义 action 最好带上应用包名前缀,避免跟别人的 action 撞名。

data 表示操作的数据,Demo 里是 study://week11/navigation?from=implicit。系统会拿 scheme、host、path 等信息和 <data> 匹配。

CATEGORY_DEFAULT 很重要。一个 Activity 想接收 startActivity() 发出的隐式 Intent,通常需要在 <intent-filter> 里声明 android.intent.category.DEFAULT。少了它,明明 action 对了,也可能匹配不到。

try/catch ActivityNotFoundException 也不是多余代码。隐式跳转本来就存在"没人接"的情况,比如用户没有安装地图 App、浏览器被禁用、分享目标不存在。真实项目不能假设系统一定能找到接收方。

实践

分享、打开浏览器、打开地图、选择文件、扫码结果跳转,都是隐式 Intent 高频场景。成熟团队会对这些入口做兜底:先检查能否处理,或者捕获异常;涉及外部应用时,还会关注导出组件、安全校验和用户隐私边界。

相关技术
  • API / 类:Intent(action)Uri.parse()ActivityNotFoundException<intent-filter>CATEGORY_DEFAULT
  • 系统机制:Intent 解析、action/data/category 匹配、组件导出边界
  • Jetpack / 三方库:无强依赖
  • 性能工具:Logcat、adb shell cmd package resolve-activity(后续调试可用)
  • 优化关键词:外部能力复用、跳转兜底、接收方匹配
  • 常见坑:忘记 CATEGORY_DEFAULT、没有处理找不到目标、把 intent-filter 当安全边界

五、ActivityResult API:结果返回不要再靠一堆 requestCode

传统写法里,很多项目会在一个 Activity 里维护一堆 REQUEST_CODE_SELECT_CITYREQUEST_CODE_PICK_IMAGEREQUEST_CODE_EDIT_PROFILE。等结果回来,再在 onActivityResult() 里写一大串 if / when

第 11 周直接使用新版 API:

ini 复制代码
binding.btnForResult.setOnClickListener {
    val intent = Week11ResultActivity.createIntent(
        context = this,
        source = "ActivityResult API",
        requestId = "REQ-${System.currentTimeMillis()}"
    )
    editProfileLauncher.launch(intent)
    addLog("启动结果页:使用 registerForActivityResult 返回用户选择。")
}

目标页返回结果:

scss 复制代码
binding.btnReturnOk.setOnClickListener {
    val resultIntent = Intent().putExtra(
        EXTRA_RESULT_MESSAGE,
        "目标页确认完成:${System.currentTimeMillis()}"
    )
    setResult(Activity.RESULT_OK, resultIntent)
    finishAfterTransition()
}

这两段合起来就是完整的结果链路:主页面提前注册 launcher,点击时 launch(intent);目标页 setResult();主页面回调里处理 resultCode 和返回的 Intent

ActivityResult API 的价值不是"写法新",而是把结果回调和生命周期重新对齐。它要求你提前注册回调,这样系统重建页面后仍有机会把结果送到正确位置。它也减少了手写 requestCode 的混乱。

但它不是魔法。它不会自动帮你保存业务上下文。比如你启动选择地址页前,页面里有一个未保存的临时订单对象;如果进程被杀,结果回来时这个对象可能没了。真正重要的业务状态,仍然要放到 ViewModel、持久化存储或可恢复的状态容器里。

实践

城市选择、地址选择、资料编辑、优惠券选择这类"打开一个页面,回来给当前页一个结果"的流程,都可以优先考虑 ActivityResult API。成熟团队会进一步封装成更清晰的业务 contract,但底层原则仍然是:注册稳定、结果类型清楚、业务状态可恢复。

相关技术
  • API / 类:registerForActivityResult()ActivityResultLauncher.launch()ActivityResultContracts.StartActivityForResultsetResult()finishAfterTransition()
  • 系统机制:结果回调分发、生命周期重建、Activity 返回栈
  • Jetpack / 三方库:AndroidX Activity Result API
  • 性能工具:Logcat、异常日志
  • 优化关键词:去 requestCode、结果回调稳定、生命周期安全
  • 常见坑:把 launcher 注册放进点击事件、以为 API 会自动保存业务状态

六、Intent 大数据传输:传 key,不传对象

官方文档对 ParcelableBundle 的提醒非常直接:通过 Intent 发送数据时,应把数据限制在几 KB。数据过大可能触发 TransactionTooLargeException

这背后不是 Android 小气,而是因为 Intent 的 extras 底层要进入 Bundle,很多系统交互还会经过 Binder 事务。Binder 缓冲区大小有限,并且由进程中正在处理的事务共享。你以为只是一个页面传参,实际可能和状态保存、系统服务调用一起抢这块缓冲区。

Demo 里故意不用 Intent 传大对象,而是传 payloadId

ini 复制代码
private fun openWithLargePayloadKey() {
    val payloadId = UUID.randomUUID().toString()
    Week11PayloadStore.put(
        payloadId,
        Week11LargePayload(
            title = "本地缓存的大对象",
            description = "真实项目不要把大列表、Bitmap 或完整 JSON 塞进 Intent。",
            itemCount = 500
        )
    )
​
    val intent = Week11ResultActivity.createIntent(
        context = this,
        source = "大数据优化",
        requestId = "PAYLOAD-${payloadId.take(8)}",
        payloadId = payloadId
    )
    startActivity(intent)
}

目标页再根据 key 取数据:

ini 复制代码
val payloadId = intent.getStringExtra(EXTRA_PAYLOAD_ID)
val payload = payloadId?.let(Week11PayloadStore::get)

这里的 Week11PayloadStore 只是本周的最小 Demo,用来说明"Intent 只传 key"的思路。真实项目里更稳的方案通常是 Repository、数据库、文件、缓存层,或者和页面生命周期绑定的 ViewModel。单例缓存有明显边界:进程死亡后数据会丢,长期持有大对象也可能造成内存压力。

如果你要打开商品详情,传商品 id;打开图片预览,传图片 URI;打开订单页,传订单号。不要把完整商品对象、Bitmap、大 JSON、完整列表塞进去。短期省事,后期会用卡顿和崩溃还回来。

实践

电商、内容社区、IM、搜索页都经常有"大对象传参诱惑":商品卡片对象、帖子对象、聊天消息列表、搜索结果列表。成熟团队通常只传稳定 id,然后目标页从统一数据层加载;如果为了体验需要预加载,也会走缓存 key,而不是把整包数据塞进 Bundle

相关技术
  • API / 类:BundleIntent.putExtra()ParcelableTransactionTooLargeExceptionUUID
  • 系统机制:Binder 事务、序列化 / 反序列化、进程级缓冲区共享
  • Jetpack / 三方库:ViewModel、SavedStateHandle、Repository(后续架构周深入)
  • 性能工具:崩溃日志、StrictMode、启动耗时分析、Memory Profiler
  • 优化关键词:传 ID、不传对象、轻量参数、缓存 key
  • 常见坑:传 Bitmap、大列表、大 JSON、自定义 Parcelable 跨进程传递

七、共享元素动画:让跳转有连续性,但别滥用

共享元素动画适合"用户刚点了一个视觉元素,目标页还要继续展示它"的场景。比如列表缩略图到详情大图、商品卡片到商品详情、头像到个人主页。

Demo 里用一个 TextView 做共享元素:

ini 复制代码
<TextView
    android:id="@+id/tv_shared_badge"
    android:layout_width="match_parent"
    android:layout_height="72dp"
    android:gravity="center"
    android:text="共享元素:Week11 Navigation"
    android:transitionName="week11_shared_badge" />

目标页里也有同名元素:

ini 复制代码
<TextView
    android:id="@+id/tv_badge"
    android:layout_width="match_parent"
    android:layout_height="96dp"
    android:gravity="center"
    android:transitionName="week11_shared_badge" />

启动时用 ActivityOptionsCompat

kotlin 复制代码
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
    this,
    binding.tvSharedBadge,
    Week11ResultActivity.TRANSITION_NAME_BADGE
)
ActivityCompat.startActivity(this, intent, options.toBundle())

transitionName 是共享元素匹配的关键。源页面和目标页必须使用同一个名字,系统才知道这两个 View 是同一个视觉对象的前后状态。

这里也有边界。共享元素不应该用在所有跳转上。复杂 ViewGroup、大图未加载完成、多个元素一起转场,都可能增加测量和绘制成本。真实项目里一般优先选 1 到 2 个稳定元素,比如封面、头像、卡片容器。返回时目标页使用 finishAfterTransition(),可以让反向转场更自然。

实践

内容详情、商品详情、相册预览、个人主页都适合共享元素。成熟团队会把它当"帮助用户理解页面来源"的体验工具,而不是为了炫动画。滑动频繁、低端机占比高、图片加载不稳定的页面,要谨慎开启复杂转场。

相关技术
  • API / 类:ActivityOptionsCompat.makeSceneTransitionAnimation()ActivityCompat.startActivity()transitionNamefinishAfterTransition()
  • 系统机制:Activity window transition、共享元素匹配、布局同步
  • Jetpack / 三方库:AndroidX Core Compat
  • 性能工具:Profile GPU Rendering、Layout Inspector、帧率观察
  • 优化关键词:视觉连续性、转场降级、元素数量控制
  • 常见坑:两个页面 transitionName 不一致、目标元素异步加载导致闪烁、复杂布局做共享元素

八、本周 Demo 功能清单

能力 Demo 落点 你可以怎么验证
显式跳转 Week11ActivityNavigationActivity.openExplicitDetail() 点击"显式跳转",目标页展示 sourcerequestId
隐式跳转 openImplicitReceiver() + Manifest intent-filter 点击"隐式跳转",观察 action / data 是否被接收页展示
Bundle 小参数 Week11ResultActivity.createIntent() 查看目标页读取 EXTRA_SOURCEEXTRA_REQUEST_ID
返回结果 registerForActivityResult() + setResult() 点击"返回结果",在目标页点 RESULT_OK 回到主页面
大数据优化 Week11PayloadStore + payloadId 点击"大数据传 key",目标页从缓存读取模拟大对象
共享元素动画 transitionName + ActivityOptionsCompat 点击"共享元素",观察 badge 从主页面过渡到目标页

九、这一周最容易踩的坑

  1. 所有页面都用显式跳转:内部页面可以,外部能力如分享、浏览器、地图更适合隐式 Intent。
  2. 隐式跳转不做兜底 :没有接收方时可能抛 ActivityNotFoundException
  3. 忘记 CATEGORY_DEFAULT :Activity 声明了 action,也可能接不到 startActivity() 发出的隐式 Intent。
  4. 到处手写 extra key:调用方和目标页协议散落,后期很难维护。
  5. 把大对象塞进 Intent :可能卡顿、序列化失败,甚至触发 TransactionTooLargeException
  6. 点击时才注册 ActivityResult 回调:页面重建后结果分发容易出问题。
  7. 共享元素动画滥用:复杂 View、大图、多元素转场都可能拖慢页面。
  8. 把单例缓存当长期方案:本周只是最小 Demo,真实项目要考虑进程死亡和内存释放。

十、技术清零表

技术 它是什么 Demo 落点 真实项目价值 常见坑
Intent Android 组件间通信消息对象 主页面所有跳转方法 启动页面、传递少量参数、调用外部能力 不区分显式 / 隐式使用场景
显式 Intent 明确指定目标组件的跳转 Intent(context, Week11ResultActivity::class.java) 内部页面跳转安全可控 到处手写参数 key
隐式 Intent 只声明动作和数据,由系统匹配目标 Intent(ACTION_WEEK11_VIEW_DEMO) 分享、浏览器、地图、文件选择等通用能力 不处理无接收方
intent-filter 组件声明自己能接收什么隐式 Intent Manifest 中 Week11ImplicitReceiverActivity 建立 action / data / category 匹配规则 当成安全边界,忽略 exported
action Intent 要执行的动作 ACTION_WEEK11_VIEW_DEMO 定义业务动作或系统动作 自定义 action 不加包名前缀导致冲突
data / Uri Intent 操作的数据 study://week11/navigation 深链、网页、地图、文件定位 setData()setType() 混用互相覆盖
Bundle / extras Intent 携带的小型键值参数 putExtra(EXTRA_SOURCE, source) 传 id、来源、筛选条件等轻量参数 塞大对象、大列表、Bitmap
ActivityResult API AndroidX 推荐的结果返回机制 registerForActivityResult() 替代 requestCode 和 onActivityResult() 混乱 点击时才注册,业务状态不保存
ActivityResultContracts.StartActivityForResult 通用启动 Intent 并接收结果的 Contract editProfileLauncher 迁移旧式任意 Intent 返回流程 仍需手动解析 resultCode 和 data
setResult() 目标页设置返回结果 Week11ResultActivity.btnReturnOk 选择页、编辑页、确认页返回数据 忘记在 finish 前设置结果
TransactionTooLargeException Binder 事务数据过大时可能抛出的异常 大数据传 key 小节 约束 Intent / Bundle 的数据大小 以为没到 1MB 就一定安全
ActivityOptionsCompat AndroidX 转场动画兼容封装 makeSceneTransitionAnimation() 共享元素动画和兼容降级 共享元素过多、目标 View 不稳定
transitionName 匹配两个 Activity 中共享 View 的名字 week11_shared_badge 建立视觉连续性 源和目标名字不一致
finishAfterTransition() 带转场结束 Activity 目标页返回按钮 让共享元素反向动画更自然 用普通 finish() 导致返回动画不完整
相关推荐
私人珍藏库2 小时前
[Android] BBLL 开源第三方B哩电视TV端
android·app·生活·工具·多功能
杉氧5 小时前
跨平台资源管理:一套代码如何搞定 Android、iOS 和 Web 的图片与多语言?
android·架构·android jetpack
安卓修改大师6 小时前
安卓修改大师实战:从反编译到定制的完整APK修改指南
android
柚鸥ASO优化7 小时前
安卓APP推广的“降本增效”密码:守好商店内,打好商店外
android·aso优化
我是一颗柠檬7 小时前
【Java项目技术亮点】EXPLAIN深度分析与慢查询治理
android·java·开发语言
Android-Flutter8 小时前
android compose shadow 阴影 使用
android·kotlin·compose
帅次8 小时前
Android 高级工程师面试:Java 多线程与并发 近1年高频追问 22 题
android·java·面试
2501_943782358 小时前
【共创季稿事节】摩斯电码转换器:编码表与双向转换的实现
android·华为·鸿蒙·鸿蒙系统
STCNXPARM8 小时前
Android selinux详解
android·selinux