計算自訂小費

1. 事前準備

在本程式碼研究室中,您將使用「Compose 中的狀態簡介」程式碼研究室的解決方案程式碼,建構互動式小費計算機。使用者輸入帳單金額和小費百分比後,可以用來自動計算小費並四捨五入。最終版應用程式如下圖所示:

d8e768525099378a.png

必要條件

  • 完成「Compose 中的狀態簡介」程式碼研究室。
  • 可在應用程式中新增 TextTextField 可組合函式。
  • 具備 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. 範例應用程式總覽

本程式碼研究室會從先前的「Compose 中的狀態簡介」程式碼研究室提供的 Tip Time 應用程式開始談起,該應用程式提供以固定費率計算小費金額的使用者介面。使用者可以在「Bill amount」文字方塊中輸入服務費用。接著應用程式會在 Text 可組合函式中計算並顯示小費金額。

執行 Tip Time 應用程式

  1. 在 Android Studio 中開啟 Tip Time 專案,並在模擬器或裝置上執行應用程式。
  2. 輸入帳單金額。應用程式會自動計算並顯示小費金額。

b6bd5374911410ac.png

在目前的實作項目中,小費百分比是以硬式編碼的方式設為 15%。在本程式碼研究室中,您將透過文字欄位來擴充這項功能,讓應用程式依據自訂小費百分比計算並將小費金額四捨五入。

新增必要的字串資源

  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 檔案應該如以下程式碼片段所示,其中包含先前程式碼研究室中的字串:

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() 可組合函式。在先前的程式碼研究室中,您已將 amountInput 狀態從 EditNumberField() 可組合項提升為 TipTimeLayout() 可組合項,讓 EditNumberField() 成為無狀態的可組合項。

如要新增文字欄位,可以重複使用相同的 EditNumberField() 可組合項,但搭配不同的標籤。如要進行這項變更,您必須將標籤做為參數傳遞,而不是在 EditNumberField() 可組合函式中對標籤進行硬式編碼。

EditNumberField() 可組合函式設為可重複使用:

  1. EditNumberField() 可組合函式參數的 MainActivity.kt 檔案中,新增 Int 類型的 label 字串資源:
@Composable
fun EditNumberField(
    label: Int,
    value: String,
    onValueChanged: (String) -> Unit,
    modifier: Modifier = Modifier
)
  1. 在函式主體中,以 label 參數取代硬式編碼的字串資源 ID:
@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() 可組合函式頂端,為新增的文字欄位狀態變數加上名為 tipInputvar 屬性。接著,使用 mutableStateOf("") 將變數初始化,並將該呼叫放入 remember 函式中:
var tipInput by remember { mutableStateOf("") }
  1. 在新的 EditNumberField() 函式呼叫中,將 value 命名參數設為 tipInput 變數,然後更新 onValueChanged lambda 運算式中的 tipInput 變數:
EditNumberField(
    label = R.string.how_was_the_service,
    value = tipInput,
    onValueChanged = { tipInput = it },
    modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth()
)
  1. tipInput 變數定義後方的 TipTimeLayout() 函式中,定義可將 tipInput 變數轉換為 Double 類型的 val,並命名為 tipPercent。接著使用 Elvis 運算子並傳回 0 (如果值為 null)。如果文字欄位為空白,傳回的值可能是 null
val tipPercent = tipInput.toDoubleOrNull() ?: 0.0
  1. TipTimeLayout() 函式中更新 calculateTip() 函式呼叫,並將 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. 設定動作按鈕

在先前的程式碼研究室中,您已瞭解如何使用 KeyboardOptions 類別設定鍵盤類型。在本節中,您將瞭解如何使用相同的 KeyboardOptions 設定鍵盤動作按鈕。鍵盤動作按鈕是指鍵盤末端的按鈕,您可以在下表中看到一些範例:

屬性

鍵盤上的動作按鈕

ImeAction.Search
用於讓使用者執行搜尋。

圖中顯示的是用於執行搜尋的「搜尋」圖示。

ImeAction.Send
用於讓使用者傳送輸入欄位中的文字。

圖中顯示的是用於傳送輸入欄位文字的「傳送」圖示。

ImeAction.Go
用於讓使用者前往所輸入文字的目標。

圖中顯示的是「Go」圖示,可前往所輸入文字的目標。

在這項工作中,您會為文字方塊設定兩個不同的動作按鈕:

  • 「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」動作按鈕。不過,您的目標是為文字欄位提供兩個不同的動作按鈕。我們稍後就會修正這個問題。

  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. 在第二個 EditNumberField() 函式呼叫中,將「Tip Percentage」文字欄位的 imeAction 變更為 ImeAction.Done。函式應如以下程式碼片段所示:
EditNumberField(
    label = R.string.how_was_the_service,
    keyboardOptions = KeyboardOptions.Default.copy(
        keyboardType = KeyboardType.Number,
        imeAction = ImeAction.Done
    ),
    // ...
)
  1. 執行應用程式。畫面會隨即顯示「Next」和「Done」動作按鈕,如下圖所示:

  1. 輸入帳單金額並點選「Next」動作按鈕,然後輸入小費百分比並點選「Done」動作按鈕。這麼做會關閉撥號鍵盤。

a9e3fbddfff829c8.gif

6. 新增切換鈕

切換鈕可以切換單一項目的開啟或關閉狀態。

6923dfb1101602c7.png

切換鈕提供兩種狀態,可讓使用者選取其中一種。切換鈕是由軌道、指標和選用圖示組成,如下圖所示:

b4f7f68b848bcc2b.png

切換鈕是一種選取控制項,可用來輸入決定或宣告偏好設定,如下圖中的設定內容:

5cd8acb912ab38eb.png

使用者可以來回拖曳「指標」來選取選項,或直接輕觸切換鈕來變更狀態。下方 GIF 中的切換鈕為另一個例子,其中的視覺選項設定會切換為「Dark mode」

eabf96ad496fd226.gif

如要進一步瞭解切換鈕,請參閱「切換鈕」說明文件。

您可以使用 Switch 可組合函式,讓使用者選擇是否要將小費無條件進位至最接近的整數,如下圖所示:

b42af9f2d3861e4.png

TextSwitch 可組合函式新增一列:

  1. EditNumberField() 函式後方新增 RoundTheTipRow() 可組合函式,然後傳入預設的 Modifier,做為類似 EditNumberField() 函式的引數:
@Composable
fun RoundTheTipRow(modifier: Modifier = Modifier) {
}
  1. 實作 RoundTheTipRow() 函式,新增含有以下 modifierRow 版面配置可組合項,將子元素的寬度設為畫面最大寬度、置中對齊,並確保大小為 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 版面配置可組合項的 lambda 區塊中,新增使用 R.string.round_up_tip 字串資源的 Text 可組合項,即可顯示 Round up tip? 字串:
Text(text = stringResource(R.string.round_up_tip))
  1. Text 可組合項後方新增 Switch 可組合項,並傳遞設為 roundUpchecked 具名參數以及設為 onRoundUpChangedonCheckedChange 具名參數。
Switch(
    checked = roundUp,
    onCheckedChange = onRoundUpChanged,
)

下表包含上述參數的資訊,這些參數與您為 RoundTheTipRow() 函式定義的參數相同:

參數

說明

checked

是否已勾選切換鈕。這是 Switch 可組合項的狀態。

onCheckedChange

使用者點選切換鈕時要呼叫的回呼。

  1. 匯入下列內容:
import androidx.compose.material3.Switch
  1. RoundTheTipRow() 函式中新增 Boolean 類型的 roundUp 參數,以及可接受 Boolean 且不會傳回任何結果的 onRoundUpChanged lambda 函式:
@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 變數。建立名為 roundUpvar 變數並設定至 mutableStateOf(),然後將初始值則設為 false。接著將呼叫放入 remember { }
fun TipTimeLayout() {
    //...
    var roundUp by remember { mutableStateOf(false) }

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

這是 Switch 可組合函式狀態的變數,預設狀態為 false。

  1. 在「Tip Percentage」文字欄位後方的 TipTimeLayout() 函式 Column 區塊中,呼叫含有以下引數的 RoundTheTipRow() 函式:設為 roundUproundUp 命名參數,以及設為 lambda 回呼 (可更新 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?」列。

  1. 執行應用程式。應用程式會顯示「Round up tip?」切換按鈕。

5225395a29022a5e.png

  1. 輸入帳單金額和小費百分比,然後選取「Round up tip?」切換按鈕。這時小費金額並不會四捨五入,如果有此需求,請繼續依下一節說明更新 calculateTip() 函式。

更新 calculateTip() 函式將小費四捨五入

請修改 calculateTip() 函式以接受 Boolean 變數,將小費無條件進位至最接近的整數:

  1. 如要將小費四捨五入,calculateTip() 函式必須知道切換按鈕的狀態,這是一個 Boolean 值。請在 calculateTip() 函式中新增 Boolean 類型的 roundUp 參數:
private fun calculateTip(
    amount: Double,
    tipPercent: Double = 15.0,
    roundUp: Boolean
): String { 
    //...
}
  1. calculateTip() 函式的 return 陳述式前方,新增 if() 條件來檢查 roundUp 值。如果 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()可組合函式中新增另一個名為 leadingIcon 的參數,類型為 Int,並加上 @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. 取得解決方案程式碼

完成程式碼研究室後,如要下載當中用到的程式碼,您可以使用以下 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 主題標記!

瞭解詳情