第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 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui