🐕

[チュートリアル6]UIを改善してみよう[compose multiplatform]

2023/11/09に公開

はじめに

公式ドキュメントの和訳と要約+αです。
前回はこちらです。

デザインを改良する

今の見た目ってこんな感じだと思います。
現在の画像

これだと要素が詰まりすぎているので、少し余白を作りましょう。

App.ktのApp()ファンクションの中身を以下のように換えてください。

@Composable
fun App() {
    MaterialTheme {
        var location by remember { mutableStateOf("Europe/Paris") }
        var timeAtLocation by remember { mutableStateOf("No location selected") }

        // modifierを使って20dpのpaddingを追加
        Column(modifier = Modifier.padding(20.dp)) {
            Text(
                timeAtLocation,
                // style属性を使ってfontSizeを20spに変更
                style = TextStyle(fontSize = 20.sp),
                // textAlign属性をCenterに変更し、中央に文字がくるように
                textAlign = TextAlign.Center,
                // fillMaxWidthを使って、Columnの幅いっぱいに広げる
                modifier = Modifier.fillMaxWidth()
                    .align(Alignment.CenterHorizontally)
            )

            TextField(
                value = location,
                // modifierを使って10dpのpaddingを追加
                modifier = Modifier.padding(top = 10.dp),
                onValueChange = {
                    location = it
                }
            )

            Button(
                // modifierを使って10dpのpaddingを追加
                modifier = Modifier.padding(top = 10.dp),
                onClick = {
                    timeAtLocation = currentTimeAt(location) ?: "Invalid Location"
                }
            ) {
                Text("Show Time")
            }
        }
    }
}

細かい解説は面倒なので、コメントアウトの形でコードの中に入れておきました。
気になったら呼んでください。

またimportも同時に追加します。

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

この修正によって、時間を表示する部分は画面の中央にくるようになり、それぞれの要素の間に余白が生まれたはずです。

ユーザーに優しく

現状のアプリだとユーザーがスペルミスをしたら正しく動かないので、使いやすいとは言えませんね。
リストの中から国を選んだら、その国が所属しているタイムゾーンを判別して、そのタイムゾーンの時刻を表示してくれるように修正しましょう。

細かい解説はコメントアウトで書いてあります。
App.kt

// `Country` というデータクラスを作りました。国のデータを保持するクラスです。名前とタイムゾーンをプロパティに持ちます。
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()}"
}

// `countries` 関数は、いくつかの国のリストを生成して返します。
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
fun App(countries: List<Country> = countries()) {
    MaterialTheme {
        // `showCountries` はドロップダウンメニューの表示状態を保持する変数です。
        var showCountries by remember { mutableStateOf(false) }
        // `timeAtLocation` は選択された場所の時刻を表示するための変数です。
        var timeAtLocation by remember { mutableStateOf("No location selected") }

        Column(modifier = Modifier.padding(20.dp)) {
            Text(
                timeAtLocation,
                style = TextStyle(fontSize = 20.sp),
                textAlign = TextAlign.Center,
                // `fillMaxWidth` は可能な限りの幅を埋めるようにします。
                // `align` は水平方向の位置を調整します。
                modifier = Modifier.fillMaxWidth()
                    .align(Alignment.CenterHorizontally)
            )
            // `Row` は横にコンテンツを並べるためのコンテナです。
            Row(modifier = Modifier.padding(start = 20.dp, top = 10.dp)) {
                // `DropdownMenu` はドロップダウンメニューを表示するためのコンポーザブルです。
                DropdownMenu(
                    expanded = showCountries,
                    onDismissRequest = { showCountries = false }
                ) {
                    // 国のリストをループし、各国に対してメニューアイテムを作成します。
                    countries.forEach { (name, zone) ->
                        DropdownMenuItem(
                            onClick = {
                                // メニューアイテムをクリックすると、時刻を更新してメニューを閉じます。
                                timeAtLocation = currentTimeAt(name, zone)
                                showCountries = false
                            }
                        ) {
                            Text(name)
                        }
                    }
                }
            }

            Button(
                modifier = Modifier.padding(start = 20.dp, top = 10.dp),
                onClick = { showCountries = !showCountries }
            ) {
                Text("Select Location")
            }
        }
    }
}

それに伴ってimportに以下を追加します。

import androidx.compose.foundation.layout.Row
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem

これで、国を選択したら、その国が属するtime zoneとそのタイムゾーンでの現在時刻を表示するようになったはずです。

画像を導入する

文字だけだと味気ないですよね、画像を入れてみましょう。
今回は国旗の画像を入れてみます。
これで最後なので頑張っていきましょう〜

リソースを配置する

今回はアプリ内部に画像のデータを保存するようにします。保存するパスは

src/commonMain/resources

保存する画像はflagcdnさんから拝借しましょう。
今回は、JapanFranceMexicoIndonesiaEgyptですね。

です。実はそれぞれのプラットフォームごとに別々の画像を保存しておけば、違う画像を表示してくれるのですが、今回はやりません。

リソースを表示してみる

次に先ほどの画像を表示するように、コードを修正してみましょう。
詳細な解説はコメントアウトに書いてあります。

// Countryというデータクラスで、画像を持つように修正します。
data class Country(val name: String, val zone: TimeZone, val image: String)

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()}"
}

// `countries` 関数の中で、先ほど追加した画像を含むようにします。
fun countries() = listOf(
    Country("Japan", TimeZone.of("Asia/Tokyo"), "jp.png"),
    Country("France", TimeZone.of("Europe/Paris"), "fr.png"),
    Country("Mexico", TimeZone.of("America/Mexico_City"), "mx.png"),
    Country("Indonesia", TimeZone.of("Asia/Jakarta"), "id.png"),
    Country("Egypt", TimeZone.of("Africa/Cairo"), "eg.png")
)

@OptIn(ExperimentalResourceApi::class)
@Composable
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)) {
            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 }
                ) {
                    // imageも使うように修正します。
                    countries.forEach { (name, zone, image) ->
                        DropdownMenuItem(
                            onClick = {
                                timeAtLocation = currentTimeAt(name, zone)
                                showCountries = false
                            }
                        ) {
                            Image(
                                painterResource(image),
                                modifier = Modifier.size(50.dp).padding(end = 10.dp),
                                contentDescription = "$name flag"
                            )
                            Text(name)
                        }
                    }
                }
            }

            Button(
                modifier = Modifier.padding(start = 20.dp, top = 10.dp),
                onClick = { showCountries = !showCountries }
            ) {
                Text("Select Location")
            }
        }
    }
}

それに伴いimport文を追加します。

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.painterResource

ここまでで、割とまともなアプリになっていると思います。
以上でチュートリアル終了です!
お疲れ様でした。

Discussion