从 XML + ViewBinding 到纯 Compose,APK 从 19.55MB 降至 9.59MB甚至以下,冷启动稳定在 380ms
一、写在前面
去年年底,我开源了一套 Android 基础架构框架 Android Architecture Templates,它基于 MVVM + 模块化分层架构(Core/Service/Middleware/Features),涵盖了网络、存储、路由、启动、崩溃、日志、埋点等全套基建能力。
开源后收到了不少开发者的反馈和 Star,但也有一个声音反复出现:“为什么还在用 XML + ViewBinding?能不能迁移到 Compose?”
于是,我决定把这个框架全量迁移到 Jetpack Compose。经过两周的集中开发,v1.1.0-compose 版本正式发布。
这篇文章,我会完整记录这次迁移的技术方案、踩坑经历和最终收益。
二、迁移前的项目状态
2.1 架构概览
在 v1.0 版本中,项目采用 MVVM + 模块化分层架构,共 14 个模块:
app (壳工程)
↓
features (业务功能层) —— feature_login 登录模块
↓
middleware (中间件层) —— router + permission
↓
service (基础服务层) —— startup + crash + logger
↓
core (核心基建层) —— base/network/storage/cache/common/ui
这套架构的核心设计理念是:依赖方向严格向下,features → middleware → service → core,确保代码高内聚、低耦合。
2.2 核心指标(v1.0)
| 指标 | 数值 |
|---|---|
| 冷启动耗时 | ~450ms |
| Release APK 大小 | 19.55MB(优化后 9.59MB) |
| UI 框架 | XML + ViewBinding |
| 模块数量 | 14 个 |
| 代码行数 | ~5000+ 行 |
2.3 为什么决定迁移?
技术债务积累:随着 Compose 生态的成熟,继续维护 XML + ViewBinding 意味着:
- 新功能开发效率低(XML + 绑定代码 + 状态管理三处修改)
- 难以复用 Compose 生态的成熟库(如 Navigation Compose、 Accompanist 等)
- 团队新成员更倾向于学习 Compose,而非维护旧技术栈
Google 官方推荐:增量迁移是官方推荐的最佳实践,让 Compose 和 View 在代码库中共存,直到应用完全迁移至 Compose。而我的项目页面数量很少(Splash、Login、Dashboard),正是全量迁移的最佳时机。
三、迁移方案设计
3.1 迁移范围
| 类型 | 迁移前 | 迁移后 |
|---|---|---|
| UI 框架 | XML + ViewBinding | Jetpack Compose |
| Activity 基类 | BaseActivity<VB, VM> | BaseComposeActivity<State, Event, VM> |
| Fragment | BaseFragment<VB, VM> | 已移除(全部改用 Compose Screen) |
| 主题系统 | themes.xml + attrs.xml | Compose MaterialTheme + StateFlow 驱动 |
| 主题切换 | AppCompatDelegate | StateFlow 响应式切换,无需 recreate |
| 依赖库 | appcompat / material / fragment-ktx / viewpager2 | 全部移除 |
3.2 核心原则
- 基建层完全不动:
core/network、core/storage、core/common等纯逻辑层 0 修改 - 业务逻辑层保持不变:
ViewModel、Repository继续使用,仅替换 UI 渲染层 - 渐进式清理:先让 Compose 跑起来,再逐步移除 View 体系残留
四、迁移实战:关键步骤与踩坑记录
4.1 第一步:Gradle 依赖配置
踩坑:Kotlin 版本与 Compose Compiler 版本必须严格匹配。
我的项目使用 Kotlin 2.1.20,对应的 Compose Compiler 版本是 2.1.20(Kotlin Gradle Plugin 内置)。
# gradle/libs.versions.toml
[versions]
kotlin = "2.1.20"
compose-bom = "2025.08.00"
[plugins]
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
在模块中应用 Compose Compiler 插件后,不再需要 composeOptions { kotlinCompilerExtensionVersion } 配置。
4.2 第二步:创建 Compose 主题系统
原有的 XML 主题(themes.xml、attrs.xml、colors.xml)需要迁移到 Compose。
关键设计:主题切换使用 StateFlow 驱动,而非 AppCompatDelegate。
// ThemeManager.kt
object ThemeManager {
private val _isDark = MutableStateFlow(KVStorage.getBoolean(KEY_DARK_MODE, false))
val isDark: StateFlow<Boolean> = _isDark.asStateFlow()
fun toggleTheme() {
_isDark.update { !it }
KVStorage.putBoolean(KEY_DARK_MODE, _isDark.value)
}
}
// AppTheme.kt
@Composable
fun AppTheme(content: @Composable () -> Unit) {
val isDark by ThemeManager.isDark.collectAsStateWithLifecycle()
val colorScheme = if (isDark) DarkColorScheme else LightColorScheme
MaterialTheme(colorScheme = colorScheme, content = content)
}
效果:切换主题时不重建 Activity,UI 自动重组,丝滑无闪烁。
4.3 第三步:BaseComposeActivity 设计
原有的 BaseActivity 依赖 ViewBinding,需要替换为纯 Compose 版本:
abstract class BaseComposeActivity<State, Event : ViewEvent, VM : BaseViewModel<State, Event>> : ComponentActivity() {
protected abstract val viewModel: VM
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
observeEvents()
setContent {
AppTheme {
// 占位 + 内容平滑过渡
Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background))
LaunchedEffect(Unit) {
yield() // 等待一帧,确保 Compose 完全初始化
}
ScreenContent()
}
}
}
@Composable
protected abstract fun ScreenContent()
}
4.4 第四步:Splash 闪屏页迁移(消除白屏)
问题:闪屏 Overlay 消失后,Compose 内容还未渲染完成,出现短暂白屏。
解决方案:使用 installSplashScreen() + setKeepOnScreenCondition 保持 Overlay,同时在 setContent 中立即渲染品牌色占位。
class SplashActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
var composeReady by mutableStateOf(false)
splashScreen.setKeepOnScreenCondition { !composeReady }
super.onCreate(savedInstanceState)
setContent {
AppTheme {
// 立即渲染品牌色占位
Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.primary))
LaunchedEffect(Unit) {
// 执行启动任务...
composeReady = true
navigateToMain()
}
}
}
}
}
4.5 第五步:页面迁移(Splash → Login → Dashboard)
迁移前:3 个 Activity + 3 个 XML 布局文件 + 3 个 ViewBinding 类
迁移后:3 个 @Composable Screen 函数 + 3 个继承 BaseComposeActivity 的 Activity
以 Login 页面为例:
@Composable
fun LoginScreen(viewModel: LoginViewModel = viewModel()) {
val state by viewModel.state.collectAsState()
Column(modifier = Modifier.fillMaxSize().padding(24.dp)) {
OutlinedTextField(
value = state.phone,
onValueChange = viewModel::onPhoneChanged,
label = { Text("手机号") }
)
Button(
onClick = viewModel::login,
enabled = state.isLoginEnabled
) {
Text("登录")
}
}
}
代码量对比:XML + Activity + ViewBinding 约 200 行 → Compose Screen 约 80 行,减少 60%。
4.6 第六步:清理 View 体系残留
迁移完成后,执行全量清理:
| 清理项 | 数量 |
|---|---|
| 删除 View 组件类 | 12 个(badge/dialog/loading/snackbar/state/toast/transition/veil/widget 等) |
| 删除 XML 资源文件 | 19 个(layout/anim/drawable) |
| 删除 BaseActivity/BaseFragment | 2 个 |
| 删除无用空目录 | 14 个 |
| 移除依赖库 | appcompat / material / fragment-ktx / viewpager2 |
五、关键技术亮点
5.1 KSP 路由 + 生成类型兼容性(行业痛点突破)
在 v1.0 中,我实现了一套基于 KSP 的路由注解处理器。但在迁移过程中遇到了一个行业级难题:
问题:getSymbolsWithAnnotation 在解析类时,如果类的签名中包含尚未生成的类型(如 ViewBinding),会静默跳过该类,导致 @Route 注解被遗漏。
解决方案:实现 双重扫描机制:
- 标准
resolver.getSymbolsWithAnnotation()获取能正常解析的符号 - 递归遍历
resolver.getAllFiles(),手动检查每个声明是否有目标注解 - 合并结果并去重
这套方案不仅解决了 ViewBinding 的兼容问题,也适用于任何 KSP + 生成类型的场景(如 Dagger/Hilt + ViewBinding)。
5.2 主题响应式切换(无需 recreate)
传统 XML 主题切换依赖 AppCompatDelegate.setDefaultNightMode(),会导致 Activity 重建(recreate()),造成闪烁和状态丢失。
我们的方案:ThemeManager.isDark 使用 StateFlow,UI 通过 collectAsStateWithLifecycle 订阅,主题切换时只触发 Compose 重组,不重建 Activity。
5.3 DAG 启动任务调度器(冷启动 380ms)
启动任务调度器基于有向无环图(DAG) 解析任务依赖,自动并行执行无依赖的任务。
迁移前后对比:
| 方案 | Application 创建耗时 |
|---|---|
| 传统串行初始化 | ~800ms |
| DAG 并行调度 | ~230ms |
| 性能提升 | 70% |
完整启动链路耗时:
进程创建 115ms
Application 创建 231ms
启动任务执行 2ms
Activity 创建 0ms
首帧渲染 32ms
总耗时: 380ms
5.4 APK 瘦身:19.55MB → 9.59MB
| 优化手段 | 效果 |
|---|---|
| ABI 过滤(仅保留 arm64-v8a) | -40% ~ -60% |
| 资源压缩(shrinkResources = true) | -10% ~ -20% |
| 图片转 WebP(质量 75%) | -20% ~ -30% |
| R8 混淆 + 移除未使用代码 | -15% ~ -25% |
| 移除 appcompat/material 等依赖 | -5% ~ -10% |
六、迁移前后对比
| 维度 | v1.0 (XML + ViewBinding) | v1.1.0 (纯 Compose) |
|---|---|---|
| UI 框架 | XML + ViewBinding | Jetpack Compose |
| Activity 基类 | BaseActivity<VB, VM> | BaseComposeActivity<State, Event, VM> |
| Fragment | 支持 | 已移除 |
| 主题切换 | AppCompatDelegate(重建 Activity) | StateFlow 驱动(仅重组) |
| 依赖库 | appcompat / material / fragment-ktx / viewpager2 | 全部移除 |
| APK 大小 | 9.59MB | < 9MB(预期) |
| 冷启动耗时 | ~450ms | ~380ms |
| UI 代码量 | XML + Activity + ViewBinding | 纯 Kotlin Compose(-60%) |
七、踩坑实录
7.1 Compose Compiler 版本不匹配
现象:编译报错 Compose Compiler requires Kotlin version X but found Y
解决:Kotlin 2.1.20 使用 org.jetbrains.kotlin.plugin.compose 插件,版本号与 Kotlin 保持一致,不再需要 kotlinCompilerExtensionVersion。
7.2 闪屏页白屏
现象:Splash → Main 跳转时出现短暂白屏
解决:installSplashScreen() + setKeepOnScreenCondition + 品牌色 Box 占位三重保障
7.3 主题切换后 UI 未更新
现象:调用 ThemeManager.toggleTheme() 后 UI 未变化
解决:使用 collectAsStateWithLifecycle 订阅 StateFlow,确保 Compose 能感知状态变化
7.4 路由注解被 KSP 遗漏
现象:部分 @Route 注解未被扫描到
解决:实现双重扫描机制,手动遍历 resolver.getAllFiles() 查找注解
八、迁移收益总结
8.1 开发效率提升
- UI 代码量减少 60%:纯 Kotlin,无需维护 XML 和 ViewBinding
- 状态管理更清晰:单向数据流,State 驱动 UI 重组
- 预览调试更方便:
@Preview注解支持设计时预览
8.2 性能提升
- 冷启动 380ms:达到行业顶级水平
- APK < 9MB:轻量级交付
- 主题切换无重建:体验更丝滑
8.3 技术债务清零
- 移除 12 个 View 组件类
- 移除 19 个 XML 资源文件
- 移除 appcompat / material / fragment-ktx / viewpager2 依赖
- 14 个空目录清理
九、项目地址
GitHub:github.com/kyrach-m/Sa…
如果这个项目对你有帮助,欢迎 Star、Fork、提 Issue。
十、写在最后
从 v1.0 到 v1.1.0,这次迁移不仅仅是 UI 框架的替换,更是对整套架构的一次全面体检和升级。
一些感悟:
- 架构是长期投资:好的架构能让你在技术演进时游刃有余。正是因为 v1.0 做了严格的模块化分层,迁移时才能做到“基建层 0 修改,UI 层全量替换”。
- Compose 不是银弹,但值得拥抱:迁移过程中确实遇到了一些坑(版本兼容、白屏、主题切换),但解决后的收益是巨大的。
- 开源的价值:把项目开源出来,不仅能收到反馈,也能倒逼自己把代码写得更好。
希望这篇文章对正在考虑迁移到 Compose 的你有所帮助。如果有什么问题,欢迎在评论区交流!
作者:kyrach GitHub:github.com/kyrach-m 欢迎关注我的技术专栏,更多 Android 架构实战分享。