xml-to-compose-migration について
Android XML レイアウトを機能を保ちながら、慣用的な Jetpack Compose に移行するスキルです。全面的な書き換えや段階的な導入を計画する Android 開発者向けに、レイアウト対応、状態移行、相互運用、検証を扱います。
主な機能
- 01XML レイアウトから Compose への対応
- 02状態とイベントの移行
- 03XML と Compose の段階的な相互運用
- 04一般的なウィジェットの変換パターン
- 05アクセシビリティと表示の検証
活用シーン
- 01XML 画面の Compose への置き換え
- 02RecyclerView の LazyColumn への移行
- 03既存 XML アプリへの Compose の組み込み
- 04移行後の UI と状態の検証
インストール
コマンドをコピーしてターミナルで実行します。
$
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 レイアウトを、Compose のパターンを活用しながら機能を維持して、慣用的な Jetpack Compose に体系的に変換します。このスキルでは、レイアウトの対応付け、状態の移行、段階的な導入戦略について説明します。
ワークフロー
1. XML レイアウトを分析する
- ルートレイアウトの種類(
ConstraintLayout、LinearLayout、FrameLayoutなど)を特定します。 - すべての View ウィジェットと主要な属性を一覧にします。
- データバインディング式(
@{})またはビュー バインディング参照を対応付けます。 - 特別な処理が必要なカスタムビューを特定します。
include、merge、ViewStubの使用箇所を確認します。
2. 移行を計画する
- 全面的な書き換えにするか、
ComposeView/AndroidViewを使用した段階的な移行にするかを決めます。 - 状態のソース(ViewModel、LiveData、savedInstanceState)を特定します。
- 個別の Composable として抽出する再利用可能なコンポーネントを一覧にします。
- Navigation コンポーネントを使用している場合は、ナビゲーションとの統合を計画します。
3. レイアウトを変換する
以下のレイアウト対応表を使用して、各 View を対応する Compose 要素に変換します。
4. 状態を移行する
LiveDataの監視をStateFlowの収集またはobserveAsState()に変換します。findViewById/ViewBinding を Compose の状態に置き換えます。- クリックリスナーをラムダパラメーターに変換します。
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 ラムダを使用します |
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が残っていない) - 状態を適切にホイスト済み(ユーザー入力に内部の可変状態を使用していない)
- クリックハンドラーをラムダに変換済み
- RecyclerView アダプターを削除済み(LazyColumn/LazyRow を使用)
- ViewBinding/DataBinding を削除済み
- ナビゲーションを統合済み(NavHost または相互運用)
- テーマを適用済み(MaterialTheme)
- アクセシビリティを維持済み(コンテンツの説明、タッチ領域)
- 開発用のプレビューアノテーションを追加済み
- 古い XML ファイルを削除済み
参考資料
コメント
Markdown 対応