Skip to content

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

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



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

第1ステップ

Compose Multiplatform アプリを作成する
このチュートリアルでは IntelliJ IDEA を使用しますが、Android Studio でも同様に進めることができます。どちらの IDE も同じコア機能と Kotlin Multiplatform サポートを共有しています。これは「共有のロジックと UI を使用した Compose Multiplatform アプリの作成」チュートリアルの第1パートです。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 アプリのすべての機能は、マルチプラットフォームライブラリを使用して共通コード(common code)で実装されます。ドロップダウンメニュー内での画像の読み込みと表示、イベント、スタイル、テーマ、修飾子(modifiers)、レイアウトを使用します。

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

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

土台を作る

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

  1. composeApp/src/commonMain/kotlinApp.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. Android と iOS でアプリケーションを実行します。

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

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

  3. desktopApp [hot] 🔥 実行構成を開始して、Compose ホットリロード を使用してデスクトップでアプリケーションを実行します。 アプリは動作しますが、ウィンドウが UI に対して明らかに不釣り合いに見えます。

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

  4. これを修正するために、composeApp/src/jvmMain/kotlin(またはプロジェクト構造に応じた適切なディレクトリ)にある main.kt ファイルを次のように更新します。

    kotlin
    fun 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 型を使用してウィンドウの初期サイズと画面上の位置を指定しています。

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

  6. アプリが自動的に更新されるのを確認するには、変更したファイルを保存します( / )。見た目が改善されるはずです。

    デスクトップ上の Compose Multiplatform アプリの小さいウィンドウ

    Compose ホットリロード

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

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

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

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

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

  3. ターゲットとしている各プラットフォームでアプリケーションを実行します。表示される時刻はまだハードコードされていますが、テキストフィールドにタイムゾーンを入力できるようになりました。

Android と iOS の Compose Multiplatform アプリでのユーザー入力
デスクトップの Compose Multiplatform アプリでのユーザー入力
Web の Compose Multiplatform アプリでのユーザー入力

時刻を計算する

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

  1. composeApp/src/commonMain/kotlin/compose.project.demo/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() と似ています。

    kotlinx-datetime ライブラリがまだプロジェクトに追加されていない場合は、新しい依存関係を追加する セクションの指示に従ってください。

  2. IDE の指示に従って、不足している依存関係をインポートします。 Clock クラスは kotlinx.datetime ではなく、kotlin.time からインポートするようにしてください。

  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. アプリケーションを再度実行し、有効なタイムゾーンを入力します。

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

Android と iOS の Compose Multiplatform アプリでの時刻表示
デスクトップの Compose Multiplatform アプリでの時刻表示
Web の 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 の周囲全体と、Button および TextField の上部にパディングを追加します。
    • Text コンポーザブルは、利用可能な水平スペースを埋め、コンテンツを中央に配置します。
    • style パラメーターは、Text の外観をカスタマイズします。
  2. IDE の指示に従って、不足している依存関係をインポートします。

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

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

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

アプリケーションは動作しますが、タイポ(打ち間違い)に弱いです。例えば、ユーザーが "France" の代わりに "Franse" と入力すると、アプリはその入力を処理できません。あらかじめ定義されたリストから国を選択するようにユーザーに求める方が望ましいでしょう。

  1. これを実現するために、App() コンポーザブルと currentTimeAt() 関数を更新し、補助的なデータクラスを追加します。

    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 があります。
  2. IDE の指示に従って、不足している依存関係をインポートします。 Row() をインポートする際は、@Composable バージョンを選択してください。

  3. アプリケーションを実行して、再設計されたバージョンを確認します。

Android と iOS の Compose Multiplatform アプリでの国リスト
デスクトップの Compose Multiplatform アプリでの国リスト
Web の Compose Multiplatform アプリでの国リスト

Koin などの依存関係注入(Dependency Injection)フレームワークを使用して、場所のテーブルを構築し注入することで、さらにデザインを改善できます。データが外部に保存されている場合は、Ktor ライブラリを使用してネットワーク経由で取得したり、SQLDelight ライブラリを使用してデータベースから取得したりできます。

画像を導入する

国の名前のリストは機能しますが、ユーザー体験としてはあまり良くありません。国の名前の隣に国旗の画像を追加することで、リストを改善できます。

Compose Multiplatform は、すべてのプラットフォームで共通コードを介してリソースにアクセスするためのライブラリを提供しています。Kotlin Multiplatform ウィザードは、すでにこのライブラリを追加して構成しているため、すぐにリソースの読み込みを開始できます。

プロジェクトで画像をサポートするには、画像ファイルをダウンロードし、正しいディレクトリに保存し、それらを読み込んで表示するためのコードを追加する必要があります。

  1. すでに作成した国のリストに合わせて、Flag CDN から国旗の画像をダウンロードします。この例では、日本フランスメキシコインドネシアエジプト です。

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

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

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

  4. 画像をサポートするように commonMain/kotlin/.../App.kt ファイルのコードを更新します。

    kotlin
    import 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 は、各 DropdownMenuItemImage を表示し、その後に国の名前を表示する Text コンポーザブルを表示します。
    • Image は、データを取得するために Painter オブジェクトを必要とします。
  5. IDE の指示に従って、不足している依存関係をインポートします。

  6. アプリケーションを実行して、新しい動作を確認します。

Android と iOS の Compose Multiplatform アプリでの国旗
デスクトップの Compose Multiplatform アプリでの国旗
Web の Compose Multiplatform アプリでの国旗

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

次のステップ

マルチプラットフォーム開発をさらに探索し、他のプロジェクトも試してみることをお勧めします。

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