Contents
開発環境のセットアップとプロジェクト設定
Jetpack Compose で実務レベルのカスタムコンポーネントを作るには、安定版の開発ツールと依存関係を正しく構成することが最初のハードルです。本節では、2024 年時点で公式にリリースされている Android Studio と Compose のバージョンを前提に、IDE のインストール手順と Gradle/Kotlin の推奨設定を書き出します。
Android Studio のインストール(Flamingo 2023.1.1)
Android Studio Flamingo 2023.1.1 は公式ダウンロードページから取得できます。インストーラ実行後は次の項目にチェックを入れてください。
- Android SDK – 最新プラットフォームとビルドツールが自動でインストールされます。
- Jetpack Compose 用プラグイン – IDE が
composeDSL を認識し、リアルタイムプレビューやコード補完が有効になります。
IDE 起動時に表示される「Welcome」画面の「Check for updates」を実行すれば、Kotlin 1.9 系と Compose Compiler 2.0 以降が自動的に適用された状態で作業できます。
Gradle と Kotlin のバージョン指定(推奨構成)
Compose 2024 系は Gradle 8.2 以上、Kotlin 1.9.20 以上 が必要です。プロジェクトの settings.gradle.kts に以下を追記し、プラグインと Kotlin のバージョンを統一してください。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
pluginManagement { repositories { google() mavenCentral() } plugins { kotlin("android") version "1.9.20" id("com.android.application") version "8.2.0" // Compose 用プラグインは Android Gradle Plugin に同梱されます } } |
Compose BOM の導入とバージョン統一
BOM(Bill of Materials)を利用すると、Compose のすべてのモジュールが 同一バージョン で解決されるため、個別にバージョン番号を書き分ける手間が省けます。2024 年 6 月時点の最新 BOM は androidx.compose:compose-bom:2024.06.00 です。
|
1 2 3 4 5 6 7 8 9 10 |
dependencies { // Compose BOM(2024.06.00)をプラットフォームとして指定 implementation(platform("androidx.compose:compose-bom:2024.06.00")) // 必要なモジュールだけ列挙 implementation("androidx.compose.ui:ui") implementation("androidx.compose.material3:material3") implementation("androidx.activity:activity-compose") } |
BOM を使用すると、公式ドキュメントで推奨されているバージョン組み合わせと常に一致することが保証されます。
ポイント:Compose のプラグインやコンパイラのバージョンは Android Gradle Plugin が自動で管理しますので、個別に指定しない方が安全です。
@Composable とレイアウトモデルの基礎
この章では 「関数として UI を記述する」 という Compose の根本概念と、カスタムレイアウトを実装するときに必要な 測定(Measure)・配置(Layout) の流れを解説します。概念だけでなく、実際に手を書いたコード例も掲載しているので、すぐに試すことができます。
@Composable の基本構造
@Composable アノテーションは Kotlin コンパイラに対し「この関数は UI ツリーの一部になる」ことを指示します。以下は最小限のサンプルです。
|
1 2 3 4 5 |
@Composable fun Greeting(name: String) { Text(text = "Hello, $name!") } |
- 呼び出し側は
Greeting("Compose")と書くだけで UI が生成されます。 - 関数内部で状態が変化すると、Compose ランタイムが差分だけ再描画します(再コンポーズと呼ばれるプロセス)。
Measure・Layout パスの概要
カスタムレイアウトは Layout コンポーザブルを使って実装します。測定段階で子要素に サイズ要求 を伝え、配置段階で実際の座標を決める二段階プロセスです。
重要ポイント:
measurables.map { it.measure(constraints) }の結果は Placeable というオブジェクトになり、placeRelative(x, y)で配置します。
以下はシンプルな横並びレイアウト(Row 相当)です。コードのコメントに測定と配置のポイントを示しています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
@Composable fun SimpleHorizontal( modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit ) { Layout( content = { Row(content) }, modifier = modifier ) { measurables, constraints -> // 1️⃣ 子要素を測定(子が要求するサイズを取得) val placeables = measurables.map { it.measure(constraints) } // 2️⃣ 横幅は子の合計、縦高さは最大値で決定 val width = placeables.sumOf { it.width } .coerceIn(constraints.minWidth, constraints.maxWidth) val height = placeables.maxOfOrNull { it.height } ?: 0 layout(width, height) { var xPosition = 0 // 3️⃣ 子要素を左から順に配置 placeables.forEach { placeable -> placeable.placeRelative(x = xPosition, y = 0) xPosition += placeable.width } } } } |
この流れを理解すれば、円形メニューやマスグリッドなど 任意の幾何学的レイアウト を自前で実装できます。
状態管理とベストプラクティス
Compose では UI の再利用性は「状態(State)をどこに置くか」で決まります。ここでは remember / mutableStateOf と、派生状態を扱う derivedStateOf の正しい使い分けと、スコープ管理のコツを紹介します。
remember と mutableStateOf の役割
- remember – コンポーズ関数が再実行されてもインスタンスを保持する。主にローカルな UI 状態で使用。
- mutableStateOf – 変更可能な状態オブジェクト。
byデリゲートと組み合わせて書くのが一般的。
|
1 2 3 4 5 6 7 8 |
@Composable fun ClickCounter() { var count by remember { mutableStateOf(0) } // ← ローカル UI 状態 Button(onClick = { count++ }) { Text("Clicked $count 回") } } |
注意:画面遷移や ViewModel で保持したい場合は
rememberSaveableやhiltViewModel()と併用し、プロセスが死んでも状態が失われないようにします。
derivedStateOf で計算コストを削減
派生状態は 元のステートが変化したときだけ再評価 されます。重い計算やリストフィルタリングに最適です。
|
1 2 3 4 5 6 |
@Composable fun ExpensiveResult(input: Int) { val result by derivedStateOf { heavyComputation(input) } Text("Result: $result") } |
heavyComputationが 10ms 程度かかる場合でも、inputが変わらなければ再計算は走りません。derivedStateOfは スナップショットフロー と同様に Compose の再コンポーズサイクルに統合されるため、手動でキャッシュを管理する必要がありません。
スコープ最小化の実践例
以下はリスト項目ごとにローカル状態を持たせ、上位リスト全体の再描画を防ぐパターンです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
@Composable fun TodoItem( todo: Todo, onCheckedChange: (Boolean) -> Unit ) { var checked by remember { mutableStateOf(todo.done) } Row( modifier = Modifier .fillMaxWidth() .clickable { onCheckedChange(!checked) } .padding(16.dp) ) { Checkbox(checked = checked, onCheckedChange = null) // UI だけの状態はここで管理 Spacer(Modifier.width(8.dp)) Text(todo.title, style = MaterialTheme.typography.bodyLarge) } // 親に変更を伝える(副作用) LaunchedEffect(checked) { if (checked != todo.done) onCheckedChange(checked) } } |
rememberのスコープは TodoItem に限定され、リスト全体が再描画されてもこのアイテムだけは保持されます。
カスタムコンポーネントの作成手順
実務で再利用できる UI 部品を作る際に重要なのは API 設計 と Modifier の組み合わせ方 です。この章では、引数設計から Material3 テーマ継承、さらに独自 Layout 実装までのフローを具体例とともに示します。
関数シグネチャとパラメータ設計
必須情報(ビジネスロジック)とオプション設定(見た目・挙動)を分離し、デフォルト値で柔軟性を確保します。以下は「カード型ボタン」のサンプルです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@Composable fun CardButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, backgroundColor: Color = MaterialTheme.colorScheme.primary, contentPadding: PaddingValues = PaddingValues(16.dp) ) { Button( onClick = onClick, enabled = enabled, colors = ButtonDefaults.buttonColors(containerColor = backgroundColor), modifier = modifier, contentPadding = contentPadding ) { Text(text, style = MaterialTheme.typography.bodyLarge) } } |
| パラメータ | 必須/任意 | 目的 |
|---|---|---|
text・onClick |
必須 | ビジネスロジックの入口 |
modifier |
任意 | 呼び出し側がサイズ・配置を上書きできる |
enabled |
任意 | 無効化時の UI/アクセシビリティ制御 |
backgroundColor |
任意 | テーマ外で色調整したいケース向け |
contentPadding |
任意 | 内部余白の微調整 |
Modifier の適用順序と注意点
Modifier は チェーン可能 なオブジェクトです。実装時に順序を誤ると、期待しないレイアウトや描画結果になることがあります。以下は推奨される 「サイズ系 → 形状系 → 描画系 → 入力系」 の順序と、それぞれの代表的メソッドです。
| カテゴリ | 主な Modifier | 効果例 |
|---|---|---|
| サイズ系 | fillMaxWidth(), height(), size() |
コンポーネントの外形寸法を決定 |
| 形状系 | clip(), background() |
角丸や背景色で見た目を整える |
| 描画系 | shadow(), border(), drawBehind{} |
陰影、枠線、カスタム描画 |
| 入力系 | clickable(), pointerInput{} , semantics{} |
タップ領域・アクセシビリティ情報 |
実装例(順序を意識したコード)
|
1 2 3 4 5 6 7 8 9 10 11 12 |
CardButton( text = "開始", onClick = { /* TODO */ }, modifier = Modifier .fillMaxWidth() // 1️⃣ サイズ決定 .height(56.dp) // 同上 .clip(RoundedCornerShape(12.dp)) // 2️⃣ 形状(角丸)適用 .background(MaterialTheme.colorScheme.secondary) // 背景色は shape の後に | .shadow(elevation = 4.dp, shape = RoundedCornerShape(12.dp)) // 3️⃣ 描画系 .clickable { /* click */ } // 4️⃣ 入力系(最後に配置すべき) ) |
- ポイント:
backgroundはclipの後に置くと、角丸が正しく適用された領域だけに色が付く。 clickableを最初に置くと、サイズが決まる前にヒットテストが走り、期待したタップ領域にならないことがあります。
Material3 テーマ・カラー・タイポグラフィの継承
Compose は スコープベース のテーマシステムを提供します。カスタムコンポーネント内部では MaterialTheme.colorScheme や MaterialTheme.typography に直接アクセスでき、アプリ全体のダークモード切替やカラーパレット変更に自動追従します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@Composable fun ThemedTag( label: String, modifier: Modifier = Modifier ) { Text( text = label, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.labelMedium, modifier = modifier .background( color = MaterialTheme.colorScheme.surfaceVariant, shape = RoundedCornerShape(4.dp) ) .padding(horizontal = 8.dp, vertical = 4.dp) ) } |
MaterialThemeは Composable 関数の呼び出し階層 に沿って継承されるため、テスト時にテーマだけ差し替えて UI の見た目を検証できます。
カスタム Layout 実装例(円形メニュー)
標準コンポーネントで実現できない配置は Layout コンポーザブルで自前のロジックを書きます。以下は「等間隔に配置した円形メニュー」の実装です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
@Composable fun CircularMenu( items: List<@Composable () -> Unit>, radius: Dp = 120.dp, modifier: Modifier = Modifier ) { Layout( content = { items.forEach { it() } }, modifier = modifier.size(radius * 2) ) { measurables, constraints -> val placeables = measurables.map { it.measure(constraints) } layout(constraints.maxWidth, constraints.maxHeight) { val centerX = constraints.maxWidth / 2 val centerY = constraints.maxHeight / 2 val angleStep = 360f / placeables.size placeables.forEachIndexed { index, placeable -> val rad = Math.toRadians((angleStep * index).toDouble()) val x = (centerX + radius.toPx() * kotlin.math.cos(rad) - placeable.width / 2).toInt() val y = (centerY + radius.toPx() * kotlin.math.sin(rad) - placeable.height / 2).toInt() placeable.placeRelative(x, y) } } } } |
- 測定は子要素のサイズを取得し、配置では三角関数で円周上に座標変換しています。
radius.toPx()は Dp → ピクセル変換ユーティリティです(with(LocalDensity.current) { radius.toPx() }でも書けます)。
プレビュー・テスト・マルチプラットフォーム展開
Compose コンポーネントは 即時プレビュー と 自動 UI テスト が重要です。さらに、Kotlin Compose Multiplatform (KMP) で iOS や Web に流用する際の注意点もまとめます。
@Preview の活用方法と実装例
Android Studio Flamingo の「Compose Preview」ウィンドウは @Preview アノテーションだけで UI を描画します。デバイスサイズ・テーマ切替を同時に確認できるよう、複数のプレビューを一つの関数に付与します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@Preview( name = "Light – Phone", uiMode = Configuration.UI_MODE_NIGHT_NO, showBackground = true, widthDp = 360, heightDp = 640 ) @Preview( name = "Dark – Tablet", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, widthDp = 800, heightDp = 1280 ) @Composable fun CardButtonPreview() { MaterialTheme(colorScheme = lightColorScheme()) { CardButton( text = "プレビュー", onClick = {} ) } } |
- ポイント:
showBackground = trueを付けると、背景色が自動で描画され視認性が上がります。 - 複数の
@Previewを同一関数に貼ることで、デザインレビューの手間を大幅に削減できます。
Compose UI テストの基本フロー
UI テストは Compose のテストランナー (androidx.compose.ui:ui-test-junit4) と createComposeRule() で実装します。以下はボタンがクリックされたことを検証する最小サンプルです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@get:Rule val composeTestRule = createComposeRule() @Test fun cardButton_click_updatesState() { var clicked = false composeTestRule.setContent { CardButton(text = "タップ", onClick = { clicked = true }) } // テキストが表示されているか確認し、クリックを実行 composeTestRule.onNodeWithText("タップ") .assertIsDisplayed() .performClick() assertTrue(clicked, "onClick が呼び出されていません") } |
semanticsMatcherを併用すると、アクセシビリティ属性(contentDescription など)も同時に検証できます。- 同様のテストコードは KMP の sharedTest ソースセットでも利用可能です(
androidMainとiosMain両方で実行できるよう設定すれば、プラットフォーム間でテストロジックを共有できます)。
Kotlin Compose Multiplatform での流用ポイント
Compose UI は 共通コード として Android・iOS・Web に展開可能ですが、プラットフォーム固有の API は expect/actual で分岐させます。以下に主な注意点をまとめました。
| プラットフォーム | 主な差分実装例 | 補足 |
|---|---|---|
| iOS | actual fun Modifier.clickable(onClick: () -> Unit) = this.then(ClickableModifier(onClick))(UIKit のタップジェスチャにブリッジ) |
公式ライブラリ compose-uikit が提供する ComposeViewController をエントリポイントとして使用 |
| Web | actual fun Modifier.pointerHoverIcon(icon: Cursor) = this(HTML の cursor スタイルへ変換) |
compose-html / compose-web パッケージの Modifier.style { property("cursor", "pointer") } が代替手段 |
| Desktop (JVM) | 変更不要 – Android と同じ API が利用可能 | Window コンポーネントでウィンドウサイズやメニューを制御 |
実務的なヒント:KMP プロジェクトでは UI のプレビューが Android Studio に限定されるため、iOS は Xcode シミュレータ・実機、Web は Chrome/Edge で手動確認するフローを CI に組み込むと品質が保てます。
パフォーマンス最適化チェックリスト
| 手法 | 実装上のポイント |
|---|---|
| remember のスコープ最小化 | 状態は可能な限りローカル @Composable に閉じ込め、上位関数での再描画を防ぐ |
| derivedStateOf の活用 | 高価な計算やフィルタリングは派生状態にキャッシュし、入力が変化したときだけ再評価 |
| Lazy 系コンポーネント | LazyColumn / LazyRow は表示領域外の項目を描画しないので、大量データでもスムーズ |
| key 指定で安定 ID | items(key = { it.id }) とすると、リスト再配置時に状態が保持されアニメーションが滑らかになる |
| Modifier の順序最適化 | サイズ系 → 形状系 → 描画系 → 入力系の順で書くと、無駄なレイアウト計算やヒットテストを回避できる |
これらのベストプラクティスを組み合わせれば、Compose のデクララティブ特性を活かしつつ FPS 30+ を維持した快適 UI が実現できます。
まとめ
- 開発環境:Android Studio Flamingo 2023.1.1、Gradle 8.2、Kotlin 1.9.20、Compose BOM 2024.06.00 を組み合わせると、公式が保証する安定構成で作業を開始できる。
- @Composable の基礎:関数として UI を記述し、Measure‑Layout パスを理解すれば任意のレイアウトロジックが実装可能になる。
- 状態管理:
rememberとmutableStateOfでローカルステートを保持し、derivedStateOfで計算コストを抑える。スコープは最小限に保ち、ViewModel との連携はrememberSaveableや Hilt を活用する。 - コンポーネント設計:必須パラメータとデフォルト Modifier を分離し、Material3 テーマを継承させることで再利用性とテーマ適応性が向上する。Modifier は サイズ → 形状 → 描画 → 入力 の順序でチェーンすると予期せぬレイアウトバグを防げる。
- プレビュー・テスト:複数の
@Previewでデバイス・テーマを網羅し、Compose UI テストでクリックやアクセシビリティ属性を検証する。KMP の共有テストコードに組み込めば iOS/Web でも同様の品質基準が保てる。 - マルチプラットフォーム:共通 UI は
compose.ui系でそのまま流用でき、iOS と Web はexpect/actualによる差分実装と公式ライブラリ(compose-uikit・compose-web)を組み合わせて対応。
以上の手順とベストプラクティスに従えば、公式ドキュメント通りかつ実務で即戦力となるカスタムコンポーネント を作成でき、プロジェクト全体の UI 品質向上につながります。ぜひ本記事をリファレンスとして、あなたのアプリに合わせた独自部品開発に挑戦してください。