カスタムのチップ金額を計算する

1. 始める前に

この Codelab では、Compose の状態の概要 Codelab の解答コードを使用して、インタラクティブなチップ計算ツールを作成します。このアプリでは、請求額とチップ率を入力するとチップ金額が自動的に計算され、四捨五入されます。最終的なアプリの外観は次のとおりです。

d8e768525099378a.png

前提条件

  • Compose の状態の概要 Codelab
  • Text コンポーザブルと TextField コンポーザブルをアプリに追加できる
  • remember() 関数、状態、状態ホイスティング、コンポーズ可能な関数のステートフル / ステートレスの違いに関する知識

学習内容

  • 仮想キーボードにアクション ボタンを追加する方法
  • Switch コンポーザブルの概要と使用方法
  • テキスト フィールドに先頭のアイコンを追加します。

作成するアプリの概要

  • ユーザーが入力した請求金額とチップ率に基づいてチップ金額を計算する Tip Time アプリ

必要なもの

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 アプリを実行する

  1. Android Studio で Tip Time プロジェクトを開き、エミュレータまたはデバイスでアプリを実行します。
  2. 請求金額を入力します。アプリが自動的にチップ金額を計算して表示します。

b6bd5374911410ac.png

現在の実装では、チップ率は 15% にハードコードされています。この Codelab では、テキスト フィールドを使用してこの機能を拡張し、アプリでカスタムのチップ率を計算してチップ金額を四捨五入できるようにします。

必要な文字列リソースを追加する

  1. [Project] タブで、[res] > [values] > [strings.xml] をクリックします。
  2. 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. チップ率のテキスト フィールドを追加する

提供されたサービスの質やその他のさまざまな理由によって、チップを増減したい場合があります。これに対応するために、アプリでユーザーがカスタムのチップ金額を計算できるようにする必要があります。このセクションでは、次の画像のように、ユーザーがカスタムのチップ率を入力するテキスト フィールドを追加します。

391b4b1a090687ef.png

アプリにはすでに [Bill Amount] テキスト フィールド コンポーザブルがあります。これはステートレスな、EditNumberField() というコンポーズ可能な関数です。前の Codelab では、amountInput の状態を EditNumberField() コンポーザブルから TipTimeLayout() コンポーザブルにホイスティングし、EditNumberField() コンポーザブルをステートレスにしました。

テキスト フィールドを追加するには、同じ EditNumberField() コンポーザブルを再利用しますが、別のラベルを使用します。この変更を行うには、ラベルをコンポーズ可能な関数 EditNumberField() 内でハードコードするのではなく、パラメータとして渡す必要があります。

コンポーズ可能な関数 EditNumberField() を再利用可能にします。

  1. MainActivity.kt ファイルで、コンポーズ可能な関数 EditNumberField() のパラメータに Int 型の label 文字列リソースを追加します。
@Composable
fun EditNumberField(
    label: Int,
    value: String,
    onValueChanged: (String) -> Unit,
    modifier: Modifier = Modifier
)
  1. 関数本体で、ハードコードされた文字列リソース ID を label パラメータに置き換えます。
@Composable
fun EditNumberField(
    //...
) {
     TextField(
         //...
         label = { Text(stringResource(label)) },
         //...
     )
}
  1. label パラメータが文字列リソース参照であることを示すために、関数パラメータに @StringRes アノテーションを付けます。
@Composable
fun EditNumberField(
    @StringRes label: Int,
    value: String,
    onValueChanged: (String) -> Unit,
    modifier: Modifier = Modifier
)
  1. 以下をインポートします。
import androidx.annotation.StringRes
  1. コンポーズ可能な関数 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()
)
  1. [Preview] ペインに視覚的な変更はありません。

b223d5ba4a54f792.png

  1. コンポーズ可能な関数 TipTimeLayout() で、EditNumberField() 関数呼び出しの後に、カスタムのチップ率用に別のテキスト フィールドを追加します。次のパラメータを使用して、コンポーズ可能な関数 EditNumberField() を呼び出します。
EditNumberField(
    label = R.string.how_was_the_service,
    value = "",
    onValueChanged = { },
    modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth()
)

これで、カスタムのチップ率を入力する別のテキスト ボックスが追加されます。

  1. 次の画像のように、アプリのプレビューに [Tip Percentage] テキスト フィールドが表示されるようになりました。

a5f5ef5e456e185e.png

  1. コンポーズ可能な関数 TipTimeLayout() の先頭に、追加したテキスト フィールドの状態変数用に tipInput という var プロパティを追加します。mutableStateOf("") を使用して変数を初期化し、remember 関数で呼び出しを囲みます。
var tipInput by remember { mutableStateOf("") }
  1. 新しい EditNumberField() 関数呼び出しで、名前付きパラメータ valuetipInput 変数に設定し、ラムダ式 onValueChangedtipInput 変数を更新します。
EditNumberField(
    label = R.string.how_was_the_service,
    value = tipInput,
    onValueChanged = { tipInput = it },
    modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth()
)
  1. TipTimeLayout() 関数で、tipInput 変数の定義の後、tipInput 変数を Double 型に変換する tipPercent という val を定義します。エルビス演算子を使用して、値が null の場合に 0 を返します。テキスト フィールドが空の場合、この値は null になります。
val tipPercent = tipInput.toDoubleOrNull() ?: 0.0
  1. 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))
    }
}
  1. エミュレータまたはデバイスでアプリを実行し、請求額とチップ率を入力します。チップ金額は正しく計算されていますか。

請求額が 100、チップ率が 20、チップ金額が 20 ドルと表示されているスクリーンショット

5. アクション ボタンを設定する

前の Codelab では、KeyboardOptions クラスを使用してキーボードのタイプを設定する方法について説明しました。このセクションでは、同じ KeyboardOptions を使用してキーボード アクション ボタンを設定する方法について説明します。キーボード アクション ボタンは、キーボードの末端にあるボタンです。次の表に例を示します。

プロパティ

キーボード アクション ボタン

ImeAction.Search
ユーザーが検索を行うときに使用します。

この画像は、検索を実行するための検索アイコンを表しています。

ImeAction.Send
ユーザーが入力フィールドでテキストを送信するときに使用します。

この画像は、入力フィールドのテキストを送信するための送信アイコンを表しています。

ImeAction.Go
ユーザーが入力テキストのターゲットに移動するときに使用します。

この画像は、入力テキストのターゲットに移動するための移動アイコンを表しています。

このタスクでは、テキスト ボックスのアクション ボタンを 2 種類設定します。

  • [Bill Amount] テキスト ボックスの Next アクション ボタン。ユーザーが現在の入力を完了し、次のテキスト ボックスに移動することを示します。
  • [Tip Percentage] テキスト ボックスの Done アクション ボタン。ユーザーが所定の入力を終了したことを示します。

これらのアクション ボタンを備えたキーボードの例を次の画像に示します。

キーボード オプションを追加します。

  1. 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
        )
    )
}
  1. エミュレータまたはデバイスでアプリを実行します。次の画像のように、キーボードに Next アクション ボタンが表示されるようになりました。

82574a95b658f052.png

[Tip Percentage] テキスト フィールドが選択されている場合、キーボードには同じ [Next] アクション ボタンが表示されます。しかし、テキスト フィールドにはアクション ボタンが 2 種類必要です。この問題はすぐに修正できます。

  1. 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
        )
    )
}
  1. EditNumberField() 関数定義に、KeyboardOptions 型の keyboardOptions パラメータを追加します。関数本体で、これを TextField() 関数の keyboardOptions 名前付きパラメータに代入します。
@Composable
fun EditNumberField(
    @StringRes label: Int,
    keyboardOptions: KeyboardOptions,
    // ...
){
    TextField(
        //...
        keyboardOptions = keyboardOptions
    )
}
  1. TipTimeLayout() 関数で、最初の EditNumberField() 関数呼び出しを更新し、[Bill Amount] テキスト フィールドの keyboardOptions 名前付きパラメータを渡します。
EditNumberField(
    label = R.string.bill_amount,
    keyboardOptions = KeyboardOptions.Default.copy(
        keyboardType = KeyboardType.Number,
        imeAction = ImeAction.Next
    ),
    // ...
)
  1. 2 番目の EditNumberField() 関数呼び出しで、[Tip Percentage] テキスト フィールドの imeActionImeAction.Done に変更します。関数は次のコード スニペットのようになります。
EditNumberField(
    label = R.string.how_was_the_service,
    keyboardOptions = KeyboardOptions.Default.copy(
        keyboardType = KeyboardType.Number,
        imeAction = ImeAction.Done
    ),
    // ...
)
  1. アプリを実行します。次の画像のように、NextDone のアクション ボタンが表示されます。

  1. 請求額を入力して Next アクション ボタンをクリックします。次にチップ率を入力し、Done アクション ボタンをクリックします。キーパッドが閉じます。

a9e3fbddfff829c8.gif

6. スイッチを追加する

スイッチは、1 つのアイテムの状態をオンまたはオフに切り替えます。

6923dfb1101602c7.png

切り替えボタンには 2 つの状態があり、ユーザーは 2 つのオプションのいずれかを選択できます。切り替えボタンは、次の画像のように、トラック、つまみ、オプションのアイコンで構成されています。

b4f7f68b848bcc2b.png

スイッチは、次の画像のように、決定内容の入力や設定の宣言に使用できる選択コントロールです。

5cd8acb912ab38eb.png

ユーザーは、つまみを前後にドラッグしてオプションを選択するか、単にスイッチをタップして切り替えます。別の切り替え例として、次の GIF ではビジュアル オプションの設定をダークモードに切り替えています。

eabf96ad496fd226.gif

スイッチについて詳しくは、スイッチのドキュメントをご覧ください。

次の画像のように、Switch コンポーザブルを使用して、チップを最も近い整数に切り上げるかどうかを選択できるようにします。

b42af9f2d3861e4.png

Text コンポーザブルと Switch コンポーザブルの行を追加します。

  1. EditNumberField() 関数の後にコンポーズ可能な関数 RoundTheTipRow() を追加し、EditNumberField() 関数と同様の引数としてデフォルトの Modifier を渡します。
@Composable
fun RoundTheTipRow(modifier: Modifier = Modifier) {
}
  1. RoundTheTipRow() 関数を実装し、次の modifier を使用して Row レイアウト コンポーザブルを追加します。子要素の幅を画面上の最大値に設定し、中央に配置して、サイズを 48dp にします。
Row(
   modifier = modifier
       .fillMaxWidth()
       .size(48.dp),
   verticalAlignment = Alignment.CenterVertically
) {
}
  1. 以下をインポートします。
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
  1. Row レイアウト コンポーザブルのラムダブロックに、R.string.round_up_tip 文字列リソースを使用して Round up tip? 文字列を表示する Text コンポーザブルを追加します。
Text(text = stringResource(R.string.round_up_tip))
  1. Text コンポーザブルの後に Switch コンポーザブルを追加します。checked 名前付きパラメータを roundUp に設定し、onCheckedChange 名前付きパラメータを onRoundUpChanged に設定して渡します。
Switch(
    checked = roundUp,
    onCheckedChange = onRoundUpChanged,
)

次の表に、RoundTheTipRow() 関数で定義したものと同じパラメータに関する情報が含まれています。

パラメータ

説明

checked

スイッチがオンになっているかどうか。これは Switch コンポーザブルの状態です。

onCheckedChange

スイッチがクリックされたときに呼び出されるコールバック。

  1. 以下をインポートします。
import androidx.compose.material3.Switch
  1. RoundTheTipRow() 関数に、Boolean 型の roundUp パラメータと、Boolean を受け取って何も返さない onRoundUpChanged ラムダ関数を追加します。
@Composable
fun RoundTheTipRow(
    roundUp: Boolean,
    onRoundUpChanged: (Boolean) -> Unit,
    modifier: Modifier = Modifier
)

これにより、スイッチの状態がホイスティングされます。

  1. Switch コンポーザブルに、この modifier を追加して、Switch コンポーザブルを画面の末端に配置します。
       Switch(
           modifier = modifier
               .fillMaxWidth()
               .wrapContentWidth(Alignment.End),
           //...
       )
  1. 以下をインポートします。
import androidx.compose.foundation.layout.wrapContentWidth
  1. TipTimeLayout() 関数に、Switch コンポーザブルの状態の var 変数を追加します。roundUp という var 変数を作成して mutableStateOf() に設定し、初期値として false を設定します。呼び出しを remember { } で囲みます。
fun TipTimeLayout() {
    //...
    var roundUp by remember { mutableStateOf(false) }

    //...
    Column(
        ...
    ) {
      //...
   }
}

これは Switch コンポーザブルの状態の変数であり、false がデフォルトの状態になります。

  1. TipTimeLayout() 関数の Column ブロック内([Tip Percentage] テキスト フィールドの後)。次の引数を持つ RoundTheTipRow() 関数を呼び出します。roundUp に設定された名前付きパラメータ roundUproundUp 値を更新するラムダ コールバックに設定された 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?] 行が表示されます。

  1. アプリを実行します。アプリに [Round up tip?] の切り替えが表示されます。

5225395a29022a5e.png

  1. 請求額とチップ率を入力し、[Round up tip?] の切り替えを選択します。チップの金額は四捨五入されませんが、これはまだ calculateTip() 関数を更新する必要があるためです(次のセクションで行います)。

チップを四捨五入するように calculateTip() 関数を更新する

Boolean 変数を受け入れてチップを最も近い整数に切り上げるように、calculateTip() 関数を変更します。

  1. チップを切り上げるには、calculateTip() 関数がスイッチの状態(Boolean)を把握する必要があります。calculateTip() 関数に、Boolean 型の roundUp パラメータを追加します。
private fun calculateTip(
    amount: Double,
    tipPercent: Double = 15.0,
    roundUp: Boolean
): String {
    //...
}
  1. calculateTip() 関数の return ステートメントの前に、roundUp の値をチェックする if() 条件を追加します。roundUptrue であれば、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)
}
  1. TipTimeLayout() 関数で、calculateTip() 関数呼び出しを更新し、roundUp パラメータを渡します。
val tip = calculateTip(amount, tipPercent, roundUp)
  1. アプリを実行します。次の画像のように、チップ金額が切り上げられるようになりました。

7. 横向きのサポートを追加する

Android デバイスには、スマートフォン、タブレット、折りたたみ式デバイス、ChromeOS デバイスなど、さまざまなフォーム ファクタがあり、広範な画面サイズが用意されています。アプリは、縦向きと横向きの両方をサポートする必要があります。

  1. アプリを横向きでテストし、自動回転をオンにします。

8566fc367d5a5b2f.png

  1. エミュレータまたはデバイスを左に回転させると、チップの金額が表示されません。この問題を解決するには、アプリ画面をスクロールできる縦方向のスクロールバーが必要です。

28d23a73c2a5ea24.png

  1. 修飾子に .verticalScroll(rememberScrollState()) を追加して、列を縦方向にスクロールできるようにします。rememberScrollState() はスクロール状態を作成し、自動的に記憶します。
@Composable
fun TipTimeLayout() {
    // ...
    Column(
        modifier = Modifier
            .padding(40.dp)
            .verticalScroll(rememberScrollState()),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        //...
    }
}
  1. 以下をインポートします。
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
  1. アプリを再度実行します。横表示でスクロールしてみましょう。

179866a0fae00401.gif

8. テキスト フィールドに先頭のアイコンを追加する(省略可)

アイコンを使用すると、テキスト フィールドをより視覚的にアピールし、テキスト フィールドに関する追加情報を提供できます。想定されるデータの種類や必要な入力の種類など、テキスト フィールドの目的をアイコンで示すことができます。たとえば、テキスト フィールドの横にある電話のアイコンは、ユーザーが電話番号を入力することが期待されていることを示します。

アイコンを使用して、ユーザーに期待する内容を視覚的に伝えることで入力を促すことができます。たとえば、テキスト フィールドの横にあるカレンダー アイコンは、ユーザーが日付を入力することが期待されていることを示します。

以下に、検索キーワードを入力することを示す検索アイコンを含むテキスト フィールドの例を示します。

9318c9a2414c4add.png

EditNumberField() コンポーザブルに、Int 型の leadingIcon という別のパラメータを追加します。@DrawableRes でアノテーションを付けます。

@Composable
fun EditNumberField(
    @StringRes label: Int,
    @DrawableRes leadingIcon: Int,
    keyboardOptions: KeyboardOptions,
    value: String,
    onValueChanged: (String) -> Unit,
    modifier: Modifier = Modifier
)
  1. 以下をインポートします。
import androidx.annotation.DrawableRes
import androidx.compose.material3.Icon
  1. テキスト フィールドに先頭のアイコンを追加します。leadingIcon はコンポーザブルを受け取り、次の Icon コンポーザブルを渡します。
TextField(
    value = value,
    leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) },
    //...
)
  1. 先頭のアイコンをテキスト フィールドに渡します。便宜上、アイコンはすでにスターター コード内にあります。
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
)
  1. アプリを実行します。

bff007b9d67ede83.png

おめでとうございます。これで、アプリでカスタムのチップ金額を計算できるようになりました。

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 を付けて、ソーシャル メディアで共有しましょう。

関連リンク