[チュートリアル6]UIを改善してみよう[compose multiplatform]
はじめに
デザインを改良する
今の見た目ってこんな感じだと思います。
これだと要素が詰まりすぎているので、少し余白を作りましょう。
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さんから拝借しましょう。
今回は、Japan、France、Mexico、Indonesia、Egyptですね。
です。実はそれぞれのプラットフォームごとに別々の画像を保存しておけば、違う画像を表示してくれるのですが、今回はやりません。
リソースを表示してみる
次に先ほどの画像を表示するように、コードを修正してみましょう。
詳細な解説はコメントアウトに書いてあります。
// 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