Skip to content

独自のアプリケーションを作成する

このチュートリアルではIntelliJ IDEAを使用していますが、Android Studioでも同様に進めることができます。どちらのIDEもコア機能とKotlin Multiplatformサポートは共通しています。


これは「共有ロジックとUIを持つCompose Multiplatformアプリを作成する」チュートリアルの最終パートです。先に進む前に、前の手順を完了していることを確認してください。

最初のステップ

Compose Multiplatformアプリを作成する
このチュートリアルではIntelliJ IDEAを使用していますが、Android Studioでも同様に進めることができます。どちらのIDEもコア機能とKotlin Multiplatformサポートは共通しています。これは共有ロジックとUIを持つCompose Multiplatformアプリを作成するチュートリアルの最初のパートです。Compose Multiplatformアプリを作成する コンポーザブルコードを探る プロジェクトを修正する 独自のアプリケーションを作成する

2番目のステップ
コンポーザブルコードを探る
このチュートリアルではIntelliJ IDEAを使用していますが、Android Studioでも同様に進めることができます。どちらのIDEもコア機能とKotlin Multiplatformサポートは共通しています。これは共有ロジックとUIを持つCompose Multiplatformアプリを作成するチュートリアルの2番目のパートです。先に進む前に、前の手順を完了していることを確認してください。Compose Multiplatformアプリを作成する コンポーザブルコードを探る プロジェクトを修正する 独自のアプリケーションを作成する

3番目のステップ
プロジェクトを修正する
このチュートリアルではIntelliJ IDEAを使用していますが、Android Studioでも同様に進めることができます。どちらのIDEもコア機能とKotlin Multiplatformサポートは共通しています。これは共有ロジックとUIを持つCompose Multiplatformアプリを作成するチュートリアルの3番目のパートです。先に進む前に、前の手順を完了していることを確認してください。Compose Multiplatformアプリを作成する コンポーザブルコードを探る プロジェクトを修正する 独自のアプリケーションを作成する

4番目のステップ 独自のアプリケーションを作成する

ウィザードで作成されたサンプルプロジェクトを探求し、強化したところで、あなたはすでに知っている概念を使い、新しい概念を導入しながら、ゼロから独自のアプリケーションを作成することができます。

ユーザーが国と都市を入力し、アプリがその国の首都の時刻を表示する「ローカル時刻表示アプリケーション」を作成します。Compose Multiplatformアプリのすべての機能は、マルチプラットフォームライブラリを使用して共通コードで実装されます。ドロップダウンメニュー内に画像をロードして表示し、イベント、スタイル、テーマ、モディファイア、レイアウトを使用します。

各ステージで、アプリケーションを3つのプラットフォーム(iOS、Android、デスクトップ)すべてで実行することも、ニーズに最適な特定のプラットフォームに集中することもできます。

プロジェクトの最終状態は、GitHubリポジトリで確認できます。

基礎を構築する

まず、新しいAppコンポーザブルを実装します。

  1. 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イベントハンドラーを使用して状態を変更します。
  1. AndroidとiOSでアプリケーションを実行します。

AndroidとiOSでの新しいCompose Multiplatformアプリ

アプリケーションを実行し、ボタンをクリックすると、ハードコードされた時刻が表示されます。

  1. デスクトップでアプリケーションを実行します。動作しますが、UIに対してウィンドウが明らかに大きすぎます。

デスクトップでの新しいCompose Multiplatformアプリ

  1. これを修正するには、composeApp/src/desktopMain/kotlinにあるmain.ktファイルを次のように更新します。

    kotlin
    fun 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を使用します。

    1. main.ktファイルで、ガターにあるRunアイコンをクリックします。
    2. **Run 'main [desktop]' with Compose Hot Reload (Alpha)**を選択します。 ガターからCompose Hot Reloadを実行する

    アプリが自動的に更新されるのを見るには、変更されたファイルを保存します( / )。

    Compose Hot Reloadは現在アルファ版であり、その機能は変更される可能性があります。

  2. IDEの指示に従って、不足している依存関係をインポートします。

  3. デスクトップアプリケーションを再度実行します。見た目が改善されているはずです。

デスクトップ上のCompose Multiplatformアプリの改善された外観

Compose Hot Reloadのデモ

Compose Hot Reload

ユーザー入力をサポートする

次に、ユーザーが都市名を入力してその場所の時刻を確認できるようにします。これを実現する最も簡単な方法は、TextFieldコンポーザブルを追加することです。

  1. 現在の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")
                }
            }
        }
    }

    新しいコードは、TextFieldlocationプロパティの両方を追加します。ユーザーがテキストフィールドに入力すると、onValueChangeイベントハンドラーを使用してプロパティの値が段階的に更新されます。

  2. IDEの指示に従って、不足している依存関係をインポートします。

  3. ターゲットとする各プラットフォームでアプリケーションを実行します。

AndroidとiOSにおけるCompose Multiplatformアプリのユーザー入力
デスクトップにおけるCompose Multiplatformアプリのユーザー入力

時刻を計算する

次のステップは、与えられた入力を使用して時刻を計算することです。これを行うには、currentTimeAt()関数を作成します。

  1. App.ktファイルに戻り、以下の関数を追加します。

    kotlin
    fun 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()に似ています。

  2. IDEの指示に従って、不足している依存関係をインポートします。

  3. 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")
               }
           }
       }
    }
  4. wasmJsMain/kotlin/main.ktファイルで、Web用のタイムゾーンサポートを初期化するために、main()関数の前に以下のコードを追加します。

    kotlin
    @JsModule("@js-joda/timezone")
    external object JsJodaTimeZoneModule
    
    private val jsJodaTz = JsJodaTimeZoneModule
  5. アプリケーションを再度実行し、有効なタイムゾーンを入力します。

  6. ボタンをクリックします。正しい時刻が表示されるはずです。

AndroidとiOSにおけるCompose Multiplatformアプリの時刻表示
デスクトップにおけるCompose Multiplatformアプリの時刻表示

スタイルを改善する

アプリケーションは動作していますが、見た目に問題があります。コンポーザブルの間隔を適切にし、時刻メッセージをより目立つように表示できます。

  1. これらの問題を解決するには、以下のバージョンの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の周囲全体、およびButtonTextFieldの上部にパディングを追加します。
    • Textコンポーザブルは利用可能な水平方向のスペースを埋め、そのコンテンツを中央に配置します。
    • styleパラメーターは、Textの見た目をカスタマイズします。
  2. IDEの指示に従って、不足している依存関係をインポートします。 Alignmentにはandroidx.compose.uiバージョンを使用してください。

  3. アプリケーションを実行して、見た目がどのように改善されたかを確認します。

AndroidとiOSにおけるCompose Multiplatformアプリの改善されたスタイル
デスクトップにおけるCompose Multiplatformアプリの改善されたスタイル

デザインをリファクタリングする

アプリケーションは動作していますが、スペルミスが起こりやすいです。たとえば、ユーザーが「France」の代わりに「Franse」と入力した場合、アプリはその入力を処理できません。ユーザーに定義済みリストから国を選択してもらう方が望ましいでしょう。

  1. これを実現するには、Appコンポーザブルのデザインを変更します。

    kotlin
    data 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()関数がリストを提供します。
  • TextFieldDropdownMenuに置き換えられました。showCountriesプロパティの値がDropdownMenuの表示を決定します。各国にはDropdownMenuItemがあります。
  1. IDEの指示に従って、不足している依存関係をインポートします。
  2. アプリケーションを実行して、再設計されたバージョンを確認します。
AndroidとiOSにおけるCompose Multiplatformアプリの国リスト
デスクトップにおけるCompose Multiplatformアプリの国リスト

Koinのような依存性注入フレームワークを使用して、場所のテーブルを構築および注入することで、デザインをさらに改善できます。データが外部に保存されている場合は、Ktorライブラリを使用してネットワーク経由でフェッチするか、SQLDelightライブラリを使用してデータベースからフェッチできます。

画像を導入する

国のリストは動作しますが、視覚的に魅力的ではありません。国名を国旗の画像に置き換えることで改善できます。

Compose Multiplatformは、すべてのプラットフォームで共通コードを通じてリソースにアクセスするためのライブラリを提供しています。Kotlin Multiplatformウィザードは、このライブラリをすでに加えて設定しているため、ビルドファイルを変更することなくリソースのロードを開始できます。

プロジェクトで画像をサポートするには、画像ファイルをダウンロードし、適切なディレクトリに保存し、それらをロードして表示するコードを追加する必要があります。

  1. Flag CDNなどの外部リソースを使用して、すでに作成した国のリストに一致する国旗をダウンロードします。この場合、これらは日本フランスメキシコインドネシアエジプトです。

  2. 画像をcomposeApp/src/commonMain/composeResources/drawableディレクトリに移動して、すべてのプラットフォームで同じ国旗が利用できるようにします。

Compose Multiplatformリソースのプロジェクト構造

  1. アプリケーションをビルドまたは実行して、追加されたリソースへのアクセサーを含むResクラスを生成します。

  2. 画像をサポートするために、commonMain/kotlin/.../App.ktファイル内のコードを更新します。

    kotlin
    import 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`オブジェクトを必要とします。
  1. IDEの指示に従って、不足している依存関係をインポートします。
  2. アプリケーションを実行して、新しい動作を確認します。
AndroidとiOSにおけるCompose Multiplatformアプリの国旗
デスクトップにおけるCompose Multiplatformアプリの国旗

プロジェクトの最終状態は、GitHubリポジトリで確認できます。

次のステップ

マルチプラットフォーム開発をさらに探求し、より多くのプロジェクトを試すことをお勧めします。

コミュニティに参加しましょう: