Jetpack Compose 架构选型与陷阱:从 XML 到声明式的渐进式迁移
Jetpack Compose 不是"更简单的 XML",而是 Android UI 的范式转移。本文以百万日活 App 的迁移实践为蓝本,拆解 Compose 单向数据流、状态管理选型、性能陷阱、混编策略与未来 Multiplatform 演进,给出 15 年架构师视角下的完整 trade-off 矩阵。
一、为什么 Compose 是范式转移
Android UI 的范式转移来得比 iOS 晚——SwiftUI 2019,Jetpack Compose 1.0 在 2021 年发布。但背后的设计哲学完全一致:状态驱动 UI。开发者只描述 UI 应该长什么样,由框架根据状态变化自动 reconcile。
范式转移带来四个根本变化:状态显式化、UI 与逻辑彻底分离、Kotlin 优先、跨平台 UI 一致性。这四点决定了 Compose 时代的架构决策与 XML 时代完全不同——XML 时代是"怎么画",Compose 时代是"怎么定义状态"。
graph LR
A[XML 命令式
手动 setText/setVisibility] --> B[状态分散]
B --> C[UI 与逻辑耦合]
C --> D[测试困难]
E[Compose 声明式
state 自动驱动] --> F[单一数据源]
F --> G[UI 与逻辑分离]
G --> H[易测试]
二、XML 时代架构回顾与痛点
理解 Compose 的价值,先要理解 XML 时代的痛点。Android XML + View 系统用了 14 年,遗留了大量架构问题。
2.1 状态分散
XML View 时代,UI 状态分散在多个地方:View 内部状态、Activity/Fragment 字段、数据库、网络响应、手动同步。状态不同步是头号 bug 来源——一个数据更新要手动同步 5 个 View,漏一个就出问题。
2.2 findViewById 与 ViewBinding
早期 findViewById 是性能与可维护性双重灾难。ViewBinding(2019)通过编译期代码生成解决了大部分问题,但仍然不能解决状态分散。
2.3 MVC / MVP / MVVM 之争
Android 时代的架构之争持续 10 年——MVC(Activity 充当 Controller)、MVP(Presenter 解耦)、MVVM(ViewModel + DataBinding)。没有银弹,每个团队都有自己的偏好,但共同问题是双向数据流——调试时追状态变化像噩梦。
graph TB
A[MVC Activity胖] --> B[MVP Presenter解耦]
B --> C[MVVM DataBinding双向]
C --> D[Compose UDF单向数据流]
三、Compose 单向数据流(UDF)
Compose 的官方推荐架构是单向数据流(UDF)——State 由 ViewModel 持有,UI 只读取 + 发送 Event。这是 Google 在 Android 团队反复强调的架构原则。
graph LR
A[State] --> B[Composable]
B -->|Event| C[ViewModel]
C -->|更新| A
3.1 UDF 的三大原则
- 单一数据源:状态只在 ViewModel 定义,UI 永远从同一处读取
- 状态只读:UI 不能直接修改状态,必须通过 ViewModel 方法
- 副作用隔离:网络、数据库等副作用集中在 ViewModel/repository 层
3.2 trade-off
UDF 的代价是代码量增加——State + Event + Effect 三套定义。但收益是可预测性指数级提升,任何 UI 异常都能通过状态历史回放定位。我们的实践:UDF 让线上 UI 相关 bug 减少 60%。
四、状态管理:State / MutableState / StateFlow
Compose 状态管理是最容易出错的地方。我总结成一张选型表——
4.1 状态容器选型
- remember:组合内短期状态(开关、文本框)
- rememberSaveable:跨配置变更保留(旋转屏幕)
- mutableStateOf:可变状态(必须配合 remember)
- StateFlow:跨 Composable 共享状态(推荐用于 ViewModel)
- CompositionLocal:跨组件树隐式传递(慎用)
4.2 State vs StateFlow
核心区别:State 是 Compose 原生(轻量、自动重组),StateFlow 是 Kotlin Flow(适合复杂业务流、跨平台)。
经验:UI 内部用 mutableStateOf,跨组件/跨页面用 StateFlow。混用会导致数据流混乱。
4.3 经典 bug:状态丢失
最常见 bug 是配置变更时状态丢失(旋转屏幕、切换深色模式)。remember 不行,必须用 rememberSaveable。或者把状态提升到 ViewModel,用 StateFlow 持有。
graph LR
A[remember] -->|旋转屏幕| B[状态丢失]
C[rememberSaveable] -->|旋转屏幕| D[状态保留]
E[StateFlow in ViewModel] -->|旋转屏幕| F[状态保留]
五、重组机制与性能陷阱
Compose 自动检测状态变化触发重组(Recomposition)——只重绘状态相关的 UI 部分。这是性能优势,但也是陷阱来源。
5.1 重组原理
Compose 编译器会静态分析哪些 Composable 函数读取了哪些 State。State 变化时,只重组读取它的函数(而非整个 UI)。这是 Compose 比 XML 快的根本原因。
5.2 常见性能陷阱
- Lambda 内创建对象:每次重组都新建 lambda,导致子组件无法跳过
- 不稳定参数:List、Map 等可变集合无法优化,导致不必要的重组
- 派生状态不缓存:derivedStateOf 没正确使用,导致重复计算
- 大列表渲染:LazyColumn 没正确用 key,导致 diff 失败
5.3 优化武器
三个核心优化手段——
| 优化 | 原理 | 场景 |
|---|---|---|
| remember | 缓存计算结果 | 派生数据、对象 |
| derivedStateOf | 派生状态 | 依赖其他 state 的 state |
| key() / stable | 标记稳定 | 自定义 data class |
| LazyColumn | 懒加载 | 长列表 |
六、迁移策略:渐进式 vs 重写式
已有 Android 项目迁移到 Compose,不能一次重写。两种策略:渐进式(新页面 Compose + 老页面 XML)和重写式(一次性全切)。我们选了渐进式——3 年迁移完,最稳。
6.1 渐进式迁移路径
Compose 通过 ComposeView 嵌入 XML,通过 AndroidView 在 Compose 中使用 XML View。新页面用 Compose,老页面逐步替换。我们的节奏:
graph LR
A[2022 Compose 5%] --> B[2023 Compose 25%]
B --> C[2024 Compose 55%]
C --> D[2025 Compose 80%]
D --> E[2026 Compose 100%]
6.2 重写式风险
一次性重写风险极高——业务不能停、UI 不能崩、API 不能变。我们见过两次大重写失败的案例:项目延期 9 个月 + 用户流失 25%。只有全新项目才适合重写。
6.3 迁移优先级
页面迁移按 ROI 排序——优先迁移:新页面 > 简单页面 > 中等页面 > 复杂页面。复杂页面(动画、表单、地图)最后迁,因为涉及 ViewModel 重构。
七、与 XML/View 混编:ComposeView 实战
渐进式迁移期间必须混编。两个关键 API:ComposeView(在 XML 中嵌入 Compose)和 AndroidView(在 Compose 中嵌入 View)。
7.1 ComposeView 模式
把 Composable 函数包装成 Android View,可以像普通 View 一样在 XML 中使用。这是新页面嵌入老工程的主要方式。注意:ComposeView 必须 setViewCompositionStrategy 控制生命周期。
7.2 AndroidView 模式
反过来,在 Compose 中嵌入传统 View(比如地图、相机、第三方 SDK)。这种场景在迁移期非常常见——老 SDK 还没出 Compose 版本。
graph LR
A[XML 工程] -->|ComposeView| B[Composable]
C[Composable] -->|AndroidView| D[View 控件]
7.3 数据流同步
混编期间数据流方向要严格——XML → Compose 用 update 回调,Compose → XML 用 state listener。混乱的数据流是迁移期 bug 的头号来源。
八、Navigation:Navigation Compose 架构
Navigation Compose 是 Compose 时代的官方导航方案。基于NavController + NavHost + composable destinations,取代了 XML 时代的 Fragment + Navigation Component。
8.1 Navigation Compose 优势
导航路径变成类型安全的 API(Kotlin Serialization 支持)、deep link 集成更自然、跨页面共享 ViewModel 简单。对复杂业务流(电商下单、多步骤表单)是革命性改进。
8.2 路径管理
路径必须在ViewModel 而非 Composable 里——否则页面栈状态无法持久化。NavController 由 Activity 持有,ViewModel 通过 SavedStateHandle 访问。
8.3 trade-off
Navigation Compose 的代价是与原有 Fragment 导航共存。我们用 NavHostController 同时管理 Compose 和 Fragment 目的地,通过 destination route 区分。复杂度可控,但需要统一抽象。
九、Compose 与 Kotlin 协程协作
Compose 是Kotlin 优先的 UI 框架,与协程(coroutines + Flow)天然集成。理解两者的协作是 Compose 架构的关键。
9.1 LaunchedEffect
在 Composable 中启动协程的最常用方式。LaunchedEffect(key) 会在 key 变化时取消并重启协程。典型用途:进入页面发请求、订阅 Flow、定时器。
9.2 collectAsState
把 Flow 转换成 State 的关键 API。flow.collectAsState() 会订阅 Flow 并在 Composable 中显示最新值。这是ViewModel StateFlow → UI 的标准模式。
9.3 rememberCoroutineScope
在 Composable 中获取协程 scope,用于主动取消(如退出页面时停止任务)。与 LaunchedEffect 的区别:前者由生命周期自动管理,后者由开发者手动控制。
graph LR
A[ViewModel] -->|StateFlow| B[Composable]
B -->|collectAsState| C[UI State]
B -->|LaunchedEffect| D[副作用]
D -->|结果| A
十、未来形态:Compose Multiplatform
Compose 不再是 Android 专属——JetBrains 在 2024 年发布了 Compose Multiplatform 稳定版,同一套 Composable 代码同时跑在 Android、iOS、Desktop、Web。这是 Android 架构的下一站。
10.1 Compose Multiplatform 战略
Google 与 JetBrains 合作,把 Compose 编译器与 UI 层抽离出 Android,做成跨平台框架。这对 Android 团队意味着——学会 Compose = 同时学会 iOS UI,跨平台成本降到最低。
10.2 与 KMP 协同
Compose Multiplatform + Kotlin Multiplatform = 完整跨平台方案——业务逻辑(数据层、网络层、业务规则)KMP 共享,UI 层 Compose Multiplatform 统一。我们已经在新业务试点。
10.3 Wear OS / TV 适配
Compose 已经扩展到 Wear OS、Android TV、Auto。同一套 UI 代码可以适配手表、电视、车机,跨端架构一致性达到新高度。
graph LR
A[Compose 代码] --> B[Android]
A --> C[iOS]
A --> D[Desktop]
A --> E[Web]
A --> F[Wear OS]
📌 关键架构决策清单
| 决策点 | 推荐方案 | 关键 trade-off |
|---|---|---|
| 架构模式 | UDF 单向数据流 | 代码量 vs 可维护性 |
| 状态管理 | remember + StateFlow 协同 | 灵活性 vs 复杂度 |
| 迁移策略 | 渐进式 3-5 年 | 风险 vs 技术债 |
| 性能优化 | 拆分小组件 + stable | 代码量 vs 性能 |
| 导航 | Navigation Compose + 路径绑定 | 类型安全 vs 学习 |
| 协程 | LaunchedEffect + collectAsState | 副作用隔离 vs 泄漏风险 |
| 混编 | ComposeView + AndroidView | 迁移成本 vs 长期分裂 |
| 未来 | Compose Multiplatform + KMP | 跨平台 vs 平台深度 |