第 11 周继续围绕 Activity,但重点从"页面自己怎么活着",切到"页面之间怎么来回"。很多刚入门的 Demo 会把跳转写成一行:startActivity(Intent(this, DetailActivity::class.java))。这当然能跑,可真实业务里,页面跳转从来不只是一行代码。
你会遇到这些问题:这个页面应该显式跳还是隐式跳?参数放 Intent 还是 Bundle?为什么不能把大对象直接塞进 Intent?旧的 startActivityForResult() 还能不能用?共享元素动画到底是体验优化,还是又一个容易掉帧的坑?
所以这一周我没有只做"点按钮打开新页面",而是做了一个 Activity 跳转实验室:同一页里观察显式跳转、隐式跳转、Bundle 小参数、ActivityResult API、共享元素动画和大数据传输替代方案。
一、相关资料
| 来源 | 相关 |
|---|---|
| Android Developers:Intent 和 Intent Filter | 显式 / 隐式 Intent、action / data / category、intent-filter |
| Android Developers:Parcelable 和 Bundle | Bundle 小参数、Binder 事务大小、TransactionTooLargeException 边界依据 |
| Android Developers:获取 Activity 的结果 | registerForActivityResult() 和 ActivityResultContracts 的实践依据 |
AndroidX API:ActivityResultContracts |
用于确认 StartActivityForResult 通用 Contract 的 API 边界 |
| Android Developers:使用动画启动 Activity | 共享元素动画、ActivityOptionsCompat、transitionName 的实践依据 |
二、先看第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.StartActivityForResult、ActivityResultLauncher、Activity.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(不指定具体组件,只描述"我要做什么")依靠 action、data、category 和 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_CITY、REQUEST_CODE_PICK_IMAGE、REQUEST_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.StartActivityForResult、setResult()、finishAfterTransition() - 系统机制:结果回调分发、生命周期重建、Activity 返回栈
- Jetpack / 三方库:AndroidX Activity Result API
- 性能工具:Logcat、异常日志
- 优化关键词:去 requestCode、结果回调稳定、生命周期安全
- 常见坑:把 launcher 注册放进点击事件、以为 API 会自动保存业务状态
六、Intent 大数据传输:传 key,不传对象
官方文档对 Parcelable 和 Bundle 的提醒非常直接:通过 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 / 类:
Bundle、Intent.putExtra()、Parcelable、TransactionTooLargeException、UUID - 系统机制: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()、transitionName、finishAfterTransition() - 系统机制:Activity window transition、共享元素匹配、布局同步
- Jetpack / 三方库:AndroidX Core Compat
- 性能工具:Profile GPU Rendering、Layout Inspector、帧率观察
- 优化关键词:视觉连续性、转场降级、元素数量控制
- 常见坑:两个页面
transitionName不一致、目标元素异步加载导致闪烁、复杂布局做共享元素
八、本周 Demo 功能清单
| 能力 | Demo 落点 | 你可以怎么验证 |
|---|---|---|
| 显式跳转 | Week11ActivityNavigationActivity.openExplicitDetail() |
点击"显式跳转",目标页展示 source 和 requestId |
| 隐式跳转 | openImplicitReceiver() + Manifest intent-filter |
点击"隐式跳转",观察 action / data 是否被接收页展示 |
Bundle 小参数 |
Week11ResultActivity.createIntent() |
查看目标页读取 EXTRA_SOURCE、EXTRA_REQUEST_ID |
| 返回结果 | registerForActivityResult() + setResult() |
点击"返回结果",在目标页点 RESULT_OK 回到主页面 |
| 大数据优化 | Week11PayloadStore + payloadId |
点击"大数据传 key",目标页从缓存读取模拟大对象 |
| 共享元素动画 | transitionName + ActivityOptionsCompat |
点击"共享元素",观察 badge 从主页面过渡到目标页 |
九、这一周最容易踩的坑
- 所有页面都用显式跳转:内部页面可以,外部能力如分享、浏览器、地图更适合隐式 Intent。
- 隐式跳转不做兜底 :没有接收方时可能抛
ActivityNotFoundException。 - 忘记
CATEGORY_DEFAULT:Activity 声明了 action,也可能接不到startActivity()发出的隐式 Intent。 - 到处手写 extra key:调用方和目标页协议散落,后期很难维护。
- 把大对象塞进 Intent :可能卡顿、序列化失败,甚至触发
TransactionTooLargeException。 - 点击时才注册 ActivityResult 回调:页面重建后结果分发容易出问题。
- 共享元素动画滥用:复杂 View、大图、多元素转场都可能拖慢页面。
- 把单例缓存当长期方案:本周只是最小 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() 导致返回动画不完整 |