1. 始める前に
この Codelab では、Compose の状態の概要 Codelab の解答コードを使用して、インタラクティブなチップ計算ツールを作成します。このアプリでは、請求額とチップ率を入力するとチップ金額が自動的に計算され、四捨五入されます。最終的なアプリの外観は次のとおりです。
前提条件
- Compose の状態の概要 Codelab
Text
コンポーザブルとTextField
コンポーザブルをアプリに追加できるremember()
関数、状態、状態ホイスティング、コンポーズ可能な関数のステートフル / ステートレスの違いに関する知識
学習内容
- 仮想キーボードにアクション ボタンを追加する方法
Switch
コンポーザブルの概要と使用方法- テキスト フィールドに先頭のアイコンを追加します。
作成するアプリの概要
- ユーザーが入力した請求金額とチップ率に基づいてチップ金額を計算する Tip Time アプリ
必要なもの
- Android Studio の最新バージョン
- Compose の状態の概要 Codelab の解答コード
2. スターター コードを取得する
まず、スターター コードをダウンロードします。
または、GitHub リポジトリのクローンを作成してコードを入手することもできます。
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git $ cd basic-android-kotlin-compose-training-tip-calculator $ git checkout state
コードは Tip Time
GitHub リポジトリで確認できます。
3.スターター アプリの概要
この Codelab は、前の Codelab「Compose の状態の概要」の「Tip Time アプリ」を使って始めます。このアプリは、固定のチップ率でチップ金額を計算するために必要なユーザー インターフェースを提供するものです。[請求額] テキスト ボックスに、サービス料金を入力します。アプリはチップ金額を計算して Text
コンポーザブルに表示します。
Tip Time アプリを実行する
- Android Studio で Tip Time プロジェクトを開き、エミュレータまたはデバイスでアプリを実行します。
- 請求金額を入力します。アプリが自動的にチップ金額を計算して表示します。
現在の実装では、チップ率は 15% にハードコードされています。この Codelab では、テキスト フィールドを使用してこの機能を拡張し、アプリでカスタムのチップ率を計算してチップ金額を四捨五入できるようにします。
必要な文字列リソースを追加する
- [Project] タブで、[res] > [values] > [strings.xml] をクリックします。
strings.xml
ファイルの<resources>
タグの間に、次の文字列リソースを追加します。
<string name="how_was_the_service">Tip Percentage</string>
<string name="round_up_tip">Round up tip?</string>
strings.xml
ファイルは、前の Codelab の文字列を含む次のコード スニペットのようになります。
strings.xml
<resources>
<string name="app_name">Tip Time</string>
<string name="calculate_tip">Calculate Tip</string>
<string name="bill_amount">Bill Amount</string>
<string name="how_was_the_service">Tip Percentage</string>
<string name="round_up_tip">Round up tip?</string>
<string name="tip_amount">Tip Amount: %s</string>
</resources>
4. チップ率のテキスト フィールドを追加する
提供されたサービスの質やその他のさまざまな理由によって、チップを増減したい場合があります。これに対応するために、アプリでユーザーがカスタムのチップ金額を計算できるようにする必要があります。このセクションでは、次の画像のように、ユーザーがカスタムのチップ率を入力するテキスト フィールドを追加します。
アプリにはすでに [Bill Amount] テキスト フィールド コンポーザブルがあります。これはステートレスな、EditNumberField()
というコンポーズ可能な関数です。前の Codelab では、amountInput
の状態を EditNumberField()
コンポーザブルから TipTimeLayout()
コンポーザブルにホイスティングし、EditNumberField()
コンポーザブルをステートレスにしました。
テキスト フィールドを追加するには、同じ EditNumberField()
コンポーザブルを再利用しますが、別のラベルを使用します。この変更を行うには、ラベルをコンポーズ可能な関数 EditNumberField()
内でハードコードするのではなく、パラメータとして渡す必要があります。
コンポーズ可能な関数 EditNumberField()
を再利用可能にします。
MainActivity.kt
ファイルで、コンポーズ可能な関数EditNumberField()
のパラメータにInt
型のlabel
文字列リソースを追加します。
@Composable
fun EditNumberField(
label: Int,
value: String,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier
)
- 関数本体で、ハードコードされた文字列リソース ID を
label
パラメータに置き換えます。
@Composable
fun EditNumberField(
//...
) {
TextField(
//...
label = { Text(stringResource(label)) },
//...
)
}
label
パラメータが文字列リソース参照であることを示すために、関数パラメータに@StringRes
アノテーションを付けます。
@Composable
fun EditNumberField(
@StringRes label: Int,
value: String,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier
)
- 以下をインポートします。
import androidx.annotation.StringRes
- コンポーズ可能な関数
TipTimeLayout()
のEditNumberField()
関数呼び出しで、label
パラメータをR.string.bill_amount
文字列リソースに設定します。
EditNumberField(
label = R.string.bill_amount,
value = amountInput,
onValueChanged = { amountInput = it },
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth()
)
- [Preview] ペインに視覚的な変更はありません。
- コンポーズ可能な関数
TipTimeLayout()
で、EditNumberField()
関数呼び出しの後に、カスタムのチップ率用に別のテキスト フィールドを追加します。次のパラメータを使用して、コンポーズ可能な関数EditNumberField()
を呼び出します。
EditNumberField(
label = R.string.how_was_the_service,
value = "",
onValueChanged = { },
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth()
)
これで、カスタムのチップ率を入力する別のテキスト ボックスが追加されます。
- 次の画像のように、アプリのプレビューに [Tip Percentage] テキスト フィールドが表示されるようになりました。
- コンポーズ可能な関数
TipTimeLayout()
の先頭に、追加したテキスト フィールドの状態変数用にtipInput
というvar
プロパティを追加します。mutableStateOf("")
を使用して変数を初期化し、remember
関数で呼び出しを囲みます。
var tipInput by remember { mutableStateOf("") }
- 新しい
EditNumberField
()
関数呼び出しで、名前付きパラメータvalue
をtipInput
変数に設定し、ラムダ式onValueChanged
のtipInput
変数を更新します。
EditNumberField(
label = R.string.how_was_the_service,
value = tipInput,
onValueChanged = { tipInput = it },
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth()
)
TipTimeLayout()
関数で、tipInput
変数の定義の後、tipInput
変数をDouble
型に変換するtipPercent
というval
を定義します。エルビス演算子を使用して、値がnull
の場合に0
を返します。テキスト フィールドが空の場合、この値はnull
になります。
val tipPercent = tipInput.toDoubleOrNull() ?: 0.0
TipTimeLayout()
関数で、calculateTip()
関数呼び出しを更新し、2 番目のパラメータとしてtipPercent
変数を渡します。
val tip = calculateTip(amount, tipPercent)
TipTimeLayout()
関数のコードは次のコード スニペットのようになりました。
@Composable
fun TipTimeLayout() {
var amountInput by remember { mutableStateOf("") }
var tipInput by remember { mutableStateOf("") }
val amount = amountInput.toDoubleOrNull() ?: 0.0
val tipPercent = tipInput.toDoubleOrNull() ?: 0.0
val tip = calculateTip(amount, tipPercent)
Column(
modifier = Modifier.padding(40.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = stringResource(R.string.calculate_tip),
modifier = Modifier
.padding(bottom = 16.dp)
.align(alignment = Alignment.Start)
)
EditNumberField(
label = R.string.bill_amount,
value = amountInput,
onValueChanged = { amountInput = it },
modifier = Modifier
.padding(bottom = 32.dp)
.fillMaxWidth()
)
EditNumberField(
label = R.string.how_was_the_service,
value = tipInput,
onValueChanged = { tipInput = it },
modifier = Modifier
.padding(bottom = 32.dp)
.fillMaxWidth()
)
Text(
text = stringResource(R.string.tip_amount, tip),
style = MaterialTheme.typography.displaySmall
)
Spacer(modifier = Modifier.height(150.dp))
}
}
- エミュレータまたはデバイスでアプリを実行し、請求額とチップ率を入力します。チップ金額は正しく計算されていますか。
5. アクション ボタンを設定する
前の Codelab では、KeyboardOptions
クラスを使用してキーボードのタイプを設定する方法について説明しました。このセクションでは、同じ KeyboardOptions
を使用してキーボード アクション ボタンを設定する方法について説明します。キーボード アクション ボタンは、キーボードの末端にあるボタンです。次の表に例を示します。
プロパティ | キーボード アクション ボタン |
| |
| |
|
このタスクでは、テキスト ボックスのアクション ボタンを 2 種類設定します。
- [Bill Amount] テキスト ボックスの Next アクション ボタン。ユーザーが現在の入力を完了し、次のテキスト ボックスに移動することを示します。
- [Tip Percentage] テキスト ボックスの Done アクション ボタン。ユーザーが所定の入力を終了したことを示します。
これらのアクション ボタンを備えたキーボードの例を次の画像に示します。
キーボード オプションを追加します。
EditNumberField()
関数のTextField()
関数呼び出しで、ImeAction.Next
値に設定したimeAction
名前付き引数をKeyboardOptions
コンストラクタに渡します。KeyboardOptions.Default.copy()
関数を使用して、他のデフォルト オプションを指定します。
import androidx.compose.ui.text.input.ImeAction
@Composable
fun EditNumberField(
//...
) {
TextField(
//...
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Next
)
)
}
- エミュレータまたはデバイスでアプリを実行します。次の画像のように、キーボードに Next アクション ボタンが表示されるようになりました。
[Tip Percentage] テキスト フィールドが選択されている場合、キーボードには同じ [Next] アクション ボタンが表示されます。しかし、テキスト フィールドにはアクション ボタンが 2 種類必要です。この問題はすぐに修正できます。
EditNumberField()
関数を調べます。TextField()
関数のkeyboardOptions
パラメータはハードコードされています。テキスト フィールド用に種々のアクション ボタンを作成するには、KeyboardOptions
オブジェクトを引数として渡す必要があります。これは次のステップで行います。
// No need to copy, just examine the code.
fun EditNumberField(
@StringRes label: Int,
value: String,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier
) {
TextField(
//...
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Next
)
)
}
EditNumberField()
関数定義に、KeyboardOptions
型のkeyboardOptions
パラメータを追加します。関数本体で、これをTextField()
関数のkeyboardOptions
名前付きパラメータに代入します。
@Composable
fun EditNumberField(
@StringRes label: Int,
keyboardOptions: KeyboardOptions,
// ...
){
TextField(
//...
keyboardOptions = keyboardOptions
)
}
TipTimeLayout()
関数で、最初のEditNumberField()
関数呼び出しを更新し、[Bill Amount] テキスト フィールドのkeyboardOptions
名前付きパラメータを渡します。
EditNumberField(
label = R.string.bill_amount,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Next
),
// ...
)
- 2 番目の
EditNumberField()
関数呼び出しで、[Tip Percentage] テキスト フィールドのimeAction
をImeAction.Done
に変更します。関数は次のコード スニペットのようになります。
EditNumberField(
label = R.string.how_was_the_service,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done
),
// ...
)
- アプリを実行します。次の画像のように、Next と Done のアクション ボタンが表示されます。
- 請求額を入力して Next アクション ボタンをクリックします。次にチップ率を入力し、Done アクション ボタンをクリックします。キーパッドが閉じます。
6. スイッチを追加する
スイッチは、1 つのアイテムの状態をオンまたはオフに切り替えます。
切り替えボタンには 2 つの状態があり、ユーザーは 2 つのオプションのいずれかを選択できます。切り替えボタンは、次の画像のように、トラック、つまみ、オプションのアイコンで構成されています。
スイッチは、次の画像のように、決定内容の入力や設定の宣言に使用できる選択コントロールです。
ユーザーは、つまみを前後にドラッグしてオプションを選択するか、単にスイッチをタップして切り替えます。別の切り替え例として、次の GIF ではビジュアル オプションの設定をダークモードに切り替えています。
スイッチについて詳しくは、スイッチのドキュメントをご覧ください。
次の画像のように、Switch
コンポーザブルを使用して、チップを最も近い整数に切り上げるかどうかを選択できるようにします。
Text
コンポーザブルと Switch
コンポーザブルの行を追加します。
EditNumberField()
関数の後にコンポーズ可能な関数RoundTheTipRow()
を追加し、EditNumberField()
関数と同様の引数としてデフォルトのModifier
を渡します。
@Composable
fun RoundTheTipRow(modifier: Modifier = Modifier) {
}
RoundTheTipRow()
関数を実装し、次のmodifier
を使用してRow
レイアウト コンポーザブルを追加します。子要素の幅を画面上の最大値に設定し、中央に配置して、サイズを48dp
にします。
Row(
modifier = modifier
.fillMaxWidth()
.size(48.dp),
verticalAlignment = Alignment.CenterVertically
) {
}
- 以下をインポートします。
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
Row
レイアウト コンポーザブルのラムダブロックに、R.string.round_up_tip
文字列リソースを使用してRound up tip?
文字列を表示するText
コンポーザブルを追加します。
Text(text = stringResource(R.string.round_up_tip))
Text
コンポーザブルの後にSwitch
コンポーザブルを追加します。checked
名前付きパラメータをroundUp
に設定し、onCheckedChange
名前付きパラメータをonRoundUpChanged
に設定して渡します。
Switch(
checked = roundUp,
onCheckedChange = onRoundUpChanged,
)
次の表に、RoundTheTipRow()
関数で定義したものと同じパラメータに関する情報が含まれています。
パラメータ | 説明 |
| スイッチがオンになっているかどうか。これは |
| スイッチがクリックされたときに呼び出されるコールバック。 |
- 以下をインポートします。
import androidx.compose.material3.Switch
RoundTheTipRow()
関数に、Boolean
型のroundUp
パラメータと、Boolean
を受け取って何も返さないonRoundUpChanged
ラムダ関数を追加します。
@Composable
fun RoundTheTipRow(
roundUp: Boolean,
onRoundUpChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier
)
これにより、スイッチの状態がホイスティングされます。
Switch
コンポーザブルに、このmodifier
を追加して、Switch
コンポーザブルを画面の末端に配置します。
Switch(
modifier = modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.End),
//...
)
- 以下をインポートします。
import androidx.compose.foundation.layout.wrapContentWidth
TipTimeLayout()
関数に、Switch
コンポーザブルの状態の var 変数を追加します。roundUp
というvar
変数を作成してmutableStateOf()
に設定し、初期値としてfalse
を設定します。呼び出しをremember { }
で囲みます。
fun TipTimeLayout() {
//...
var roundUp by remember { mutableStateOf(false) }
//...
Column(
...
) {
//...
}
}
これは Switch
コンポーザブルの状態の変数であり、false がデフォルトの状態になります。
TipTimeLayout()
関数のColumn
ブロック内([Tip Percentage] テキスト フィールドの後)。次の引数を持つRoundTheTipRow()
関数を呼び出します。roundUp
に設定された名前付きパラメータroundUp
、roundUp
値を更新するラムダ コールバックに設定されたonRoundUpChanged
名前付きパラメータ。
@Composable
fun TipTimeLayout() {
//...
Column(
...
) {
Text(
...
)
Spacer(...)
EditNumberField(
...
)
EditNumberField(
...
)
RoundTheTipRow(
roundUp = roundUp,
onRoundUpChanged = { roundUp = it },
modifier = Modifier.padding(bottom = 32.dp)
)
Text(
...
)
}
}
[Round up tip?] 行が表示されます。
- アプリを実行します。アプリに [Round up tip?] の切り替えが表示されます。
- 請求額とチップ率を入力し、[Round up tip?] の切り替えを選択します。チップの金額は四捨五入されませんが、これはまだ
calculateTip()
関数を更新する必要があるためです(次のセクションで行います)。
チップを四捨五入するように calculateTip()
関数を更新する
Boolean
変数を受け入れてチップを最も近い整数に切り上げるように、calculateTip()
関数を変更します。
- チップを切り上げるには、
calculateTip()
関数がスイッチの状態(Boolean
)を把握する必要があります。calculateTip()
関数に、Boolean
型のroundUp
パラメータを追加します。
private fun calculateTip(
amount: Double,
tipPercent: Double = 15.0,
roundUp: Boolean
): String {
//...
}
calculateTip()
関数のreturn
ステートメントの前に、roundUp
の値をチェックするif()
条件を追加します。roundUp
がtrue
であれば、tip
変数を定義してkotlin.math.
ceil
()
関数に設定し、引数として関数tip
を渡します。
if (roundUp) {
tip = kotlin.math.ceil(tip)
}
完成した calculateTip()
関数は次のコード スニペットのようになります。
private fun calculateTip(amount: Double, tipPercent: Double = 15.0, roundUp: Boolean): String {
var tip = tipPercent / 100 * amount
if (roundUp) {
tip = kotlin.math.ceil(tip)
}
return NumberFormat.getCurrencyInstance().format(tip)
}
TipTimeLayout()
関数で、calculateTip()
関数呼び出しを更新し、roundUp
パラメータを渡します。
val tip = calculateTip(amount, tipPercent, roundUp)
- アプリを実行します。次の画像のように、チップ金額が切り上げられるようになりました。
7. 横向きのサポートを追加する
Android デバイスには、スマートフォン、タブレット、折りたたみ式デバイス、ChromeOS デバイスなど、さまざまなフォーム ファクタがあり、広範な画面サイズが用意されています。アプリは、縦向きと横向きの両方をサポートする必要があります。
- アプリを横向きでテストし、自動回転をオンにします。
- エミュレータまたはデバイスを左に回転させると、チップの金額が表示されません。この問題を解決するには、アプリ画面をスクロールできる縦方向のスクロールバーが必要です。
- 修飾子に
.verticalScroll(rememberScrollState())
を追加して、列を縦方向にスクロールできるようにします。rememberScrollState()
はスクロール状態を作成し、自動的に記憶します。
@Composable
fun TipTimeLayout() {
// ...
Column(
modifier = Modifier
.padding(40.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
//...
}
}
- 以下をインポートします。
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
- アプリを再度実行します。横表示でスクロールしてみましょう。
8. テキスト フィールドに先頭のアイコンを追加する(省略可)
アイコンを使用すると、テキスト フィールドをより視覚的にアピールし、テキスト フィールドに関する追加情報を提供できます。想定されるデータの種類や必要な入力の種類など、テキスト フィールドの目的をアイコンで示すことができます。たとえば、テキスト フィールドの横にある電話のアイコンは、ユーザーが電話番号を入力することが期待されていることを示します。
アイコンを使用して、ユーザーに期待する内容を視覚的に伝えることで入力を促すことができます。たとえば、テキスト フィールドの横にあるカレンダー アイコンは、ユーザーが日付を入力することが期待されていることを示します。
以下に、検索キーワードを入力することを示す検索アイコンを含むテキスト フィールドの例を示します。
EditNumberField()
コンポーザブルに、Int
型の leadingIcon
という別のパラメータを追加します。@DrawableRes
でアノテーションを付けます。
@Composable
fun EditNumberField(
@StringRes label: Int,
@DrawableRes leadingIcon: Int,
keyboardOptions: KeyboardOptions,
value: String,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier
)
- 以下をインポートします。
import androidx.annotation.DrawableRes
import androidx.compose.material3.Icon
- テキスト フィールドに先頭のアイコンを追加します。
leadingIcon
はコンポーザブルを受け取り、次のIcon
コンポーザブルを渡します。
TextField(
value = value,
leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) },
//...
)
- 先頭のアイコンをテキスト フィールドに渡します。便宜上、アイコンはすでにスターター コード内にあります。
EditNumberField(
label = R.string.bill_amount,
leadingIcon = R.drawable.money,
// Other arguments
)
EditNumberField(
label = R.string.how_was_the_service,
leadingIcon = R.drawable.percent,
// Other arguments
)
- アプリを実行します。
おめでとうございます。これで、アプリでカスタムのチップ金額を計算できるようになりました。
9. 解答コードを取得する
この Codelab の完成したコードをダウンロードするには、次の git コマンドを使用します。
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git
または、リポジトリを ZIP ファイルとしてダウンロードし、Android Studio で開くこともできます。
解答コードを確認する場合は、GitHub で表示します。
10. まとめ
おめでとうございます。Tip Time アプリにカスタムのチップ機能を追加しました。このアプリでは、ユーザーがカスタムのチップ率を入力し、チップの金額を切り上げられるようになりました。#AndroidBasics を付けて、ソーシャル メディアで共有しましょう。