Android / Compose 2026-09-11

Jetpack Compose 架构选型与陷阱:从 XML 到声明式的渐进式迁移

Jetpack Compose 不是"更简单的 XML",而是 Android UI 的范式转移。本文以百万日活 App 的迁移实践为蓝本,拆解 Compose 单向数据流、状态管理选型、性能陷阱、混编策略与未来 Multiplatform 演进,给出 15 年架构师视角下的完整 trade-off 矩阵。

Jetpack Compose Android 声明式 UI 单向数据流 范式转移 渐进式迁移

一、为什么 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[易测试]
架构哲学:Compose 的本质是状态机规范。架构师的核心工作是定义 State、Event、Effect 的边界——而不是堆砌 Composable 函数。

二、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[状态保留]
原则:UI 内部状态用 remember,业务状态用 ViewModel + StateFlow。这条规则能避免 90% 的状态 bug。

五、重组机制与性能陷阱

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懒加载长列表
性能心法:重组次数 × 单次成本 = 总成本。要么减少重组次数(稳定参数 + 拆分小组件),要么降低单次成本(避免在 Composable 里做重操作)。两者皆优才是真优化。

六、迁移策略:渐进式 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
陷阱:Composable 不应该有"长期存活"的协程。所有协程都该由 LaunchedEffect / ViewModelScope 管理,离开页面时必须取消。否则会内存泄漏。

十、未来形态: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 平台深度
写在最后:Jetpack Compose 不只是 UI 框架的升级,而是Android 架构哲学的重塑。从 XML 命令式到 Compose 声明式,本质是复杂度管理方式的演进。架构师的工作不是堆砌新 API,而是在范式转移中找到稳定的核心——状态清晰、数据单向、副作用可控、协程隔离。这才是「探索架构之美」在 Android 时代的回响。