Android開発を変える!Jetpack Compose 入門
はじめに:Android開発の夜明け、そして変革の時
長らくAndroid開発のUI構築は、XMLレイアウトファイルと命令的なJava/Kotlinコードを組み合わせて行うのが主流でした。XMLでUIの構造やスタイルを定義し、アクティビティやフラグメントのコード内でfindViewByIdを使ってXML要素を取得し、そのプロパティを命令的に操作することでUIを更新していました。
この伝統的なアプローチは、多くのAndroidアプリ開発を支えてきましたが、いくつかの課題も抱えていました。
- 複雑なUI構造と管理: XMLレイアウトはネストが深くなりやすく、大規模な画面では管理が複雑になりがちでした。また、UI要素間の依存関係や可視性の制御などもXMLとコードに分散するため、全体像を把握しにくいという問題がありました。
- 煩雑な状態管理: ユーザーのアクションやデータの変更に応じてUIを更新する場合、手動で各Viewのプロパティ(テキスト、色、可視性など)を変更する必要がありました。状態が複雑になるにつれて、どのViewをいつ、どのように更新すべきかを管理するのが非常に難しくなり、意図しないUIの不整合(バグ)を引き起こしやすくなりました。
- ボイラープレートコード:
findViewById
、Adapterクラス、Fragmentトランザクションなど、定型的な記述が多くなりがちで、純粋なUIロジック以外のコードが増える傾向がありました。 - プレビューの限界: XMLレイアウトプレビューは便利でしたが、動的なデータや複雑な状態変化を正確にシミュレートするのは困難でした。
このような課題に対し、GoogleはAndroid開発のUIレイヤーを刷新する全く新しいツールキットとしてJetpack Composeを発表しました。Jetpack Composeは、宣言的なUIプログラミングパラダイムを採用し、Kotlinの力を最大限に活用することで、Android UI開発をより直感的、効率的、そして楽しいものに変える可能性を秘めています。
この記事では、Jetpack Composeの基本から応用までを詳しく解説し、なぜこれがAndroid開発を変えると言われているのか、そしてどのようにしてComposeでの開発を始めるのかをステップバイステップで学んでいきます。約5000語というボリュームで、Composeの核心に迫り、あなたのAndroid開発スキルを次のレベルへと引き上げる手助けをします。
さあ、新しいAndroid開発の世界へ一緒に飛び込みましょう!
1. Jetpack Composeとは? 宣言的UIの世界へようこそ
Jetpack Composeを理解する上で最も重要なキーワードは「宣言的UI (Declarative UI)」です。これは従来の命令的UI (Imperative UI) と対比される考え方です。
命令的UI (従来のAndroid Viewシステム)
従来のXMLベースのAndroid Viewシステムは、命令的UIアプローチを採用していました。
- 「どうやって」UIを作るか指示する: Viewツリーを構築し(XMLで定義)、各Viewをコードで取得し(
findViewById
)、そのプロパティを一つずつ命令的に変更することでUIの状態を更新します。 - 状態はUI要素自身が持つ: UI要素(View)自身が自分の現在の状態(表示されているテキスト、色、チェックされているかなど)を持ちます。
- UI更新の手順が複雑: アプリの状態が変化したときに、どのViewのどのプロパティをどのように変更するかを、開発者が具体的に指示する必要があります。
宣言的UI (Jetpack Compose)
一方、Jetpack Composeは宣言的UIアプローチを採用しています。
- 「何を」表示するか宣言する: アプリの現在の「状態」に基づき、表示したいUIの最終的な姿を宣言します。UI要素の階層やプロパティを、その状態の関数として定義します。
- 状態はUIの外側で管理される: UI要素(Composable)自身は不変であり、状態を持ちません。状態はComposableの外側で管理され、その状態が変更されると、Composeフレームワークが必要なUI要素を再描画(リコンポジション)します。
- UI更新はフレームワークが自動で行う: アプリの状態が変化すると、Composeフレームワークが変更された状態に基づいてUI全体(または変更が必要な部分のみ)を再評価し、自動的にUIを更新します。開発者はUIの「変化の手順」ではなく、「ある状態のときにどう表示されるべきか」を宣言するだけで済みます。
Composeの主要な特徴
宣言的UIであることに加えて、Jetpack Composeには以下のような特徴があります。
- Kotlinベース: Composeは100% Kotlinで記述されており、Kotlinの強力な機能(コルーチン、拡張関数、DSLなど)を最大限に活用できます。XMLを記述する必要はありません。
- コンポーネント指向: UIは小さな再利用可能な部品(Composable関数)の組み合わせとして構築されます。これにより、UIの構造がモジュール化され、管理しやすくなります。
- 高速な開発サイクル: プレビュー機能や、コード変更が即座に反映されるLive Editなどにより、UIの試行錯誤が容易になり、開発速度が向上します。
- パフォーマンス: 必要な部分だけを効率的に再描画する「リコンポジション」メカニズムにより、UIのパフォーマンスが高いです。
- Android Jetpackとの統合: Navigation, ViewModel, Roomなどの既存のJetpackライブラリとシームレスに連携します。
- 他のプラットフォームへの展開: Composeの考え方は、デスクトップ(Compose for Desktop)やWeb(Compose for Web/Kotlin/JS)への展開も進んでおり、将来的にはマルチプラットフォームUI開発の可能性も広がっています。
Composeのメリット・デメリット
メリット:
- 開発速度の向上: 直感的で簡潔なコード、高速なプレビュー機能により、UI開発のイテレーションが速まります。
- コード量の削減: XMLとKotlin/Javaを行き来する必要がなくなり、ボイラープレートコードが削減されるため、全体的なコード量が減ります。
- メンテナンス性の向上: UIが状態の関数として記述されるため、コードが理解しやすく、バグの温床となりやすい命令的なUI更新ロジックが不要になります。再利用可能なComposableの作成も容易です。
- 状態管理の改善: 宣言的UIとComposeの状態管理APIにより、UIの状態をより明確かつ安全に管理できます。
- 強力なプレビュー機能: 様々な状態や設定(画面サイズ、言語、ダークテーマなど)でのUI表示を、実機やエミュレーターなしで確認できます。
デメリット:
- 学習コスト: 宣言的UIの考え方や、Compose独自のAPI(State, Modifier, Lifecycleなど)を習得する必要があります。特に、従来の命令的UIに慣れている開発者にとっては、思考の切り替えが必要です。
- 新しいツールとエコシステム: まだ比較的新しい技術であり、既存のライブラリとの互換性や、情報、サンプルコードの量は従来のViewシステムに比べて少ない場合があります(急速に増えています)。
- デバッグ: 初期のうちはリコンポジションの挙動などを理解するのに時間がかかり、デバッグに戸惑うことがあるかもしれません。
全体として、学習コストは存在するものの、それを上回るメリットが多く、今後のAndroid開発の主流になっていくことは間違いありません。
2. 開発環境の準備
Jetpack Composeでの開発を始めるには、Android Studioの特定のバージョンが必要です。
Android Studioの要件
Jetpack ComposeはKotlin Compiler Pluginとして実装されているため、対応したAndroid Studioのバージョンが必要です。通常、最新の安定版、またはComposeの最新機能を利用したい場合はPreview版やBeta版を使用することをおすすめします。
記事執筆時点(2024年)では、Android Studio Hedgehog (2023.1.1) 以降がCompose開発に適しています。特に、Composeのプレビュー機能やデバッグ機能が強化されています。
プロジェクトの作成
Android Studioで新しいプロジェクトを作成する際、以下のテンプレートを選択します。
- Empty Compose Activity: Jetpack Composeを使用する新しいプロジェクトを開始する最も簡単な方法です。最初からComposeの基本的な設定がされています。
プロジェクト作成ウィザードを進める際に、言語としてKotlinが選択されていることを確認してください。
build.gradleの設定
Empty Compose Activityテンプレートを使用した場合、自動的に必要な設定が build.gradle
ファイルに追加されます。しかし、既存のプロジェクトにComposeを導入する場合や、特定のバージョンを指定したい場合は、手動で設定する必要があります。
プロジェクトレベルの build.gradle
(build.gradle (Project: YourAppName)
):
“`gradle
buildscript {
// … その他の設定 …
dependencies {
// Compose Compiler Plugin のバージョンを指定 (Kotlinバージョンに対応したもの)
classpath(“org.jetbrains.kotlin:kotlin-gradle-plugin:YOUR_KOTLIN_VERSION”) // 例: 1.9.20
classpath(“com.android.tools.build:gradle:YOUR_AGP_VERSION”) // 例: 8.2.0
// … その他の dependencies …
}
}
plugins {
id ‘com.android.application’ version ‘YOUR_AGP_VERSION’ apply false // 例: 8.2.0
id ‘com.android.library’ version ‘YOUR_AGP_VERSION’ apply false // 例: 8.2.0
id ‘org.jetbrains.kotlin.android’ version ‘YOUR_KOTLIN_VERSION’ apply false // 例: 1.9.20
// … その他の plugins …
}
``
YOUR_KOTLIN_VERSIONと
YOUR_AGP_VERSION` は使用しているバージョンに合わせてください。Compose CompilerのバージョンはKotlinのバージョンに依存するので注意が必要です。
アプリレベルの build.gradle
(build.gradle (Module: YourAppName.app)
):
“`gradle
plugins {
id ‘com.android.application’
id ‘org.jetbrains.kotlin.android’
}
android {
namespace ‘com.yourcompany.yourappname’ // プロジェクトに合わせて変更
compileSdk 34 // またはそれ以上
defaultConfig {
applicationId 'com.yourcompany.yourappname'
minSdk 24 // ComposeのサポートSDKに合わせて調整
targetSdk 34 // またはそれ以上
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true // <-- Composeを有効にする
}
composeOptions {
kotlinCompilerExtensionVersion 'YOUR_COMPOSE_COMPILER_VERSION' // <-- Kotlinバージョンに対応したCompilerバージョンを指定
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
implementation(“androidx.core:core-ktx:1.12.0”) // 最新版を使用
implementation(“androidx.lifecycle:lifecycle-runtime-ktx:2.6.2”) // 最新版を使用
implementation(“androidx.activity:activity-compose:1.8.1”) // <– ActivityからComposeを起動するために必要
// Compose本体
implementation(platform("androidx.compose:compose-bom:2023.10.00")) // <-- BOM (Bill of Materials) で依存関係のバージョン管理を一元化
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview") // プレビュー機能用
implementation("androidx.compose.material3:material3") // Material Design 3
// Optional dependencies for more features
// implementation("androidx.compose.ui:ui-tooling") // 追加ツール(属性インスペクターなど)
// implementation("androidx.compose.foundation:foundation") // スクロール、キャンバスなど
// implementation("androidx.compose.material:material") // 古いMaterial Design 2
// implementation("androidx.compose.material:material-icons-core") // マテリアルアイコン
// implementation("androidx.compose.material:material-icons-extended") // マテリアルアイコン拡張
// Testing
testImplementation("junit:junit:4.13.2") // 最新版を使用
androidTestImplementation("androidx.test.ext:junit:1.1.5") // 最新版を使用
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") // 最新版を使用
androidTestImplementation(platform("androidx.compose:compose-bom:2023.10.00")) // BOM
androidTestImplementation("androidx.compose.ui:ui-test-junit4") // Composeテスト用
debugImplementation("androidx.compose.ui:ui-tooling") // デバッグ・ツール用
debugImplementation("androidx.compose.ui:ui-test-manifest") // テストマニフェスト用
}
“`
重要な点は以下の通りです。
compileSdk
とtargetSdk
を適切なバージョンに設定します。Composeは比較的新しいAPIを使用するため、高めのバージョンが必要です。minSdk
はComposeがサポートするバージョン(通常はAndroid 5.0 Lollipop / API 21以降ですが、依存関係によって異なります。使用しているComposeのバージョンに合わせて確認してください。多くのComposeライブラリはAPI 21以上を要求しますが、Material 3などはAPI 24以上を推奨することがあります)。buildFeatures { compose true }
でComposeを有効にします。composeOptions { kotlinCompilerExtensionVersion 'YOUR_COMPOSE_COMPILER_VERSION' }
でCompose Compilerのバージョンを指定します。このバージョンは使用しているKotlinのバージョンと互換性がなければなりません。Compose公式サイトの「Compose-Kotlin Compatibility Map」で確認できます。- 依存関係として、
androidx.activity:activity-compose
、androidx.compose.ui
、androidx.compose.material3
などが必要です。compose-bom
(Bill of Materials) を使うと、Compose関連ライブラリのバージョン管理が容易になります。
設定後、Gradle Syncを実行してください。これでJetpack Compose開発の準備が整いました。
3. 基本的なComposables
Jetpack ComposeのUIは、Composable関数と呼ばれる特別な関数で構築されます。
Composable関数とは?
@Composable
アノテーションが付与された関数です。- UIの一部を記述します。他のComposable関数を呼び出してUIツリーを構築します。
- 値を返しません。UIの構造を「宣言」するだけで、Viewインスタンスなどを返すわけではありません。
- 副作用(Side-effects)を持つ処理(例: データベースへの書き込み、ネットワークリクエスト、UI状態の更新など)は、特定のAPI(
LaunchedEffect
,rememberCoroutineScope
など)を使って安全に実行する必要があります。後述します。 - 通常の関数とは異なり、Composable関数は任意の順序で、または複数回、あるいは全く実行されない可能性があります(リコンポジションの仕組みによる)。したがって、呼び出し順序や実行回数に依存するロジック(例: グローバル変数の変更)を含めるべきではありません。
MainActivityの基本構造
Empty Compose Activityテンプレートで作成される MainActivity.kt
は、通常以下のようになっています。
“`kotlin
package com.yourcompany.yourappname
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_main) に代わるComposeの開始点
setContent {
YourAppNameTheme { // アプリのテーマを適用 (後述)
// A surface container using the ‘background’ color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
Greeting(“Android”) // 表示するComposableを呼び出し
}
}
}
}
}
// UIの一部を記述するComposable関数
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = “Hello $name!”,
modifier = modifier // Modifiersは引数で渡すのが慣習
)
}
// プレビューを表示するためのComposable関数
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
YourAppNameTheme { // プレビューにもテーマを適用
Greeting(“Android”)
}
}
“`
setContent
ブロックが、このActivityのルートとなるComposableツリーを定義します。ここでは YourAppNameTheme
というテーマを適用し、その中に Surface
と Greeting
Composableを配置しています。
基本的なUI要素 (Composables)
Composeには、テキスト、ボタン、画像などの基本的なUI要素に対応する組み込みのComposableが多数用意されています。これらは通常、Material Designライブラリの一部として提供されます。
-
Text: テキストを表示します。
kotlin
@Composable
fun SimpleText() {
Text("こんにちは、Compose!")
}引数でフォントサイズ、色、スタイルなどを設定できます。
-
Button: クリック可能なボタンを作成します。
kotlin
@Composable
fun SimpleButton() {
Button(onClick = { /* ボタンがクリックされた時の処理 */ }) {
Text("クリックしてね")
}
}onClick
ラムダがボタンが押されたときに実行されます。ボタンの中に別のComposable(例:Text
)を配置することで、ボタンの見た目をカスタマイズできます。 -
Image: 画像を表示します。
“`kotlin
@Composable
fun SimpleImage() {
val imageVector = Icons.Default.Star // マテリアルアイコンを使用する場合
Image(
imageVector = imageVector,
contentDescription = “星のアイコン” // アクセシビリティのために重要
)
}// リソースの画像を使用する場合
@Composable
fun ResourceImage() {
val imagePainter = painterResource(id = R.drawable.my_image) // res/drawable/my_image.png など
Image(
painter = imagePainter,
contentDescription = “私の画像”
)
}
“`painterResource
やImageVector
などを使用して表示する画像をソースを指定します。contentDescription
は、視覚障碍者向けの音声読み上げなどに使用されるため、設定することが強く推奨されます。 -
TextField: テキスト入力フィールドを作成します。
“`kotlin
@Composable
fun SimpleTextField() {
var text by remember { mutableStateOf(“”) } // 状態を管理 (後述)TextField( value = text, // 現在のテキスト値 onValueChange = { newText -> text = newText // テキストが変更されたら状態を更新 }, label = { Text("名前を入力してください") } // ヒントテキスト )
}
“`TextFieldは、入力されたテキストを状態として保持・更新する必要があります。これについては、状態管理のセクションで詳しく解説します。
レイアウトComposables
複数のComposableを画面上に配置するためには、レイアウト用のComposableを使用します。これらは子要素をどのように配置するかを決定します。
-
Column: 子要素を縦一列に配置します。
kotlin
@Composable
fun ColumnExample() {
Column { // デフォルトでは上詰めに配置
Text("最初のアイテム")
Text("二番目のアイテム")
Button(onClick = {}) {
Text("ボタン")
}
}
}verticalArrangement
引数で縦方向の配置(Top, Center, Bottom, SpaceBetween, SpaceAround, SpaceEvenly)を、horizontalAlignment
引数で横方向の配置(Start, CenterHorizontally, End)を指定できます。 -
Row: 子要素を横一列に配置します。
kotlin
@Composable
fun RowExample() {
Row { // デフォルトでは左詰めに配置
Text("左")
Text("中央")
Text("右")
}
}horizontalArrangement
引数で横方向の配置(Start, Center, End, SpaceBetween, SpaceAround, SpaceEvenly)を、verticalAlignment
引数で縦方向の配置(Top, CenterVertically, Bottom)を指定できます。 -
Box: 子要素を重ねて配置します(Z軸方向)。例えば、画像の上にテキストを重ねる場合などに使用します。
kotlin
@Composable
fun BoxExample() {
Box { // デフォルトでは左上詰めに重ねて配置
Image(
painter = painterResource(id = R.drawable.background_image),
contentDescription = "背景画像"
)
Text(
"画像の上に表示されるテキスト",
modifier = Modifier.align(Alignment.Center) // Box内で子要素の位置を指定
)
}
}align
修飾子を使って、Box内で子要素の位置を個別に指定できます。
修飾子 (Modifiers)
Composableの見た目や動作をカスタマイズするために、修飾子 (Modifiers) を使用します。修飾子は、Composable関数に特別な指示(例: サイズ、パディング、マージン、クリック動作など)を与えるためのオブジェクトです。
- 修飾子はドットチェーンで複数連結して使用できます。連結の順番は重要です。
- ほとんどのComposable関数は、Modifier型の引数を持ちます。
kotlin
@Composable
fun ModifierExample() {
Text(
"修飾子の例",
modifier = Modifier
.padding(16.dp) // 内側の余白を16dp追加
.background(Color.Yellow) // 背景色を黄色に
.clickable { /* テキストをクリック可能にする */ } // クリック動作を追加
)
}
よく使う修飾子の例:
Modifier.padding(dp)
: 内側の余白 (パディング)Modifier.size(width, height)
: 要素のサイズを指定Modifier.fillMaxWidth()
: 親要素の幅いっぱいに広がるModifier.fillMaxHeight()
: 親要素の高さいっぱいに広がるModifier.fillMaxSize()
: 親要素のサイズいっぱいに広がるModifier.background(color)
: 背景色Modifier.clickable { ... }
: 要素をクリック可能にするModifier.weight(float)
: RowやColumn内で、余ったスペースを比例配分する(flexibleレイアウト)Modifier.align(alignment)
: BoxやColumn内で、子要素の配置を個別に指定Modifier.border(width, color)
: 境界線を追加
修飾子は非常に強力であり、Compose開発において中心的な役割を果たします。Composablesの引数としてModifierを受け取るように設計することで、そのComposableの再利用性と柔軟性が大幅に向上します。
プレビュー機能
@Preview
アノテーションをComposable関数に付けると、Android StudioのPreviewウィンドウでそのComposableの表示を確認できます。
“`kotlin
@Preview(showBackground = true, name = “シンプルボタンのプレビュー”)
@Composable
fun SimpleButtonPreview() {
// プレビューしたいComposableを呼び出す
SimpleButton()
}
@Preview(showBackground = true, widthDp = 320, heightDp = 480)
@Composable
fun SmallScreenPreview() {
SimpleButton()
}
“`
@Preview
アノテーションには、name
、widthDp
、heightDp
、uiMode
(ダークテーマなど)、locale
などの引数を指定でき、様々な条件下でのUIの表示を確認できます。複数の @Preview
アノテーションを同じComposableに付けることも可能です。
プレビュー機能は、UI開発のイテレーションを劇的に高速化し、様々なデバイスや設定での表示を確認するのに不可欠なツールです。
4. 状態管理 (State Management)
宣言的UIであるJetpack Composeにおいて、状態はUIの表示内容を決定する最も重要な要素です。UIは、アプリケーションの現在の状態を反映する関数として記述されます。状態が変化すると、その状態に依存するComposable関数がリコンポジション (Recomposition) され、UIが必要に応じて更新されます。
なぜ状態管理が重要か
ユーザーの操作(ボタンクリック、テキスト入力など)や外部データの変更(ネットワークからの応答、データベースの更新など)によって、アプリの状態は絶えず変化します。命令的UIでは、これらの変化に応じて影響を受けるViewを特定し、手動で更新する必要がありました。これは、特に状態が複雑に絡み合う大規模なアプリでは非常に困難で、バグの温床となりました。
Composeでは、UIは状態の関数であるため、開発者は「状態 X
のときにはUIをこのように表示する」と宣言するだけで済みます。状態が X'
に変化すれば、Composeフレームワークが自動的にUIを更新して X'
に対応する表示にします。この仕組みを効果的に利用するためには、アプリの状態を適切に管理する必要があります。
Composeにおける状態 (State)
ComposeでUIの状態として追跡したい変数は、State
または MutableState
オブジェクトとして保持します。これらのオブジェクトに格納された値が変更されると、Composeは自動的にその状態を読み取っているComposableを検出し、リコンポジションをスケジュールします。
mutableStateOf()
: 変更可能な状態を作成します。remember
: Composableがリコンポジションされても、状態オブジェクトを保持し続けるために使用します。
例:クリック回数を表示するカウンター
“`kotlin
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue // デリゲートプロパティ用
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue // デリゲートプロパティ用
@Composable
fun Counter() {
// ‘count’ という状態を定義。初期値は 0。
// remember は、この Composable がリコンポジションされても count の状態を保持するために必要。
// mutableStateOf は、値が変更可能であることを示す。
// by キーワード (プロパティデリゲート) を使うと、value プロパティにアクセスせずに count 変数自体を直接扱える。
var count by remember { mutableStateOf(0) }
Column {
Text("クリックされた回数: $count")
Button(onClick = {
count++ // 状態を変更すると、この Composable はリコンポジションされる
}) {
Text("カウントアップ")
}
}
}
“`
この例では、count
が状態です。mutableStateOf(0)
で初期値 0 の変更可能な状態オブジェクトを作成し、remember
でリコンポジション後も状態が維持されるようにしています。by
デリゲートプロパティを使うことで、count.value
ではなく count
変数自体を直接読み書きできるようになり、コードが簡潔になります。
ボタンがクリックされると count++
が実行され、count
の値が変更されます。これは MutableState
の値を変更することになるため、Composeはこの状態を読み取っている Counter
Composable(そしてその中の Text
)をリコンポジションします。結果として、画面に表示されるテキストが自動的に更新されます。
ステートホイスティング (State Hoisting)
上記の Counter
Composableは、自身の状態(count
)を内部で保持しています。これはシンプルな場合は問題ありませんが、状態が複数のComposable間で共有されたり、親のComposableから制御されたりする必要がある場合、問題が発生します。
ステートホイスティングとは、Composableが自身の状態を保持するのではなく、その状態を呼び出し元(親)のComposableに引き上げる(ホイストする)デザインパターンです。
ステートホイスティングを行うと、Composableは以下の2つのものを引数として受け取ります。
- 状態自体: 表示に必要なデータ。
- 状態を変更するイベント: 親に通知するためのラムダ関数。
この原則に従ったComposableはステートレス (Stateless) であると言われます。自身の状態を持たず、外部から与えられた状態を表示し、外部に状態変更のイベントを通知するだけだからです。
ステートホイスティングのメリット:
- 再利用性の向上: ステートレスなComposableは、様々な場所で異なる状態を与えて再利用できます。
- テストの容易性: ステートレスなComposableは、特定の入力(状態とイベント)に対して特定のUIを出力する純粋な関数に近くなるため、テストが容易になります。
- 状態の集中管理: アプリの状態をアプリ構造のより高いレベル(例えばViewModel)で集中管理できます。
例:ステートホイスティングされたカウンター
“`kotlin
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
// ステートレスな Composable。状態とその変更イベントを引数で受け取る。
@Composable
fun StatelessCounter(count: Int, onIncrement: () -> Unit) {
Column {
Text(“クリックされた回数: $count”)
Button(onClick = onIncrement) { // イベントを呼び出し元に通知
Text(“カウントアップ”)
}
}
}
// 状態を保持するステートフルな Composable (呼び出し元)
@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) } // 状態をここで保持
StatelessCounter(
count = count, // 状態を渡す
onIncrement = { count++ } // 状態変更のイベントを渡す
)
}
“`
StatelessCounter
は count
の値を表示し、ボタンが押されたら onIncrement
ラムダを実行するだけです。実際の count
の値と、その値をインクリメントするロジックは CounterScreen
が担当しています。
ほとんどの場合、再利用性を高めるために、可能な限りステートレスなComposableを作成し、状態はそれらを呼び出す親のComposableやViewModelなどで管理することが推奨されます。
ViewModelとの連携
Android開発では、ActivityやFragmentのライフサイクルを超えてデータを保持するためにViewModelを使用するのが一般的です。Jetpack Composeでも、アプリの状態を管理するためにViewModelと連携させることができます。
ViewModelはUIの状態を保持し、ビジネスロジックを実行します。Composeでは、ViewModelが公開する LiveData
や StateFlow
などのストリームを監視し、その値が変更されたときに自動的にUIを更新することができます。
collectAsState()
などの拡張関数を使用すると、Flow
(特に StateFlow
) をComposeの State
に変換し、その値の変更を自動的に追跡できるようになります。
“`kotlin
// ViewModelの例
class CounterViewModel : ViewModel() {
// UIの状態を StateFlow で公開
private val _count = MutableStateFlow(0)
val count: StateFlow
fun incrementCount() {
_count.value++
}
}
// Composableの例
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState // <– FlowをStateに変換する関数
import androidx.compose.runtime.getValue
import androidx.lifecycle.viewmodel.compose.viewModel // <– ViewModelを取得する関数
@Composable
fun CounterScreenWithViewModel(viewModel: CounterViewModel = viewModel()) { // viewModel() でインスタンスを取得/再利用
// ViewModel の StateFlow を Composable の State として収集
val count by viewModel.count.collectAsState()
Column {
Text("クリックされた回数: $count")
Button(onClick = { viewModel.incrementCount() }) { // ViewModel の関数を呼び出す
Text("カウントアップ")
}
}
}
“`
このパターンでは、CounterScreenWithViewModel
はステートレスです。状態(count
)はViewModelから提供され、状態変更のロジック(incrementCount
)もViewModelに委譲しています。これにより、UIコードは表示に集中でき、状態管理やビジネスロジックはViewModelに分離されるため、コードの見通しが良くなり、テストも容易になります。
5. リスト表示 (Lazy Composables)
従来のAndroid開発でリスト表示を行う際は、大量のデータを効率的に表示するためにRecyclerViewを使用しました。RecyclerViewは、画面外にスクロールされたアイテムのViewを再利用することでパフォーマンスを最適化しています。
Jetpack Composeでは、このRecyclerViewに相当する機能としてLazy Composablesが提供されています。LazyColumn
は縦方向のスクロールリスト、LazyRow
は横方向のスクロールリストを実現します。
LazyColumn と LazyRow
Lazy Composableは、画面に表示されている(または表示されようとしている)アイテムだけをComposeし、リサイクルする仕組みを持っています。これにより、リストのアイテム数が非常に多くてもパフォーマンスを維持できます。
基本的な使い方:
“`kotlin
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun SimpleLazyColumn(itemsList: List
LazyColumn {
// items 関数でリストの各要素を Composable として表示
items(itemsList) { item ->
// 各アイテムに対して表示したい Composable を記述
Text(
text = “アイテム: $item”,
modifier = Modifier.padding(8.dp)
)
}
// items 以外にも、単一のアイテムを追加可能
item {
Text(
text = "これはリストの最後のアイテムです",
modifier = Modifier.padding(8.dp)
)
}
}
}
“`
LazyColumn
またはLazyRow
ブロック内で、リストのコンテンツを定義します。items(list)
関数を使うと、指定したリストの各要素に対して引数に指定したラムダ(Composable)が実行されます。ラムダの引数にはリストの各要素が渡されます。item { ... }
ブロックを使うと、リストとは別に単一のComposableを追加できます(例えばヘッダーやフッターなど)。items
関数には、リストだけでなく、要素のインデックスを扱うバリエーションや、要素にユニークなキーを指定できるバリエーションもあります。
RecyclerViewとの比較
特徴 | 従来のRecyclerView | Jetpack Compose Lazy Composables (LazyColumn /LazyRow ) |
---|---|---|
UI構築方法 | XML + Adapter + ViewHolder (命令的) | Composable関数 (宣言的) |
アダプター | 必須 (RecyclerView.Adapter) | 不要 |
ViewHolder | 必須 (Viewを保持し再利用) | 不要 (Composablesは再利用される仕組みを持つ) |
データバインディング | 手動またはData Bindingライブラリ | 状態の変化に応じて自動的に再Composeされる |
リストの更新 | notifyDataSetChanged() , notifyItemInserted() など |
データのリストオブジェクト自体を変更するだけ |
コード量 | アダプター、ViewHolder、XMLレイアウトなどが多い | 少ない |
実装の複雑さ | アダプターの実装、Viewタイプの扱い、状態管理が複雑 | 比較的シンプル |
パフォーマンス | Viewの再利用により最適化 | Composableの再Composeにより最適化 |
カスタマイズ | ItemDecoration, ItemAnimatorなどで複雑 | Modifierやコンテンツ内のComposableで柔軟にカスタマイズ可能 |
Lazy Composablesは、RecyclerViewに比べてコード量が大幅に削減され、実装が直感的になります。リストデータの変更(アイテムの追加、削除、更新)も、渡しているListオブジェクト自体を変更するだけで済みます(状態として保持していれば自動的にUIが更新されます)。これは非常に大きな改善点です。
キー (Keys) の重要性
items
関数には、オプションで各アイテムに対してユニークなキーを指定することができます。
kotlin
import androidx.compose.foundation.lazy.items
// ...
LazyColumn {
items(
items = itemsList,
key = { item -> item.id } // 例えば、データクラスに id フィールドがある場合
) { item ->
// ...
}
}
キーを指定することで、Composeはリスト内のアイテムの追加、削除、移動、変更をより効率的に追跡できます。これにより、リコンポジションの最適化が進み、アニメーション(アイテムの移動など)がよりスムーズになります。リストのアイテムがユニークなIDなどを持っている場合は、キーを指定することが強く推奨されます。
6. テーマ設定とスタイル
Jetpack Composeでは、アプリ全体の見た目(色、タイポグラフィ、シェイプなど)を統一的に管理するために、Material Design 3 をベースとしたテーマシステムが用意されています。
MaterialTheme
アプリのルートとなるComposableで MaterialTheme
Composableを呼び出すことで、その配下にあるComposablesにテーマが適用されます。
“`kotlin
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun YourAppNameTheme(
// ダークテーマかどうかなどを引数で受け取る
darkTheme: Boolean = false, // 例えば、isSystemInDarkTheme() を使う
content: @Composable () -> Unit // テーマを適用したいコンテンツ
) {
// 色スキーム、タイポグラフィ、シェイプを定義
val colors = if (darkTheme) {
DarkColorScheme
} else {
LightColorScheme
}
val typography = Typography
val shapes = Shapes
MaterialTheme(
colorScheme = colors,
typography = typography,
shapes = shapes,
content = content // ここで実際のアプリコンテンツをCompose
)
}
// Activity の setContent で利用
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val isSystemInDarkTheme = isSystemInDarkTheme() // システム設定からダークテーマを取得
YourAppNameTheme(darkTheme = isSystemInDarkTheme) {
Surface(color = MaterialTheme.colorScheme.background) {
// アプリのUIコンテンツをここに記述
}
}
}
}
}
“`
新しいプロジェクトテンプレートでは、ui.theme
パッケージ内に Theme.kt
ファイルが作成され、この YourAppNameTheme
Composableが定義されています。このファイル内で、ライトテーマ・ダークテーマの色、タイポグラフィ、シェイプをカスタマイズします。
- ColorScheme: プライマリ、セカンダリ、テキスタリーなどの色を定義します。ダークテーマ用の
DarkColorScheme
とライトテーマ用のLightColorScheme
を別々に定義できます。 - Typography: テキストのスタイル(フォントファミリー、太さ、サイズなど)を定義します(例:
headlineLarge
,bodyMedium
)。 - Shapes: ボタン、カードなどの要素の角の丸みなどを定義します(例:
small
,medium
,large
)。
これらのテーマ設定は、MaterialTheme.colorScheme
, MaterialTheme.typography
, MaterialTheme.shapes
プロパティを通じて、テーマ配下の任意のComposableからアクセスできます。
kotlin
@Composable
fun ThemedText() {
Text(
"これはテーマのテキストスタイルを使用します",
style = MaterialTheme.typography.bodyMedium, // テーマで定義されたスタイルを適用
color = MaterialTheme.colorScheme.primary // テーマで定義された色を使用
)
}
ダークテーマのサポート
MaterialTheme
の darkTheme
引数を切り替えることで、簡単にダークテーマに対応できます。通常は isSystemInDarkTheme()
関数を使用して、ユーザーのシステム設定に合わせてテーマを切り替えます。
スタイルとカプセル化
特定のUIパターンの見た目を統一したい場合は、そのパターンをカスタムComposableとして抽出し、そこで共通の修飾子やスタイルを適用するのがCompose流のスタイリング方法です。これにより、スタイリングと構造が密接に関連付けられ、変更が容易になります。
7. Navigation (Compose Navigation)
Androidアプリにおける画面遷移は、ユーザー体験の重要な要素です。Jetpack Composeでは、画面遷移を管理するためのライブラリとして Navigation-Compose が提供されています。これは、従来のXMLベースのNavigationコンポーネントの考え方をCompose向けに再設計したものです。
Navigation-Composeの基本的な考え方
- ナビゲーショングラフ (Navigation Graph): アプリ内の画面(Composable)と、それらの間の遷移を定義します。
- NavHostController: ナビゲーションの状態(現在の画面、バックスタックなど)を保持し、画面遷移の操作(例:
navigate
,popBackStack
)を行うオブジェクトです。 - NavHost: 現在の画面(Composable)をナビゲーショングラフに基づいて表示するComposableです。ナビゲーショングラフと
NavHostController
を受け取ります。 - Route (ルート): 各画面を一意に識別するための文字列または型安全なキーです。遷移時に使用します。
依存関係の追加
Navigation-Composeを使用するには、以下の依存関係を build.gradle (app)
に追加します。
gradle
dependencies {
// ... その他の Compose 依存関係 ...
implementation("androidx.navigation:navigation-compose:2.7.5") // 最新版を使用
}
ナビゲーションの実装例
“`kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController // <– NavHostController
import androidx.navigation.compose.NavHost // <– NavHost
import androidx.navigation.compose.composable // <– composable 関数
import androidx.navigation.compose.rememberNavController // <– NavHostController を remember する関数
// 画面を識別するためのルート定義
object Destinations {
const val HOME_ROUTE = “home”
const val DETAIL_ROUTE = “detail/{itemId}” // 引数を含むルート
}
@Composable
fun AppNavigation() {
// NavHostController を作成し、リコンポジション後も状態を保持
val navController = rememberNavController()
// ナビゲーショングラフを定義
NavHost(
navController = navController, // NavHostController を関連付け
startDestination = Destinations.HOME_ROUTE // アプリ起動時の最初の画面
) {
// 各画面 (Composable) とルートを関連付け
composable(Destinations.HOME_ROUTE) {
// Home画面の Composable を呼び出し
HomeScreen(navController = navController)
}
composable(Destinations.DETAIL_ROUTE) { backStackEntry ->
// Detail画面の Composable を呼び出し
// ルートから引数を取得
val itemId = backStackEntry.arguments?.getString("itemId")
DetailScreen(itemId = itemId, navController = navController)
}
// 他の画面も同様に追加
}
}
// Home画面の Composable
@Composable
fun HomeScreen(navController: NavHostController) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(“ホーム画面”)
Button(onClick = {
// Detail画面へ遷移 (ルートを指定)
navController.navigate(“detail/123”) // 引数 “123” を渡す
}) {
Text(“詳細画面へ (ID: 123)”)
}
Button(onClick = {
// Detail画面へ遷移 (別の引数)
navController.navigate(“detail/456”) // 引数 “456” を渡す
}) {
Text(“詳細画面へ (ID: 456)”)
}
}
}
// Detail画面の Composable
@Composable
fun DetailScreen(itemId: String?, navController: NavHostController) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(“詳細画面”)
Text(“アイテムID: ${itemId ?: “不明”}”)
Button(onClick = {
// バックスタックから前の画面に戻る
navController.popBackStack()
}) {
Text(“ホームに戻る”)
}
}
}
“`
rememberNavController()
でNavHostController
のインスタンスを作成します。これは画面遷移の状態を保持するため、remember
を使用します。NavHost
ComposableにnavController
とstartDestination
を指定します。startDestination
はアプリ起動時に最初に表示される画面のルートです。NavHost
ブロック内で、composable()
関数を使用して、各ルートに対応するComposable関数を定義します。composable()
関数にはルート文字列と、そのルートが表示されるときに実行されるラムダを指定します。引数が必要な場合は、ルート文字列に{argumentName}
のようにプレースホルダーを含め、ラムダのbackStackEntry.arguments?.getString("argumentName")
などで取得します。- 画面遷移を行う際は、
navController.navigate("route")
を呼び出します。 - 前の画面に戻る際は、
navController.popBackStack()
を呼び出します。
Navigation-Composeは、従来のFragmentベースのナビゲーションに比べて実装がシンプルになり、Composeの世界で画面遷移を完結できるため、コードの見通しが良くなります。
8. 非同期処理と副作用 (Side-effects)
Composable関数はいつでも、どのような順序で実行されるかわからない、という性質を持ちます。そのため、データベースへの書き込み、ネットワークリクエストの実行、アニメーションの開始、共有状態の更新など、呼び出し回数や順序に依存する「副作用 (Side-effects)」を含む処理を、Composable関数の中で直接実行することは危険です。リコンポジションのたびに意図せず副作用が何度も実行されてしまう可能性があります。
Composeでは、副作用を安全に実行するための専用のAPIが用意されています。
副作用とは?
副作用とは、関数の実行が、その関数の戻り値以外のところに影響を与えるような操作のことです。
- グローバル変数の変更
- ログ出力
- データベースへの書き込み
- ネットワーク呼び出し
- 共有可能な可変状態の変更
- UIの状態以外の状態変更(ViewModelの更新など)
- アニメーションの開始
これらの処理は、特定のイベント(ボタンクリック、画面表示など)が発生したときにのみ実行されるべきであり、リコンポジションのたびに実行されるべきではありません。
副作用を安全に扱うための主なAPI
-
LaunchedEffect
: キーが変更されたときにコルーチンを開始したい場合に使用します。Composableのライフサイクルに関連付けられます。“`kotlin
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect // <– LaunchedEffect
import androidx.compose.ui.tooling.preview.Preview
import kotlinx.coroutines.delay@Composable
fun ShowLoadingMessage(isLoading: Boolean) {
if (isLoading) {
Text(“読み込み中…”)
// isLoading が true になったときにコルーチンを開始し、false になるか Composable が破棄されるまで実行
LaunchedEffect(isLoading) { // isLoading をキーとして指定
delay(3000) // 3秒待機
// 3秒後に何か処理を行う(例: isLoading を false にする)
// 注意: Composable 内で直接状態を変更するのは避けるべき。ViewModel などを介して行う。
}
}
}@Preview(showBackground = true)
@Composable
fun LoadingPreview() {
ShowLoadingMessage(isLoading = true)
}
“`LaunchedEffect
は、指定したキー(この例ではisLoading
)が変更されるたびに内部のブロックをコルーチンとして実行します。キーが変更されずにComposableがリコンポジションされた場合は、コルーチンは継続されます。Composableが画面から消えるなどして破棄されると、コルーチンは自動的にキャンセルされます。 -
rememberCoroutineScope
: UIイベント(ボタンクリックなど)に応じてコルーチンを起動したい場合に使用します。クリックリスナーなどのラムダ内でコルーチンを起動するのに便利です。“`kotlin
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope // <– rememberCoroutineScope
import kotlinx.coroutines.launch // コルーチンを起動@Composable
fun SaveButton(onSave: suspend () -> Unit) {
// ボタンクリックなどのイベントハンドラ内でコルーチンを起動するためのスコープを取得
val coroutineScope = rememberCoroutineScope()Button(onClick = { // ボタンがクリックされたら、コルーチンを起動して非同期処理を実行 coroutineScope.launch { onSave() // 非同期で保存処理を実行 } }) { Text("保存") }
}
“`rememberCoroutineScope
は、Composableのライフサイクルに関連付けられたCoroutineScope
を提供します。このスコープ内でlaunch
を使用してコルーチンを起動すると、Composableが破棄されたときにコルーチンも自動的にキャンセルされるため、メモリリークなどを防ぐことができます。 -
SideEffect
: 副作用ではあるが、リコンポジションが成功した後に一度だけ実行したい処理(例: 分析ログの記録、UI要素のサイズに基づく計算など)。ほとんどの副作用はLaunchedEffect
やrememberCoroutineScope
で扱うべきですが、Viewシステムへのアクセスなど、リコンポジション後のUIの状態に依存する処理でまれに使用します。 DisposableEffect
: リソースの購読/解除、リスナーの登録/解除など、セットアップとクリーンアップが必要な副作用に使用します。キーが変更されたときやComposableが破棄されたときに、クリーンアップ処理が実行されます。produceState
: 非同期データストリームをComposeの状態として公開する場合に使用します。例えば、Flowを収集してStateに変換する代わりに使えます。collectAsState
の方が一般的でシンプルです。
非同期処理や副作用を安全に扱うことは、Composeで安定したアプリを開発する上で非常に重要です。どのAPIを使うべきかは、副作用を実行したいタイミング(画面表示時、イベント発生時など)や、副作用の性質(クリーンアップが必要かなど)によって異なります。
9. 既存のXML Viewとの相互運用
既存のAndroidプロジェクトでJetpack Composeを段階的に導入したい場合や、まだComposeで提供されていない特定の機能(例えば、複雑なカスタムViewや特定のサードパーティライブラリのView)を使用したい場合があります。Jetpack Composeは、従来のAndroid Viewシステムとの相互運用性を考慮して設計されています。
ComposeViewでViewをホストする
既存のXMLレイアウトやカスタムViewの中にComposeコンテンツを埋め込みたい場合は、ComposeView
を使用します。ComposeView
は、従来のView階層の中に配置できる特別なViewで、その中にComposeコンテンツをホストできます。
-
XMLレイアウトファイルに
ComposeView
を配置します。“`xml
<androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view" android:layout_width="match_parent" android:layout_height="match_parent" />
“` -
ActivityまたはFragmentのコードから
ComposeView
を取得し、setContent
メソッドでComposeコンテンツを設定します。“`kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)val composeView = findViewById<ComposeView>(R.id.compose_view) composeView.setContent { // ここに表示したい Compose コンテンツを記述 MaterialTheme { // テーマも忘れずに適用 Column(modifier = Modifier.fillMaxSize()) { Text("これはComposeView内のTextです") Button(onClick = {}) { Text("Composeボタン") } } } } }
}
“`
このように、ComposeView
を使うことで、既存のXMLレイアウトの一部を徐々にComposeに置き換えることができます。
AndroidViewでComposeからViewをホストする
逆に、Jetpack Composeの画面の中に、特定のAndroid View(例えば WebView
、MapView
、広告Viewなど)を配置したい場合は、AndroidView
Composableを使用します。
“`kotlin
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView // <– AndroidView
import android.webkit.WebView // 使用したい View
import android.webkit.WebViewClient // WebView の設定用
@Composable
fun ComposeWithWebView(url: String) {
Column {
Text(“以下はWebViewで表示されるコンテンツです:”)
AndroidView(
factory = { context ->
// ここで View のインスタンスを作成して返す
WebView(context).apply {
settings.javaScriptEnabled = true // 必要に応じて設定
webViewClient = WebViewClient() // リンクをアプリ内で開くなど
loadUrl(url) // URL をロード
}
},
update = { view ->
// State が変更されたときに View を更新する処理
// この例では URL が変わったら再ロード
view.loadUrl(url)
},
modifier = Modifier.fillMaxSize() // Modifier も適用可能
)
}
}
“`
AndroidView
のfactory
ラムダ内で、表示したいViewのインスタンスを作成します。このラムダは一度だけ実行されます。update
ラムダは、AndroidView
が読み取っているState(この例ではurl
)が変更されたときに実行されます。ここでViewのプロパティを更新するなど、命令的な操作を行います。AndroidView
にも Modifier を適用できます。
AndroidView
は、Composeが管理するUIツリーの中に従来のViewツリーを挿入するイメージです。これにより、特定の既存Viewや、まだComposeで代替が難しいViewをCompose画面内にシームレスに組み込むことができます。
これらの相互運用機能は、大規模な既存プロジェクトを段階的にComposeへ移行する際に非常に役立ちます。全く新しいプロジェクトであれば最初から全てComposeで記述するのが理想的ですが、既存プロジェクトの場合はこれらの機能を使ってリスクを抑えながら導入を進めることができます。
10. テスト
Android開発において、UIテストは品質を保証するために不可欠です。Jetpack Composeも、UIをテストするための専用のテストAPIを提供しています。Composeのテストは、JUnit 4フレームワーク上で実行され、特定のComposableが画面上に存在するか、正しいプロパティを持っているか、ユーザーインタラクションに対して正しく反応するかなどを検証できます。
Composeテストの基本
Composeテストは、androidx.compose.ui.test
ライブラリに含まれています。テスト対象のComposableをホストするテストルールと、画面上のComposableノードを検索・検証・操作するAPIを使用します。
-
依存関係の追加: build.gradle (app) に以下の依存関係を追加します。
gradle
dependencies {
// ... その他の依存関係 ...
androidTestImplementation("androidx.compose.ui:ui-test-junit4") // Composeテスト用
debugImplementation("androidx.compose.ui:ui-tooling") // テスト中のUIを表示するため (オプション)
} -
テストクラスの作成:
androidTest
ディレクトリにテストクラスを作成します。JUnit 4のアノテーションを使用します。 -
テストルールの設定: テスト対象のComposableをホストするために
createComposeRule()
を使用します。“`kotlin
import androidx.compose.ui.test.junit4.createComposeRule // <– テストルール
import org.junit.Rule // <– JUnit の Rule アノテーション
import org.junit.Testclass MyComposeTest {
@get:Rule val composeTestRule = createComposeRule() // Composable をホストするルール @Test fun myComposableTest() { // 1. テスト対象の Composable をセットアップ composeTestRule.setContent { // テストしたい Composable を呼び出す // MaterialTheme などを適用するのも忘れずに MyScreen() } // 2. Composable を検索、検証、操作する // ... }
}
“` -
Composableノードの検索:
composeTestRule.onNode()
,composeTestRule.onAllNodes()
メソッドを使用して、画面上の特定のComposable要素(ノード)を検索します。検索条件には、テキスト、コンテンツディスクリプション、テストタグなどが使用できます。“`kotlin
import androidx.compose.ui.test.onNodeWithText // テキストで検索するメソッド
import androidx.compose.ui.test.assertExists // 存在を確認するアサーション// … composeTestRule.setContent { … } の後 …
// テキスト “こんにちは” を持つノードを検索し、存在することを確認
composeTestRule.onNodeWithText(“こんにちは”).assertExists()// コンテンツディスクリプション “画像を説明” を持つノードを検索
composeTestRule.onNodeWithContentDescription(“画像を説明”).assertExists()// Modifier.testTag(“my_button”) が付いたノードを検索
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.assertIsDisplayed // 表示されていることを確認
composeTestRule.onNodeWithTag(“my_button”).assertIsDisplayed()
“` -
検証 (Assertions): 検索したノードに対して、その状態を検証するアサーションを実行します。
“`kotlin
import androidx.compose.ui.test.assertTextEquals // テキスト内容を確認
import androidx.compose.ui.test.assertIsEnabled // 有効 (Enabled) か確認
import androidx.compose.ui.test.assertIsNotEnabled // 無効 (Disabled) か確認
import androidx.compose.ui.test.assertIsSelected // 選択状態か確認composeTestRule.onNodeWithText(“Hello”).assertTextEquals(“Hello”)
composeTestRule.onNodeWithTag(“submit_button”).assertIsEnabled()
“` -
操作 (Actions): 検索したノードに対して、ユーザー操作をシミュレートするアクションを実行します。
“`kotlin
import androidx.compose.ui.test.performClick // クリック操作
import androidx.compose.ui.test.performTextInput // テキスト入力
import androidx.compose.ui.test.performScrollTo // スクロールして表示範囲に入れる// ボタンをクリック
composeTestRule.onNodeWithText(“送信”).performClick()// テキストフィールドにテキストを入力
composeTestRule.onNodeWithTag(“input_field”).performTextInput(“テスト入力”)// LazyColumn/Row のアイテムまでスクロール
composeTestRule.onNodeWithText(“リストの最後のアイテム”).performScrollTo()
“`
テストを書く上でのヒント
- テストタグ (Test Tags) の利用: テキストやコンテンツディスクリプションは変更される可能性がありますが、テストタグはテストのために固定できるため、Composableを検索するのに便利です。
Modifier.testTag("unique_tag")
を使用します。 - ステートレスなComposableのテスト: ステートレスなComposableは、特定の入力に対して特定のUIを出力する純粋な関数に近いため、テストが非常に容易です。可能な限りUIをステートレスなComposableに分解しましょう。
- ViewModelとの連携: ViewModelのテストはUnitTestで行い、ViewModelが公開するStateが変更されたときにUIが正しく更新されるかのテストはComposeテストで行う、といった役割分担が推奨されます。
ComposeテストAPIは直感的で強力であり、宣言的UIの性質と相まって、UIのテストを従来のViewシステムよりも書きやすく、メンテナンスしやすくします。
11. Compose開発のヒントとベストプラクティス
Jetpack Composeでの開発をより効率的かつ効果的に行うためのヒントとベストプラクティスを紹介します。
- Composableは小さく、焦点を絞る: 一つのComposable関数は、一つの明確な役割やUI要素を担当するように設計しましょう。これにより、再利用性が高まり、テストやメンテナンスが容易になります。複雑なUIは、小さなComposableの組み合わせで構築します。
- ステートホイスティングを積極的に活用する: UIの状態は、可能な限りUIツリーの上位、理想的にはViewModelなどの状態ホルダーで管理し、ステートレスなComposableに関数引数として渡しましょう。これにより、UIロジックと状態管理が分離され、Composableの再利用性、テスト容易性、そしてコードの見通しが向上します。
- Modifierを効果的に利用する: Composableの見た目や振る舞いのカスタマイズは、Modifierを通じて行うのがComposeの基本的な考え方です。Modifierは連結順序が重要であることに注意しましょう。カスタムComposableを作成する際は、Modifier型の引数を受け取るように設計し、柔軟性を持たせましょう。
- プレビュー機能を最大限に活用する:
@Preview
アノテーションを使って、様々な状態、画面サイズ、テーマ(ライト/ダーク)、ロケールでのUI表示を頻繁に確認しましょう。これにより、開発速度が向上し、様々な環境でのUIの崩れなどを早期に発見できます。 - リコンポジションを理解する: Composableは状態が変更されたときにリコンポジションされます。リコンポジションの頻繁すぎる実行や、リコンポジション内で重い処理を行うことはパフォーマンスに影響します。リコンポジションを最適化するためには、状態を細かく分割したり、
remember
やderivedStateOf
などのAPIを適切に利用したりします。 - 副作用を安全に扱う: ネットワーク通信、データベース操作、UI状態以外の状態変更など、副作用を伴う処理は、
LaunchedEffect
,rememberCoroutineScope
などの専用APIを使って安全に実行しましょう。Composable関数の中で直接副作用を実行するのは避けてください。 - パフォーマンスプロファイリング: 大規模なアプリや複雑なUIでは、Android StudioのProfilerを使用して、リコンポジションの回数や時間、レイアウト・描画のパフォーマンスなどを確認し、必要に応じて最適化を行いましょう。
- Composeのドキュメントとサンプルコードを参照する: Jetpack Composeはまだ発展途上の技術であり、APIも継続的に更新されています。公式ドキュメントは非常に充実しており、最新の情報や詳細なガイダンスを得るための最良のリソースです。公式サンプルコードも学習に役立ちます。
これらのプラクティスを実践することで、Jetpack Composeのメリットを最大限に引き出し、効率的かつ高品質なAndroidアプリ開発を実現できます。
12. 応用トピック (軽く触れる)
この記事では入門として基本的な概念を中心に解説してきましたが、Jetpack Composeにはさらに多くの機能や応用方法があります。ここではいくつかの応用トピックに軽く触れておきます。
- アニメーション: Composeには、要素の表示・非表示、状態変化、プロパティ値の変更などを滑らかにアニメーションさせるための強力で使いやすいアニメーションAPIが豊富に用意されています(例:
animate*AsState
,AnimatedVisibility
,Crossfade
,rememberInfiniteTransition
)。 - カスタムレイアウト:
Layout
Composableを使用することで、既存のColumn
,Row
,Box
では実現できない独自のレイアウトロジックを実装できます。例えば、円形に要素を配置するレイアウトなどを作成できます。 - グラフィックス (Canvas):
Canvas
ComposableやdrawBehind
,drawWithCache
などのModifierを使用することで、カスタムシェイプの描画、画像処理、グラフの描画など、低レベルのグラフィックス操作を行うことができます。 - Accessibility (アクセシビリティ):
contentDescription
,semantics
Modifierなどを適切に設定することで、音声読み上げやスイッチアクセスなどのアクセシビリティ機能をサポートし、より多くのユーザーがアプリを利用できるようにすることが重要です。Composeはアクセシビリティにも配慮した設計になっています。 - テスト駆動開発 (TDD): Composeのテスト容易性は、TDDのアプローチと非常に相性が良いです。先にテストを記述し、それに合格するようにUIコードを実装するというサイクルを回すことができます。
これらのトピックは、ComposeでのUI開発の可能性をさらに広げ、よりリッチでインタラクティブなユーザー体験を実現するために役立ちます。詳細については、公式ドキュメントやより専門的な記事を参照することをおすすめします。
13. まとめ:Android開発の未来へ
Jetpack Composeは、Android UI開発に革命をもたらす強力なツールキットです。宣言的UIという新しいパラダイムを採用し、Kotlinの優れた機能を活用することで、従来のXMLベースの開発に比べて、開発速度の向上、コード量の削減、メンテナンス性の向上、そしてより直感的で楽しい開発体験を提供します。
確かに、宣言的UIの考え方やCompose独自のAPIに慣れるまでには学習コストがかかります。しかし、一度その考え方を習得すれば、UIの状態管理が格段に容易になり、複雑な画面もコンポーネントとして分解して管理できるようになるなど、多くのメリットを実感できるでしょう。
GoogleはJetpack ComposeをAndroid UI開発の未来として強く推進しており、関連ライブラリやツールも急速に発展しています。既存のXML Viewシステムとの相互運用性も確保されているため、プロジェクト全体を一度に書き換えることなく、段階的に導入することも可能です。
もしあなたがまだJetpack Composeを使ったことがないのであれば、ぜひこの記事を参考に、新しいプロジェクトで試したり、既存プロジェクトの一部に導入したりしてみてください。最初は戸惑うことがあるかもしれませんが、その先にはより効率的で楽しいAndroid開発の世界が待っています。
Android開発の未来は、Jetpack Composeによって形作られていきます。この変革の波に乗り遅れることなく、新しい技術を習得し、より優れたアプリ開発を目指しましょう。
次のステップ:
- Android Studioをインストール・更新し、Compose開発環境を準備する。
- 新しい「Empty Compose Activity」プロジェクトを作成してみる。
- この記事で学んだ基本的なComposables、Modifiers、状態管理の概念を使って簡単なUIを作成してみる。
- 公式のJetpack Composeドキュメントやチュートリアルを読み進める。
- GitHubのJetpack Compose公式サンプルコードを参考に、様々なUIパターンや応用方法を学ぶ。
あなたのComposeによるAndroid開発の旅が、実りあるものになることを願っています!