1. 从一个真实痛点说起Compose 迁移为什么让人“烧心”做过 Android 开发的人大概都有这种体会项目跑了两三年UI 层全是 XML 布局加findViewById突然有一天团队决定全面转向 Jetpack Compose。这个决定本身没问题Compose 的声明式写法、状态驱动、代码量压缩确实比传统 View 体系舒服太多。但真正动手迁移的时候问题就来了——一个中等规模的 App几十个 Activity、上百个 Fragment、无数个自定义 View你不可能推倒重来只能一个页面一个页面地啃。“烧心”这个词用得很准。它不是那种一刀切的痛苦而是一种持续的、隐隐的焦虑改了一半的页面状态对不上、RecyclerView 的 Adapter 逻辑不知道怎么搬到 LazyColumn、主题和样式系统两套并存、团队里有人写 Compose 有人写 XML代码评审的时候风格割裂。更麻烦的是迁移过程中业务需求还在往前跑你一边拆旧房子一边还得住人。Meta 这次分享的就是他们在大规模 Android 项目里用 AI 辅助 Compose 迁移的实战经验。注意这里的关键词是“辅助”不是“一键替换”。市面上那些号称“AI 一键迁移”的工具真拿到生产项目里跑一遍生成的代码能编译通过就算烧高香了。Meta 的思路更务实把迁移拆成可验证的小步骤让 AI 在每一步里承担它真正擅长的部分——模式识别、代码转换、重复劳动——而把架构决策和边界判断留给人。这篇文章我会围绕这个主题把 Compose 迁移的完整思路、AI 在各个环节的具体用法、RecyclerView 到 LazyColumn 的转换细节、以及实际踩过的坑全部拆开讲清楚。不管你是刚开始接触 Compose还是已经在迁移中途卡住了应该都能从里面找到能直接用的东西。2. 迁移前的整体设计为什么不能“一把梭”2.1 迁移策略的三种选择与取舍在动手之前先要想清楚迁移的粒度。我见过团队犯的最大错误就是一开始就定“三个月内全部迁完”这种目标结果做到一半发现根本不可控。实际可行的策略大概有三种第一种是页面级迁移以 Activity 或 Fragment 为单位一个页面完整迁完再动下一个。这种方式的优点是边界清晰迁完一个页面就能上线验证缺点是页面之间如果有共享的 UI 组件会出现两套实现并存的过渡期。第二种是组件级迁移从最小的 UI 单元开始比如一个按钮、一个卡片先做成 Compose 组件再逐步往上组装。这种方式适合设计系统比较统一的团队但问题是 Compose 组件和 View 组件之间的互操作成本不低ComposeView嵌在 XML 里、AndroidView嵌在 Compose 里来回桥接容易出性能问题。第三种是混合渐进式也是 Meta 实际采用的思路以页面级迁移为主线但在页面内部允许组件级混用同时用 AI 来保证两套代码之间的接口一致性。为什么选第三种因为真实项目的 UI 复杂度是不均匀的。有些页面就是简单的列表加详情迁起来很快有些页面有复杂的自定义绘制、手势交互、动画硬迁反而容易出 bug。混合渐进式允许你根据页面复杂度分配精力而不是被一个统一的时间表绑架。2.2 AI 在迁移中的角色定位这里必须把话说清楚AI 不是用来做架构决策的。它不知道你的业务逻辑不知道哪些状态该提升、哪些该下沉也不知道你的团队约定。AI 真正擅长的是三件事模式识别与批量转换把大量结构相似的 XML 布局转成 Compose 代码这种重复劳动 AI 做得又快又稳。API 映射RecyclerView 的 Adapter、ViewHolder、DiffUtil 这一套对应到 LazyColumn 的 items、key、contentType映射关系是固定的AI 可以准确完成。代码审查辅助迁移后的代码是否符合 Compose 的最佳实践比如有没有在 composable 里做副作用操作、有没有用remember缓存对象AI 可以快速扫一遍给出提示。而人需要负责的是状态管理架构的设计、导航结构的调整、主题系统的统一、以及最终的质量把关。这个分工如果不明确要么 AI 生成的代码没人敢用要么人把时间浪费在机械转换上。2.3 迁移的优先级排序逻辑不是所有页面都值得优先迁移。我的经验是按这个顺序排新开发的页面直接用 Compose不要再写 XML 了这是最自然的增量迁移。结构简单、交互少的页面优先迁比如设置页、关于页、静态展示页这些页面迁移风险低、收益快能帮团队快速积累 Compose 经验。列表类页面作为第二批因为 RecyclerView 到 LazyColumn 的转换有固定套路AI 辅助效果最好。复杂交互页面放最后等团队对 Compose 的状态管理和副作用处理已经熟练了再动。这个排序背后的逻辑是先建立信心和工具链再啃硬骨头。如果一上来就迁最复杂的页面很容易因为一个动画问题卡住两周整个迁移计划就崩了。3. 核心细节解析AI 辅助迁移的关键环节3.1 XML 布局到 Compose 的转换要点XML 布局转 Compose表面上看是语法转换实际上有几个容易踩坑的地方。布局层级的问题。XML 里常见的LinearLayout嵌套RelativeLayout再嵌套FrameLayout这种深层嵌套在 Compose 里应该被扁平化。AI 转换的时候如果只是机械地一层层对应成Column、Box生成的代码虽然能跑但完全丧失了 Compose 的优势。正确的做法是让 AI 识别出布局的实际意图比如“这个 RelativeLayout 只是为了把两个元素左右对齐”那就直接用一个Row加Arrangement.SpaceBetween搞定。尺寸单位的处理。XML 里的dp、sp在 Compose 里对应dp和sp的扩展属性这个映射很直接。但wrap_content和match_parent在 Compose 里没有直接对应需要用Modifier.wrapContentSize()或者Modifier.fillMaxWidth()来表达。AI 在这块容易出错的地方是layout_weightXML 里的权重在 Compose 里要用Modifier.weight()而且必须在Row或Column的作用域内才能调用。样式和主题的迁移。XML 里通过style属性引用的样式在 Compose 里应该转成MaterialTheme的对应属性或者自定义的CompositionLocal。这块是最需要人工介入的因为样式系统往往和设计规范绑定AI 不知道你们团队的颜色命名规则。我实际用下来AI 转换 XML 布局的准确率大概在七成左右剩下的三成需要人工调整。但即便是七成也省了大量敲键盘的时间。关键是转换之后要有一个检查清单逐项确认布局意图有没有丢失。3.2 RecyclerView 到 LazyColumn 的映射细节这是迁移里最高频也最模式化的部分值得单独拿出来讲。RecyclerView 的那套东西——Adapter、ViewHolder、DiffUtil、ItemDecoration、LayoutManager——在 Compose 里被 LazyColumn 大幅简化了但简化不代表没有对应关系。先看一个典型的 RecyclerView Adapterclass UserAdapter( private val users: ListUser, private val onClick: (User) - Unit ) : RecyclerView.AdapterUserAdapter.UserViewHolder() { class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val name: TextView itemView.findViewById(R.id.name) val avatar: ImageView itemView.findViewById(R.id.avatar) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder { val view LayoutInflater.from(parent.context) .inflate(R.layout.item_user, parent, false) return UserViewHolder(view) } override fun onBindViewHolder(holder: UserViewHolder, position: Int) { val user users[position] holder.name.text user.name holder.itemView.setOnClickListener { onClick(user) } } override fun getItemCount() users.size }对应的 Compose 写法Composable fun UserList( users: ListUser, onClick: (User) - Unit ) { LazyColumn { items( items users, key { it.id } ) { user - UserItem(user user, onClick onClick) } } } Composable fun UserItem(user: User, onClick: (User) - Unit) { Row( modifier Modifier .fillMaxWidth() .clickable { onClick(user) } .padding(16.dp), verticalAlignment Alignment.CenterVertically ) { AsyncImage( model user.avatarUrl, contentDescription null, modifier Modifier.size(48.dp).clip(CircleShape) ) Spacer(modifier Modifier.width(12.dp)) Text(text user.name, style MaterialTheme.typography.bodyLarge) } }这个转换里AI 能自动完成的部分包括Adapter 结构识别、ViewHolder 到 composable 函数的映射、onBindViewHolder里的赋值逻辑转成参数传递。但有几个点需要人工确认key 的设置。RecyclerView 里用DiffUtil做局部刷新Compose 里用key参数来标识 item。如果列表数据有稳定的唯一 ID一定要设 key否则列表滚动和状态保持会出问题。contentType 的处理。多类型列表在 RecyclerView 里靠getItemViewType在 Compose 里用items的contentType参数。这个转换 AI 经常漏掉需要手动补。点击事件的传递。RecyclerView 里点击事件通常绑在 itemView 上Compose 里应该绑在具体的 composable 上而且要注意clickable的 modifier 顺序放在padding之前和之后效果不一样。3.3 状态管理与副作用处理的迁移这是迁移里最容易被低估的部分。XML 体系下状态往往散落在 Activity、Fragment、ViewModel 和自定义 View 里迁移到 Compose 的时候如果不重新梳理状态流向很容易写出“能跑但很乱”的代码。Compose 的核心是状态驱动 UI所以迁移的时候要问自己这个页面的状态源在哪里哪些状态是 UI 状态哪些是业务状态UI 状态应该用remember还是rememberSaveable业务状态应该放在 ViewModel 里用StateFlow还是mutableStateOfAI 在这块的辅助价值在于它可以扫描迁移后的代码找出常见的反模式。比如在 composable 函数里直接启动协程、在重组过程中做网络请求、用var声明状态但没有用mutableStateOf包裹。这些问题的检测规则是明确的AI 可以快速给出报告。但状态架构的设计必须人来做。我的建议是迁移一个页面之前先画一张状态流向图标清楚每个状态的来源、消费者、以及生命周期。这张图不需要很正式白板上画几笔就行但它能帮你避免迁移后状态混乱的问题。4. 实操过程一个列表页面的完整迁移记录4.1 迁移前的准备工作我拿一个真实的用户列表页面来演示。这个页面在 XML 体系下的结构是一个Fragment包含一个RecyclerViewAdapter 里有两种 item 类型普通用户和推荐用户支持下拉刷新和上拉加载更多点击 item 跳转详情页。迁移前需要做的准备确认依赖。在build.gradle里加上 Compose 相关的依赖包括androidx.compose.ui、androidx.compose.material3、androidx.activity:activity-compose、androidx.lifecycle:lifecycle-viewmodel-compose。如果项目还在用旧版的 Kotlin 编译器插件需要升级到和 Compose 编译器版本匹配的版本。配置 Compose 编译器。在模块的build.gradle里设置composeOptions或者用 Kotlin 2.0 之后的 Compose 编译器 Gradle 插件。这块版本兼容性很容易出问题建议直接查官方文档的版本对照表。建立迁移分支。不要在主干上直接改开一个迁移分支每迁完一个页面就提交一次方便回滚和对比。准备 AI 辅助工具。我用的是 IDE 里的 AI 编程插件配合自定义的提示词模板。提示词里要明确说明目标是把 XML 布局和 RecyclerView Adapter 转成 Compose 代码要求使用 Material3 组件状态用remember管理列表用LazyColumn。4.2 布局文件的转换过程原始的 item 布局 XML 大概长这样LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:padding16dp ImageView android:idid/avatar android:layout_width48dp android:layout_height48dp android:scaleTypecenterCrop / LinearLayout android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:orientationvertical android:layout_marginStart12dp TextView android:idid/name android:layout_widthwrap_content android:layout_heightwrap_content android:textSize16sp android:textStylebold / TextView android:idid/description android:layout_widthwrap_content android:layout_heightwrap_content android:textSize14sp android:textColorcolor/gray / /LinearLayout /LinearLayout把这段 XML 和转换要求一起喂给 AI它生成的 Compose 代码基本可用但需要做几处调整layout_weight1对应的Modifier.weight(1f)必须放在Row的作用域内AI 有时候会把它放到错误的层级。textColorcolor/gray应该转成MaterialTheme.colorScheme.onSurfaceVariant或者自定义的颜色而不是硬编码颜色值。ImageView的scaleTypecenterCrop在 Compose 里对应ContentScale.Crop这个 AI 通常会正确转换。转换后的代码Composable fun UserItem(user: User, onClick: (User) - Unit) { Row( modifier Modifier .fillMaxWidth() .clickable { onClick(user) } .padding(16.dp), verticalAlignment Alignment.CenterVertically ) { AsyncImage( model user.avatarUrl, contentDescription null, contentScale ContentScale.Crop, modifier Modifier .size(48.dp) .clip(CircleShape) ) Spacer(modifier Modifier.width(12.dp)) Column(modifier Modifier.weight(1f)) { Text( text user.name, style MaterialTheme.typography.titleMedium, fontWeight FontWeight.Bold ) Text( text user.description, style MaterialTheme.typography.bodyMedium, color MaterialTheme.colorScheme.onSurfaceVariant ) } } }4.3 列表逻辑的迁移与验证Adapter 的迁移相对直接但有几个细节要注意。原来的 Adapter 里用了DiffUtil做局部刷新迁移到 Compose 后LazyColumn的key参数会自动处理这个问题不需要再手动写 DiffUtil。但前提是 key 要设置正确。多类型列表的处理Composable fun UserList( items: ListUserListItem, onUserClick: (User) - Unit ) { LazyColumn { items( items items, key { item - when (item) { is UserListItem.Normal - item.user.id is UserListItem.Recommended - rec_${item.user.id} } }, contentType { item - when (item) { is UserListItem.Normal - normal is UserListItem.Recommended - recommended } } ) { item - when (item) { is UserListItem.Normal - UserItem(item.user, onUserClick) is UserListItem.Recommended - RecommendedUserItem(item.user, onUserClick) } } } }迁移完成后验证步骤不能省功能验证列表滚动是否流畅、点击是否响应、下拉刷新是否正常、加载更多是否触发。状态验证旋转屏幕后列表位置是否保持、item 的选中状态是否正确。性能验证用 Compose 的布局检查器看重组次数确认没有不必要的重组。我实测下来一个中等复杂度的列表页面从 XML 迁移到 Compose包括调试和验证大概需要半天到一天。如果纯手工写可能要两到三天。AI 辅助的价值主要体现在布局转换和样板代码生成上逻辑部分还是得自己来。5. 常见问题与排查技巧实录5.1 迁移过程中的典型报错与解决问题一Compose 编译器版本不匹配。报错信息通常是This version of the Compose Compiler requires Kotlin version X。解决方法是查 Compose 编译器与 Kotlin 版本的对照表把composeOptions里的kotlinCompilerExtensionVersion改成匹配的版本。Kotlin 2.0 之后Compose 编译器已经合并到 Kotlin 仓库里配置方式变了需要加org.jetbrains.kotlin.plugin.compose插件。问题二Modifier.weight()报错 “Cannot access ‘weight’: it is internal”。这是因为weight是RowScope和ColumnScope的扩展函数必须在对应的作用域内调用。检查一下是不是把weight用在了Box或者外层Modifier上。问题三remember的值在重组后丢失。如果状态需要在配置变更比如旋转屏幕后保留要用rememberSaveable而不是remember。但rememberSaveable要求数据类型可以被Bundle存储自定义对象需要实现Saver。问题四LazyColumn 的 item 状态错乱。这通常是因为没有设置key或者 key 不唯一。检查一下 key 的生成逻辑确保每个 item 的 key 在整个列表生命周期内是稳定的。问题五Compose 和 View 混用时的触摸事件冲突。比如ComposeView嵌在ScrollView里滚动事件会被抢。解决方法是给ComposeView设置Modifier.nestedScroll或者调整触摸事件的拦截逻辑。5.2 常见问题速查表问题现象可能原因排查方向解决方案编译报错版本不匹配Compose 编译器与 Kotlin 版本不一致查版本对照表调整kotlinCompilerExtensionVersionweight无法访问不在 Row/Column 作用域内检查 Modifier 调用位置移到正确的 scope 内状态重组后丢失用了remember而非rememberSaveable检查状态声明方式改用rememberSaveable或 ViewModel列表 item 状态错乱key 缺失或不唯一检查items的 key 参数设置稳定唯一的 key触摸事件冲突Compose 与 View 嵌套滚动检查嵌套结构用nestedScroll或调整拦截重组次数过多状态读取范围过大用 Layout Inspector 查看缩小状态读取范围用derivedStateOf动画卡顿在重组中创建动画对象检查动画 API 使用用remember缓存动画状态5.3 独家避坑经验坑一不要一次性迁移整个模块。我试过把一个模块里所有页面一起迁结果编译错误几百个根本不知道从哪下手。后来改成一次只迁一个页面迁完编译通过、测试通过再动下一个效率反而高很多。坑二AI 生成的代码一定要过一遍。AI 有时候会生成看起来合理但实际有问题的代码比如在 composable 里直接调用ViewModel的方法而没有用LaunchedEffect包裹或者把StateFlow的collectAsState用错。这些错误编译能过但运行时行为不对。坑三主题系统要提前统一。如果 XML 主题和 Compose 主题各搞一套后面维护会很痛苦。建议在迁移开始前先把颜色、字体、形状这些设计 token 抽出来同时供 XML 和 Compose 使用。坑四测试要跟着迁移走。XML 时代的 UI 测试用的是 EspressoCompose 有专门的测试 API。迁移页面的时候测试代码也要一起迁否则后面回归测试会漏掉。坑五性能问题要早发现。Compose 的重组机制和 View 的绘制机制完全不同有些在 View 体系下没问题的写法在 Compose 里会导致频繁重组。建议迁移完一个页面就用 Layout Inspector 看一下重组情况别等到最后再优化。6. 迁移后的代码审查与质量保障6.1 Compose 代码的审查清单迁移完成后代码审查不能只看“能不能跑”还要看“写得对不对”。我整理了一份审查清单每次迁移完一个页面就对照着过一遍composable 函数是否是无副作用的纯函数网络请求、数据库操作这些应该放在 ViewModel 或LaunchedEffect里。状态声明是否用了mutableStateOf或mutableStateListOf普通var不会触发重组。remember的 key 是否正确如果依赖了外部变量要确保 key 变化时能重新计算。LazyColumn的key是否设置列表数据是否有稳定唯一标识Modifier的顺序是否合理padding和background的顺序会影响视觉效果。是否避免了在 composable 里创建对象比如SimpleDateFormat这种应该用remember缓存。主题颜色和字体是否从MaterialTheme获取避免硬编码。6.2 AI 辅助代码审查的实践人工审查容易漏尤其是迁移页面多的时候。我的做法是用 AI 做第一轮扫描把上面这些检查项写成提示词让 AI 逐文件检查并输出问题列表。然后人工复核 AI 标记的问题确认哪些是真问题、哪些是误报。实测下来AI 对“状态声明方式”“Modifier 顺序”“remember 使用”这几类问题的识别准确率比较高对“状态提升是否合理”这种需要理解业务逻辑的问题识别率一般。所以 AI 审查适合做兜底不能替代人工判断。6.3 迁移进度的度量与追踪迁移不是一锤子买卖可能需要持续几周甚至几个月。建议建一个简单的追踪表记录每个页面的迁移状态未开始、进行中、已完成、已审查、已上线。每周同步一次进度看看有没有卡住的页面。度量指标不用太复杂我关注三个迁移完成的页面数、迁移后发现的 bug 数、以及团队对 Compose 的熟悉程度可以用代码审查的返工率来间接衡量。这三个指标能帮你判断迁移是在健康推进还是已经失控。7. 一些个人体会和后续扩展方向迁移这件事技术只是一部分更多是团队协作和节奏把控。我最大的体会是不要追求“完美迁移”追求“可持续迁移”。有些页面迁得不够优雅但只要功能正确、代码可维护就可以先上线后面再优化。如果每个页面都要求一步到位迁移永远做不完。AI 在其中的价值是把你从重复劳动里解放出来让你有精力去处理真正需要思考的问题。但它不是银弹生成的代码需要审查架构需要人来定边界情况需要人来判断。把 AI 当成一个效率很高的初级工程师而不是一个可以托付一切的专家这个心态比较健康。后续如果继续深入有几个方向可以扩展一是把迁移流程进一步自动化比如写一个 Gradle 插件自动检测哪些页面还在用 XML 并生成迁移任务二是建立 Compose 组件的内部库把常用的 UI 模式封装起来减少重复代码三是把 AI 辅助迁移的经验沉淀成提示词模板和检查清单让团队里每个人都能复用。最后分享一个小技巧迁移的时候把旧的 XML 布局文件先留着不要急着删。等新页面在线上跑了一两周确认没有问题再清理。这样万一出了线上问题还能快速回滚到旧实现。这个习惯帮我避免了好几次深夜加班。