把 XML 换成 Jetpack Compose 要动几刀?用 Sunflower 项目拆一遍
【免费下载链接】sunflowerA gardening app illustrating Android development best practices with migrating a View-based app to Jetpack Compose.项目地址: https://gitcode.com/gh_mirrors/su/sunflower
Sunflower 是一个园艺管理示例应用,仓库里完整保留了一个 View + XML 布局应用迁到 Jetpack Compose 的路径。这篇文章用仓库里的真实代码拆一遍:先动哪个页面、ViewModel 的衔接位置、安全网怎么留,以及把你自己应用迁到 Jetpack Compose 时的真实代价。
维护老 XML 布局最头疼的三件事
维护老应用时,打开 res/layout/ 通常会撞见三件事。
一是 UI 和逻辑分居两个文件:列表项在 XML 里四行,Fragment 里 findViewById、setData 又是十几行,改一个字段要动两处。二是状态自己扛:旋转屏幕时页面位置、筛选条件都要手动保存,漏一个就是"转屏回到首页"的 bug。三是测试绑死资源 ID:Espresso 的断言挂在 @+id/plant_name 上,布局一改,测试跟着崩。
这三件事不是 XML 的错,是"命令式拼视图"模型的税。Jetpack Compose 的方向是把"拼装"换成"描述",也就是 Android 声明式 UI 的核心:数据进、界面出,重组负责刷新。
🌻 Sunflower:把迁移过程完整存档的样本
Sunflower 原本是 Google 用来演示 Jetpack 各组件的园艺应用,后来接了第二个任务:记录把一个现成 View 应用迁到 Jetpack Compose 的全过程。可以克隆下来对照着看:git clone https://gitcode.com/gh_mirrors/su/sunflower
它值得读有两点。第一是策略文档:仓库里的 docs/MigrationJourney.md 写明了三步走——先定"自底向上"策略、五个页面逐个重写、最后用 Navigation Compose 替换 Fragment 导航并删光 Fragment,一步不缺。第二是终态:应用入口 GardenActivity 的正文只剩 enableEdgeToEdge() 和 setContent { SunflowerTheme { SunflowerApp() } },res/layout/ 下只剩一个 XML 文件。首页分页、植物列表、植物详情、花园、图库五个页面,全部住在 app/src/main/java/com/google/samples/apps/sunflower/compose/ 的子目录里。
这是一个"活体样本":每个页面的 Composable 源码都在,不只是前后对比截图。
先动哪里:从详情页开刀,导航最后碰
Sunflower 的迁移顺序很"工程化":五个页面逐个来,先动最简单的植物详情页,再是植物列表、图库、花园,最后才动最复杂的、带 ViewPager2 分页的首页。而导航,被刻意留到最后。
导航为什么最后动?因为每个页面重写期间仍由旧导航托管,应用随时可装可用;等页面全部重写完,最后一步才把 Fragment 导航换成 Navigation Compose,一次性删掉所有 Fragment。代价是两套导航体系并存一阵子,收益是每次合并都很小、可回滚。
页面内部,列表项的写法是彻底换了一套。旧的是一份 XML 加 findViewById,新的是一个 @Composable 函数,数据直接传进来:
<!-- 旧写法:id 定义在 XML,Fragment 里 findViewById --> <ImageView android:id="@+id/plant_image" /> <TextView android:id="@+id/plant_name" /> // 新写法:数据进,UI 出 @Composable fun PlantListItem(plant: Plant, onClick: () -> Unit) { ImageListItem(plant.name, plant.imageUrl, onClick) }等最后一个页面完成,整个导航被压缩成 Screen.kt 里的一个 sealed class 加 SunflowerApp.kt 里的一段 NavHost:
NavHost(navController, startDestination = "home") { composable("home") { HomeScreen(onPlantClick = ...) } composable("plantDetail/{plantId}") { PlantDetailsScreen(...) } composable("gallery/{plantName}") { GalleryScreen(...) } }首页同理:原来 ViewPager2 + Fragment 的分页,在 HomeScreen 里换成 HorizontalPager + TabRow,纯 Kotlin 完成。
状态衔接:ViewModel 原样不动,只换消费者
很多人担心迁 Compose 等于全量重写,Sunflower 的答案是"只换最后一公里"。
PlantListViewModel 的核心没变:growZone 用一个 MutableStateFlow 承载,flatMapLatest 出 LiveData<List >,growZone 的值同步写进 SavedStateHandle。这段逻辑迁移中一行没动,变的只是消费端——旧 Fragment 用 observe 订阅,新页面用 hiltViewModel() 拿到后收集。再往下的 Room、仓库层、Hilt 注入图、WorkManager 的数据库种子任务,全部原样保留。
这个决策的回报在细节里:选一个生长区再旋转屏幕,筛选状态还在,干重活的是 SavedStateHandle 而不是 UI 框架。
如果你要动手,先检查你的状态住在 ViewModel 里还是 Fragment/View 里。状态在 UI 层的话,迁移会变成重写——先把状态上提,再谈换页面。
⚖️ 取舍与坑:Sunflower 做了、没做什么
去掉教科书滤镜,迁移里有几处值得抄,有几处要警惕。
做了:先织安全网。app/src/androidTest/ 下每个 Compose 页面都有配套 instrumentation 测试,例如 PlantListTest 用 createComposeRule 拉起页面,再断言 "Apple" 可见。旧的资源 ID 断言换成了 onNodeWithText 的语义断言,布局怎么改,测试不再崩。
做了:数据层不动。自底向上,每一步小、可回滚,这是它敢一页一页推进的前提。
留了尾巴:试验性库。版本目录 gradle/libs.versions.toml 里列着 paging-compose 的 RC、glide compose 的 beta;示例应用可以容忍,生产项目要自己锁版本并盯变更。
这套方式的真实代价是并存期的维护成本——Fragment 导航和 Navigation Compose 并行一阵子,你得同时记住两套导航。如果你习惯"大爆炸"式重写,先评估这笔账。
下一步:删掉最后一个 XML 的账
Sunflower 已经走到"UI 层只剩 Compose",这类样本的下一站通常是:状态全面 Compose 化、用 StateFlow 替代 LiveData。PlantListViewModel 已经做到一半——growZone 本身就是 StateFlow,只是出口套了 asLiveData。想看迁移后的性能底数,仓库里还有 macrobenchmark/ 目录,启动和帧率基准都已经就位。
留一个问题给你:你维护的应用,res/layout/ 现在还有几个页面?按 Sunflower 的顺序套一遍——最简单的页面先动、导航最后碰、测试当安全网——你项目里最后一个 XML 被删掉,需要几个月?
【免费下载链接】sunflowerA gardening app illustrating Android development best practices with migrating a View-based app to Jetpack Compose.项目地址: https://gitcode.com/gh_mirrors/su/sunflower
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考