独自のアプリケーションを作成する
このチュートリアルでは IntelliJ IDEA を使用しますが、Android Studio でも同様に進めることができます。どちらの IDE も同じコア機能と Kotlin Multiplatform サポートを共有しています。
これは、「共有のロジックと UI を使用した Compose Multiplatform アプリの作成」チュートリアルの最終パートです。先に進む前に、前のステップを完了していることを確認してください。
ウィザードによって作成されたサンプルプロジェクトを探索し、機能強化したところで、既知のコンセプトを活用し、いくつかの新しい要素を導入しながら、独自のアプリケーションを一から作成してみましょう。
ここでは、ユーザーが国と都市を入力すると、その国の首都の時刻を表示する「ローカル時刻アプリケーション」を作成します。Compose Multiplatform アプリのすべての機能は、マルチプラットフォームライブラリを使用して共通コード(common code)で実装されます。ドロップダウンメニュー内での画像の読み込みと表示、イベント、スタイル、テーマ、修飾子(modifiers)、レイアウトを使用します。
各段階で、3つのプラットフォームすべて(iOS、Android、デスクトップ)でアプリケーションを実行することも、ニーズに最も適した特定のプラットフォームに集中することもできます。
プロジェクトの最終状態は、こちらの GitHub リポジトリ で確認できます。
土台を作る
まずは、新しい App() コンポーザブルを実装します。
composeApp/src/commonMain/kotlinでApp.ktファイルを開き、コードを以下のApp()コンポーザブルに置き換えます。kotlin@Composable @Preview fun App() { MaterialTheme { var timeAtLocation by remember { mutableStateOf("No location selected") } Column( modifier = Modifier .safeContentPadding() .fillMaxSize(), ) { Text(timeAtLocation) Button(onClick = { timeAtLocation = "13:30" }) { Text("Show Time At Location") } } } }- レイアウトは2つのコンポーザブルを含むカラム(Column)です。1つ目は
Textコンポーザブル、2つ目はButtonです。 - これら2つのコンポーザブルは、単一の共有状態(shared state)である
timeAtLocationプロパティによってリンクされています。Textコンポーザブルはこの状態のオブザーバーです。 Buttonコンポーザブルは、onClickイベントハンドラーを使用して状態を変更します。
- レイアウトは2つのコンポーザブルを含むカラム(Column)です。1つ目は
Android と iOS でアプリケーションを実行します。

アプリケーションを実行してボタンをクリックすると、ハードコードされた時刻である 13:30 が表示されます。
desktopApp [hot] 🔥 実行構成を開始して、Compose ホットリロード を使用してデスクトップでアプリケーションを実行します。 アプリは動作しますが、ウィンドウが UI に対して明らかに不釣り合いに見えます。

これを修正するために、
composeApp/src/jvmMain/kotlin(またはプロジェクト構造に応じた適切なディレクトリ)にあるmain.ktファイルを次のように更新します。kotlinfun main() = application { val state = rememberWindowState( size = DpSize(400.dp, 350.dp), position = WindowPosition(300.dp, 300.dp) ) Window( title = "Local Time App", onCloseRequest = ::exitApplication, state = state, alwaysOnTop = true ) { App() } }ここでは、ウィンドウのタイトルを設定し、
WindowState型を使用してウィンドウの初期サイズと画面上の位置を指定しています。IDE の指示に従って、不足している依存関係をインポートします。
アプリが自動的に更新されるのを確認するには、変更したファイルを保存します( / )。見た目が改善されるはずです。


ユーザー入力をサポートする
次に、ユーザーが都市の名前を入力して、その場所の時刻を確認できるようにしましょう。これを実現する最も簡単な方法は、TextField コンポーザブルを追加することです。
commonMain/kotlin/compose.project.demo/App.kt内の現在のApp()実装を、以下のものに置き換えます。kotlin@Composable @Preview fun App() { MaterialTheme { var location by remember { mutableStateOf("Europe/Paris") } var timeAtLocation by remember { mutableStateOf("No location selected") } Column( modifier = Modifier .safeContentPadding() .fillMaxSize(), ) { Text(timeAtLocation) TextField(value = location, onValueChange = { location = it }) Button(onClick = { timeAtLocation = "13:30" }) { Text("Show Time At Location") } } } }新しいコードでは、
TextFieldとlocationプロパティの両方が追加されています。ユーザーがテキストフィールドに入力すると、onValueChangeイベントハンドラーを使用してプロパティの値が段階的に更新されます。IDE の指示に従って、不足している依存関係をインポートします。
ターゲットとしている各プラットフォームでアプリケーションを実行します。表示される時刻はまだハードコードされていますが、テキストフィールドにタイムゾーンを入力できるようになりました。



時刻を計算する
次のステップは、入力された情報を使用して時刻を計算することです。これを行うには、currentTimeAt() 関数を作成します。
composeApp/src/commonMain/kotlin/compose.project.demo/App.ktファイルに戻り、以下の関数を追加します。kotlinfun currentTimeAt(location: String): String? { fun LocalTime.formatted() = "$hour:$minute:$second" return try { val time = Clock.System.now() val zone = TimeZone.of(location) val localTime = time.toLocalDateTime(zone).time "The time in $location is ${localTime.formatted()}" } catch (ex: IllegalTimeZoneException) { null } }この関数は、以前に作成した(現在は不要な)
todaysDate()と似ています。kotlinx-datetime ライブラリがまだプロジェクトに追加されていない場合は、新しい依存関係を追加する セクションの指示に従ってください。
IDE の指示に従って、不足している依存関係をインポートします。
Clockクラスはkotlinx.datetimeではなく、kotlin.timeからインポートするようにしてください。Appコンポーザブルを調整して、currentTimeAt()を呼び出すようにします。kotlin@Composable @Preview fun App() { MaterialTheme { var location by remember { mutableStateOf("Europe/Paris") } var timeAtLocation by remember { mutableStateOf("No location selected") } Column( modifier = Modifier .safeContentPadding() .fillMaxSize() ) { Text(timeAtLocation) TextField(value = location, onValueChange = { location = it }) Button(onClick = { timeAtLocation = currentTimeAt(location) ?: "Invalid Location" }) { Text("Show Time At Location") } } } }アプリケーションを再度実行し、有効なタイムゾーンを入力します。
ボタンをクリックします。正しい時刻が表示されるはずです。



スタイルを改善する
アプリケーションは動作していますが、見た目に問題があります。コンポーザブルの間隔を広げ、時刻のメッセージをもっと目立たせることができます。
これらの問題に対処するために、以下のバージョンの
Appコンポーザブルを使用します。kotlin@Composable @Preview fun App() { MaterialTheme { var location by remember { mutableStateOf("Europe/Paris") } var timeAtLocation by remember { mutableStateOf("No location selected") } Column( modifier = Modifier .padding(20.dp) .safeContentPadding() .fillMaxSize(), ) { Text( timeAtLocation, style = TextStyle(fontSize = 20.sp), textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth().align(Alignment.CenterHorizontally) ) TextField( value = location, onValueChange = { location = it }, modifier = Modifier.padding(top = 10.dp) ) Button( onClick = { timeAtLocation = currentTimeAt(location) ?: "Invalid Location" }, modifier = Modifier.padding(top = 10.dp) ) { Text("Show Time") } } } }modifierパラメーターは、Columnの周囲全体と、ButtonおよびTextFieldの上部にパディングを追加します。Textコンポーザブルは、利用可能な水平スペースを埋め、コンテンツを中央に配置します。styleパラメーターは、Textの外観をカスタマイズします。
IDE の指示に従って、不足している依存関係をインポートします。
アプリケーションを実行して、見た目がどのように改善されたかを確認します。



デザインをリファクタリングする
アプリケーションは動作しますが、タイポ(打ち間違い)に弱いです。例えば、ユーザーが "France" の代わりに "Franse" と入力すると、アプリはその入力を処理できません。あらかじめ定義されたリストから国を選択するようにユーザーに求める方が望ましいでしょう。
これを実現するために、
App()コンポーザブルとcurrentTimeAt()関数を更新し、補助的なデータクラスを追加します。kotlindata class Country(val name: String, val zone: TimeZone) fun currentTimeAt(location: String, zone: TimeZone): String { fun LocalTime.formatted() = "$hour:$minute:$second" val time = Clock.System.now() val localTime = time.toLocalDateTime(zone).time return "The time in $location is ${localTime.formatted()}" } fun countries() = listOf( Country("Japan", TimeZone.of("Asia/Tokyo")), Country("France", TimeZone.of("Europe/Paris")), Country("Mexico", TimeZone.of("America/Mexico_City")), Country("Indonesia", TimeZone.of("Asia/Jakarta")), Country("Egypt", TimeZone.of("Africa/Cairo")), ) @Composable @Preview fun App(countries: List<Country> = countries()) { MaterialTheme { var showCountries by remember { mutableStateOf(false) } var timeAtLocation by remember { mutableStateOf("No location selected") } Column( modifier = Modifier .padding(20.dp) .safeContentPadding() .fillMaxSize(), ) { Text( timeAtLocation, style = TextStyle(fontSize = 20.sp), textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth().align(Alignment.CenterHorizontally) ) Row(modifier = Modifier.padding(start = 20.dp, top = 10.dp)) { DropdownMenu( expanded = showCountries, onDismissRequest = { showCountries = false } ) { countries().forEach { (name, zone) -> DropdownMenuItem( text = { Text(name)}, onClick = { timeAtLocation = currentTimeAt(name, zone) showCountries = false } ) } } } Button(modifier = Modifier.padding(start = 20.dp, top = 10.dp), onClick = { showCountries = !showCountries }) { Text("Select Location") } } } }- 名前とタイムゾーンで構成される
Country型があります。 currentTimeAt()関数は、2番目のパラメーターとしてTimeZoneを受け取ります。Appはパラメーターとして国のリストを必要とするようになりました。countries()関数がそのリストを提供します。TextFieldがDropdownMenuに置き換わりました。showCountriesプロパティの値がDropdownMenuの表示を決定します。各国のDropdownMenuItemがあります。
- 名前とタイムゾーンで構成される
IDE の指示に従って、不足している依存関係をインポートします。
Row()をインポートする際は、@Composableバージョンを選択してください。アプリケーションを実行して、再設計されたバージョンを確認します。



Koin などの依存関係注入(Dependency Injection)フレームワークを使用して、場所のテーブルを構築し注入することで、さらにデザインを改善できます。データが外部に保存されている場合は、Ktor ライブラリを使用してネットワーク経由で取得したり、SQLDelight ライブラリを使用してデータベースから取得したりできます。
画像を導入する
国の名前のリストは機能しますが、ユーザー体験としてはあまり良くありません。国の名前の隣に国旗の画像を追加することで、リストを改善できます。
Compose Multiplatform は、すべてのプラットフォームで共通コードを介してリソースにアクセスするためのライブラリを提供しています。Kotlin Multiplatform ウィザードは、すでにこのライブラリを追加して構成しているため、すぐにリソースの読み込みを開始できます。
プロジェクトで画像をサポートするには、画像ファイルをダウンロードし、正しいディレクトリに保存し、それらを読み込んで表示するためのコードを追加する必要があります。
すでに作成した国のリストに合わせて、Flag CDN から国旗の画像をダウンロードします。この例では、日本、フランス、メキシコ、インドネシア、エジプト です。
同じ国旗をすべてのプラットフォームで使用できるように、画像を
composeApp/src/commonMain/composeResources/drawableディレクトリに移動します。
アプリケーションをビルドまたは実行して、追加されたリソースへのアクセサーを備えた
Resクラスを生成します。画像をサポートするように
commonMain/kotlin/.../App.ktファイルのコードを更新します。kotlinimport demo.composeapp.generated.resources.jp import demo.composeapp.generated.resources.mx import demo.composeapp.generated.resources.eg import demo.composeapp.generated.resources.fr import demo.composeapp.generated.resources.id data class Country(val name: String, val zone: TimeZone, val image: DrawableResource) fun currentTimeAt(location: String, zone: TimeZone): String { fun LocalTime.formatted() = "$hour:$minute:$second" val time = Clock.System.now() val localTime = time.toLocalDateTime(zone).time return "The time in $location is ${localTime.formatted()}" } val defaultCountries = listOf( Country("Japan", TimeZone.of("Asia/Tokyo"), Res.drawable.jp), Country("France", TimeZone.of("Europe/Paris"), Res.drawable.fr), Country("Mexico", TimeZone.of("America/Mexico_City"), Res.drawable.mx), Country("Indonesia", TimeZone.of("Asia/Jakarta"), Res.drawable.id), Country("Egypt", TimeZone.of("Africa/Cairo"), Res.drawable.eg) ) @Composable @Preview fun App(countries: List<Country> = defaultCountries) { MaterialTheme { var showCountries by remember { mutableStateOf(false) } var timeAtLocation by remember { mutableStateOf("No location selected") } Column( modifier = Modifier .padding(20.dp) .safeContentPadding() .fillMaxSize(), ) { Text( timeAtLocation, style = TextStyle(fontSize = 20.sp), textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth().align(Alignment.CenterHorizontally) ) Row(modifier = Modifier.padding(start = 20.dp, top = 10.dp)) { DropdownMenu( expanded = showCountries, onDismissRequest = { showCountries = false } ) { countries.forEach { (name, zone, image) -> DropdownMenuItem( text = { Row(verticalAlignment = Alignment.CenterVertically) { Image( painterResource(image), modifier = Modifier.size(50.dp).padding(end = 10.dp), contentDescription = "$name flag" ) Text(name) } }, onClick = { timeAtLocation = currentTimeAt(name, zone) showCountries = false } ) } } } Button(modifier = Modifier.padding(start = 20.dp, top = 10.dp), onClick = { showCountries = !showCountries }) { Text("Select Location") } } } }Country型には関連する画像へのパスが格納されます。Appに渡される国のリストには、これらのパスが含まれます。Appは、各DropdownMenuItemにImageを表示し、その後に国の名前を表示するTextコンポーザブルを表示します。- 各
Imageは、データを取得するためにPainterオブジェクトを必要とします。
IDE の指示に従って、不足している依存関係をインポートします。
アプリケーションを実行して、新しい動作を確認します。



プロジェクトの最終状態は、こちらの GitHub リポジトリ で確認できます。
次のステップ
マルチプラットフォーム開発をさらに探索し、他のプロジェクトも試してみることをお勧めします。
- 既存の Android アプリをクロスプラットフォーム化する
- Ktor と SQLDelight を使用してマルチプラットフォームアプリを作成する
- UI をネイティブに保ちながら iOS と Android でビジネスロジックを共有する
- Kotlin/Wasm を使用して Compose Multiplatform アプリを作成する
- 厳選されたサンプルプロジェクトのリストを見る
コミュニティに参加しましょう:
Compose Multiplatform GitHub: リポジトリにスターを付け、貢献してください。
Kotlin Slack: 招待を受けて、#multiplatform チャンネルに参加してください。
Stack Overflow: "kotlin-multiplatform" タグを購読してください。
Kotlin YouTube チャンネル: Kotlin Multiplatform に関する動画を購読して視聴してください。
