独自のアプリケーションを作成する
このチュートリアルではIntelliJ IDEAを使用していますが、Android Studioでも同様に進めることができます。どちらのIDEもコア機能とKotlin Multiplatformサポートは共通しています。
これは「共有ロジックとUIを持つCompose Multiplatformアプリを作成する」チュートリアルの最終パートです。先に進む前に、前の手順を完了していることを確認してください。
ウィザードで作成されたサンプルプロジェクトを探求し、強化したところで、あなたはすでに知っている概念を使い、新しい概念を導入しながら、ゼロから独自のアプリケーションを作成することができます。
ユーザーが国と都市を入力し、アプリがその国の首都の時刻を表示する「ローカル時刻表示アプリケーション」を作成します。Compose Multiplatformアプリのすべての機能は、マルチプラットフォームライブラリを使用して共通コードで実装されます。ドロップダウンメニュー内に画像をロードして表示し、イベント、スタイル、テーマ、モディファイア、レイアウトを使用します。
各ステージで、アプリケーションを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つのコンポーザブルは、
timeAtLocation
プロパティという単一の共有状態によってリンクされています。Text
コンポーザブルはこの状態のオブザーバーです。 Button
コンポーザブルは、onClick
イベントハンドラーを使用して状態を変更します。
- AndroidとiOSでアプリケーションを実行します。
アプリケーションを実行し、ボタンをクリックすると、ハードコードされた時刻が表示されます。
- デスクトップでアプリケーションを実行します。動作しますが、UIに対してウィンドウが明らかに大きすぎます。
これを修正するには、
composeApp/src/desktopMain/kotlin
にあるmain.kt
ファイルを次のように更新します。kotlinfun main() = application { val state = rememberWindowState( size = DpSize(400.dp, 250.dp), position = WindowPosition(300.dp, 300.dp) ) Window( title = "Local Time App", onCloseRequest = ::exitApplication, state = state, alwaysOnTop = true ) { App() } }
ここでは、ウィンドウのタイトルを設定し、
WindowState
型を使用してウィンドウの初期サイズと画面上の位置を設定しています。デスクトップアプリでリアルタイムに変更を見るには、Compose Hot Reloadを使用します。
main.kt
ファイルで、ガターにあるRunアイコンをクリックします。- **Run 'main [desktop]' with Compose Hot Reload (Alpha)**を選択します。
アプリが自動的に更新されるのを見るには、変更されたファイルを保存します( / )。
Compose Hot Reloadは現在アルファ版であり、その機能は変更される可能性があります。
IDEの指示に従って、不足している依存関係をインポートします。
デスクトップアプリケーションを再度実行します。見た目が改善されているはずです。
Compose Hot Reloadのデモ
ユーザー入力をサポートする
次に、ユーザーが都市名を入力してその場所の時刻を確認できるようにします。これを実現する最も簡単な方法は、TextField
コンポーザブルを追加することです。
現在の
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()
関数を作成します。
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()
に似ています。IDEの指示に従って、不足している依存関係をインポートします。
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") } } } }
wasmJsMain/kotlin/main.kt
ファイルで、Web用のタイムゾーンサポートを初期化するために、main()
関数の前に以下のコードを追加します。kotlin@JsModule("@js-joda/timezone") external object JsJodaTimeZoneModule private val jsJodaTz = JsJodaTimeZoneModule
アプリケーションを再度実行し、有効なタイムゾーンを入力します。
ボタンをクリックします。正しい時刻が表示されるはずです。


スタイルを改善する
アプリケーションは動作していますが、見た目に問題があります。コンポーザブルの間隔を適切にし、時刻メッセージをより目立つように表示できます。
これらの問題を解決するには、以下のバージョンの
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の指示に従って、不足している依存関係をインポートします。
Alignment
にはandroidx.compose.ui
バージョンを使用してください。アプリケーションを実行して、見た目がどのように改善されたかを確認します。


デザインをリファクタリングする
アプリケーションは動作していますが、スペルミスが起こりやすいです。たとえば、ユーザーが「France」の代わりに「Franse」と入力した場合、アプリはその入力を処理できません。ユーザーに定義済みリストから国を選択してもらう方が望ましいでしょう。
これを実現するには、
App
コンポーザブルのデザインを変更します。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の指示に従って、不足している依存関係をインポートします。
- アプリケーションを実行して、再設計されたバージョンを確認します。


Koinのような依存性注入フレームワークを使用して、場所のテーブルを構築および注入することで、デザインをさらに改善できます。データが外部に保存されている場合は、Ktorライブラリを使用してネットワーク経由でフェッチするか、SQLDelightライブラリを使用してデータベースからフェッチできます。
画像を導入する
国のリストは動作しますが、視覚的に魅力的ではありません。国名を国旗の画像に置き換えることで改善できます。
Compose Multiplatformは、すべてのプラットフォームで共通コードを通じてリソースにアクセスするためのライブラリを提供しています。Kotlin Multiplatformウィザードは、このライブラリをすでに加えて設定しているため、ビルドファイルを変更することなくリソースのロードを開始できます。
プロジェクトで画像をサポートするには、画像ファイルをダウンロードし、適切なディレクトリに保存し、それらをロードして表示するコードを追加する必要があります。
Flag CDNなどの外部リソースを使用して、すでに作成した国のリストに一致する国旗をダウンロードします。この場合、これらは日本、フランス、メキシコ、インドネシア、エジプトです。
画像を
composeApp/src/commonMain/composeResources/drawable
ディレクトリに移動して、すべてのプラットフォームで同じ国旗が利用できるようにします。
アプリケーションをビルドまたは実行して、追加されたリソースへのアクセサーを含む
Res
クラスを生成します。画像をサポートするために、
commonMain/kotlin/.../App.kt
ファイル内のコードを更新します。kotlinimport compose.project.demo.generated.resources.eg import compose.project.demo.generated.resources.fr import compose.project.demo.generated.resources.id import compose.project.demo.generated.resources.jp import compose.project.demo.generated.resources.mx
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に関するビデオを購読して視聴しましょう