关于 xml-to-compose-migration
此迁移技能帮助 Android 开发者在保留功能的同时,将 XML 布局转换为符合 Jetpack Compose 惯例的实现。内容涵盖布局映射、状态迁移、渐进式互操作和验证。
主要功能
- 01XML 布局到 Compose 的映射
- 02状态与事件迁移
- 03XML 和 Compose 渐进式互操作
- 04常见控件转换模式
- 05无障碍与视觉验证
适用场景
- 01将 XML 页面替换为 Compose
- 02把 RecyclerView 迁移为 LazyColumn
- 03在现有 XML 应用中嵌入 Compose
- 04验证迁移后的界面和状态
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ new-silvermoon/ awesome-android-agent-skills --skill xml-to-compose-migration安装后在 Claude Code 中输入/xml-to-compose-migration确认已加载。
详细介绍
XML 到 Compose 的迁移
概述
系统地将 Android XML 布局转换为符合惯用写法的 Jetpack Compose,同时保留功能并采用 Compose 模式。本技能涵盖布局映射、状态迁移和渐进式采用策略。
工作流
1. 分析 XML 布局
- 确定根布局类型(
ConstraintLayout、LinearLayout、FrameLayout等)。 - 列出所有 View 控件及其关键属性。
- 映射数据绑定表达式(
@{})或 View Binding 引用。 - 确定需要特殊处理的自定义 View。
- 记下是否使用了
include、merge或ViewStub。
2. 规划迁移
- 决定采用:完全重写还是渐进式迁移(使用
ComposeView/AndroidView)。 - 确定状态来源(ViewModel、LiveData、savedInstanceState)。
- 列出要提取为独立 Composable 的可复用组件。
- 如果使用 Navigation 组件,规划导航集成方式。
3. 转换布局
使用下方的布局映射表,将每个 View 转换为对应的 Compose 元素。
4. 迁移状态
- 将
LiveData观察转换为StateFlow收集或observeAsState()。 - 使用 Compose 状态替代
findViewById/ViewBinding。 - 将点击监听器转换为 lambda 参数。
5. 测试与验证
- 对比 XML 和 Compose 版本的视觉效果。
- 测试无障碍功能(内容描述、触控目标)。
- 验证状态在配置变更后是否得以保留。
布局映射参考
容器布局
| XML 布局 | Compose 对应项 | 说明 |
|---|---|---|
LinearLayout (vertical) | Column | 使用 Arrangement 和 Alignment |
LinearLayout (horizontal) | Row | 使用 Arrangement 和 Alignment |
FrameLayout | Box | 子项彼此叠放 |
ConstraintLayout | ConstraintLayout (Compose) | 使用 createRefs() 和 constrainAs |
RelativeLayout | Box 或 ConstraintLayout | 简单重叠时优先使用 Box |
ScrollView | Column + Modifier.verticalScroll() | 或使用 LazyColumn 显示列表 |
HorizontalScrollView | Row + Modifier.horizontalScroll() | 或使用 LazyRow 显示列表 |
RecyclerView | LazyColumn / LazyRow / LazyGrid | 最常见的迁移方式 |
ViewPager2 | HorizontalPager | 来自 accompanist 或 Compose Foundation |
CoordinatorLayout | 自定义实现 + Scaffold | 配合滚动行为使用 TopAppBar |
NestedScrollView | Column + Modifier.verticalScroll() | 优先使用 Lazy 系列组件 |
常见控件
| XML 控件 | Compose 对应项 | 说明 |
|---|---|---|
TextView | Text | 使用 style → TextStyle |
EditText | TextField / OutlinedTextField | 需要状态提升 |
Button | Button | 使用 onClick lambda |
ImageView | Image | 使用 painterResource() 或 Coil |
ImageButton | IconButton | 内部使用 Icon |
CheckBox | Checkbox | 需要 checked 和 onCheckedChange |
RadioButton | RadioButton | 在分组中配合 Row 使用 |
Switch | Switch | 需要状态提升 |
ProgressBar (circular) | CircularProgressIndicator | |
ProgressBar (horizontal) | LinearProgressIndicator | |
SeekBar | Slider | 需要状态提升 |
Spinner | DropdownMenu + ExposedDropdownMenuBox | 模式较复杂 |
CardView | Card | 来自 Material 3 |
Toolbar | TopAppBar | 在 Scaffold 内使用 |
BottomNavigationView | NavigationBar | Material 3 |
FloatingActionButton | FloatingActionButton | 在 Scaffold 内使用 |
Divider | HorizontalDivider / VerticalDivider | |
Space | Spacer | 使用 Modifier.size() |
属性映射
| XML 属性 | Compose 修饰符/属性 |
|---|---|
android:layout_width="match_parent" | Modifier.fillMaxWidth() |
android:layout_height="match_parent" | Modifier.fillMaxHeight() |
android:layout_width="wrap_content" | Modifier.wrapContentWidth()(通常隐式设置) |
android:layout_weight | Modifier.weight(1f) |
android:padding | Modifier.padding() |
android:layout_margin | 在父级使用 Modifier.padding(),或使用 Arrangement.spacedBy() |
android:background | Modifier.background() |
android:visibility="gone" | 条件式组合(不发出该元素) |
android:visibility="invisible" | Modifier.alpha(0f)(保留占用空间) |
android:clickable | Modifier.clickable { } |
android:contentDescription | Modifier.semantics { contentDescription = "" } |
android:elevation | Modifier.shadow() 或组件的 elevation 参数 |
android:alpha | Modifier.alpha() |
android:rotation | Modifier.rotate() |
android:scaleX/Y | Modifier.scale() |
android:gravity | Alignment 参数或 Arrangement |
android:layout_gravity | Modifier.align() |
常见模式
带权重的 LinearLayout
<LinearLayout android:orientation="horizontal">
<View android:layout_weight="1" />
<View android:layout_weight="2" />
</LinearLayout>
// Compose
Row(modifier = Modifier.fillMaxWidth()) {
Box(modifier = Modifier.weight(1f))
Box(modifier = Modifier.weight(2f))
}
将 RecyclerView 替换为 LazyColumn
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
// Compose
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items, key = { it.id }) { item ->
ItemRow(item = item, onClick = { onItemClick(item) })
}
}
带双向绑定的 EditText
<EditText
android:text="@={viewModel.username}"
android:hint="@string/username_hint" />
// Compose
val username by viewModel.username.collectAsState()
OutlinedTextField(
value = username,
onValueChange = { viewModel.updateUsername(it) },
label = { Text(stringResource(R.string.username_hint)) },
modifier = Modifier.fillMaxWidth()
)
ConstraintLayout 迁移
<androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/title"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/subtitle"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="@id/title" />
</androidx.constraintlayout.widget.ConstraintLayout>
// Compose
ConstraintLayout(modifier = Modifier.fillMaxWidth()) {
val (title, subtitle) = createRefs()
Text(
text = "Title",
modifier = Modifier.constrainAs(title) {
top.linkTo(parent.top)
start.linkTo(parent.start)
}
)
Text(
text = "Subtitle",
modifier = Modifier.constrainAs(subtitle) {
top.linkTo(title.bottom)
start.linkTo(title.start)
}
)
}
Include / Merge → 提取为 Composable
<merge>
<ImageView android:id="@+id/avatar" />
<TextView android:id="@+id/name" />
</merge>
<include layout="@layout/layout_header" />
// Compose: Extract as a reusable Composable
@Composable
fun HeaderSection(
avatarUrl: String,
name: String,
modifier: Modifier = Modifier
) {
Row(modifier = modifier) {
AsyncImage(model = avatarUrl, contentDescription = null)
Text(text = name)
}
}
// Usage
HeaderSection(avatarUrl = user.avatar, name = user.name)
渐进式迁移(互操作)
在 XML 中嵌入 Compose
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
// In Fragment/Activity
binding.composeView.setContent {
MaterialTheme {
MyComposable()
}
}
在 Compose 中嵌入 XML View
// Use AndroidView for Views that don't have Compose equivalents
@Composable
fun MapViewComposable(modifier: Modifier = Modifier) {
AndroidView(
factory = { context ->
MapView(context).apply {
// Initialize the view
}
},
update = { mapView ->
// Update the view when state changes
},
modifier = modifier
)
}
状态迁移
将 LiveData 迁移到 Compose
// Before: Observing in Fragment
viewModel.uiState.observe(viewLifecycleOwner) { state ->
binding.title.text = state.title
}
// After: Collecting in Compose
@Composable
fun MyScreen(viewModel: MyViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Text(text = uiState.title)
}
点击监听器
// Before: XML + setOnClickListener
binding.submitButton.setOnClickListener {
viewModel.submit()
}
// After: Lambda in Compose
Button(onClick = { viewModel.submit() }) {
Text("Submit")
}
检查清单
- 已转换所有布局(不再有
include或merge) - 已正确提升状态(用户输入没有内部可变状态)
- 已将点击处理器转换为 lambda
- 已移除 RecyclerView 适配器(改用 LazyColumn/LazyRow)
- 已移除 ViewBinding/DataBinding
- 已集成导航(NavHost 或互操作)
- 已应用主题(MaterialTheme)
- 已保留无障碍支持(内容描述、触控目标)
- 已添加用于开发的 Preview 注解
- 已删除旧的 XML 文件
参考资料
评论
支持 Markdown