1. 事前準備
在本程式碼研究室中,您將使用「Compose 中的狀態簡介」程式碼研究室的解決方案程式碼,建構互動式小費計算機。使用者輸入帳單金額和小費百分比後,可以用來自動計算小費並四捨五入。最終版應用程式如下圖所示:
必要條件
- 完成「Compose 中的狀態簡介」程式碼研究室。
- 可在應用程式中新增
Text
和TextField
可組合函式。 - 具備
remember()
函式、狀態、狀態提升的相關知識,並瞭解有狀態與無狀態可組合函式之間的差異。
課程內容
- 如何在虛擬鍵盤上新增動作按鈕。
Switch
可組合函式的說明和使用方法。- 在文字欄位中加入前置圖示。
建構項目
- Tip Time 應用程式,可根據使用者輸入的帳單金額和小費百分比計算小費金額。
軟硬體需求
- 最新版 Android Studio
- 「Compose 中的狀態簡介」程式碼研究室中的解決方案程式碼
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 應用程式
- 在 Android Studio 中開啟 Tip Time 專案,並在模擬器或裝置上執行應用程式。
- 輸入帳單金額。應用程式會自動計算並顯示小費金額。
在目前的實作項目中,小費百分比是以硬式編碼的方式設為 15%。在本程式碼研究室中,您將透過文字欄位來擴充這項功能,讓應用程式依據自訂小費百分比計算並將小費金額四捨五入。
新增必要的字串資源
- 在「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
檔案應該如以下程式碼片段所示,其中包含先前程式碼研究室中的字串:
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()
可組合函式。在先前的程式碼研究室中,您已將 amountInput
狀態從 EditNumberField()
可組合項提升為 TipTimeLayout()
可組合項,讓 EditNumberField()
成為無狀態的可組合項。
如要新增文字欄位,可以重複使用相同的 EditNumberField()
可組合項,但搭配不同的標籤。如要進行這項變更,您必須將標籤做為參數傳遞,而不是在 EditNumberField()
可組合函式中對標籤進行硬式編碼。
將 EditNumberField()
可組合函式設為可重複使用:
- 在
EditNumberField()
可組合函式參數的MainActivity.kt
檔案中,新增Int
類型的label
字串資源:
@Composable
fun EditNumberField(
label: Int,
value: String,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier
)
- 在函式主體中,以
label
參數取代硬式編碼的字串資源 ID:
@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
lambda 運算式中的tipInput
變數:
EditNumberField(
label = R.string.how_was_the_service,
value = tipInput,
onValueChanged = { tipInput = it },
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth()
)
- 在
tipInput
變數定義後方的TipTimeLayout()
函式中,定義可將tipInput
變數轉換為Double
類型的val
,並命名為tipPercent
。接著使用 Elvis 運算子並傳回0
(如果值為null
)。如果文字欄位為空白,傳回的值可能是null
。
val tipPercent = tipInput.toDoubleOrNull() ?: 0.0
- 在
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))
}
}
- 在模擬器或裝置上執行應用程式,然後輸入帳單金額和小費百分比。應用程式是否正確計算小費金額?
5. 設定動作按鈕
在先前的程式碼研究室中,您已瞭解如何使用 KeyboardOptions
類別設定鍵盤類型。在本節中,您將瞭解如何使用相同的 KeyboardOptions
設定鍵盤動作按鈕。鍵盤動作按鈕是指鍵盤末端的按鈕,您可以在下表中看到一些範例:
屬性 | 鍵盤上的動作按鈕 |
| |
| |
|
在這項工作中,您會為文字方塊設定兩個不同的動作按鈕:
- 「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」動作按鈕。不過,您的目標是為文字欄位提供兩個不同的動作按鈕。我們稍後就會修正這個問題。
- 檢查
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
),
// ...
)
- 在第二個
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. 新增切換鈕
切換鈕可以切換單一項目的開啟或關閉狀態。
切換鈕提供兩種狀態,可讓使用者選取其中一種。切換鈕是由軌道、指標和選用圖示組成,如下圖所示:
切換鈕是一種選取控制項,可用來輸入決定或宣告偏好設定,如下圖中的設定內容:
使用者可以來回拖曳「指標」來選取選項,或直接輕觸切換鈕來變更狀態。下方 GIF 中的切換鈕為另一個例子,其中的視覺選項設定會切換為「Dark mode」:
如要進一步瞭解切換鈕,請參閱「切換鈕」說明文件。
您可以使用 Switch
可組合函式,讓使用者選擇是否要將小費無條件進位至最接近的整數,如下圖所示:
為 Text
和 Switch
可組合函式新增一列:
- 在
EditNumberField()
函式後方新增RoundTheTipRow()
可組合函式,然後傳入預設的Modifier
,做為類似EditNumberField()
函式的引數:
@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
版面配置可組合項的 lambda 區塊中,新增使用R.string.round_up_tip
字串資源的Text
可組合項,即可顯示Round up tip?
字串:
Text(text = stringResource(R.string.round_up_tip))
- 在
Text
可組合項後方新增Switch
可組合項,並傳遞設為roundUp
的checked
具名參數以及設為onRoundUpChanged
的onCheckedChange
具名參數。
Switch(
checked = roundUp,
onCheckedChange = onRoundUpChanged,
)
下表包含上述參數的資訊,這些參數與您為 RoundTheTipRow()
函式定義的參數相同:
參數 | 說明 |
| 是否已勾選切換鈕。這是 |
| 使用者點選切換鈕時要呼叫的回呼。 |
- 匯入下列內容:
import androidx.compose.material3.Switch
- 在
RoundTheTipRow()
函式中新增Boolean
類型的roundUp
參數,以及可接受Boolean
且不會傳回任何結果的onRoundUpChanged
lambda 函式:
@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。
- 在「Tip Percentage」文字欄位後方的
TipTimeLayout()
函式Column
區塊中,呼叫含有以下引數的RoundTheTipRow()
函式:設為roundUp
的roundUp
命名參數,以及設為 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?」列。
- 執行應用程式。應用程式會顯示「Round up tip?」切換按鈕。
- 輸入帳單金額和小費百分比,然後選取「Round up tip?」切換按鈕。這時小費金額並不會四捨五入,如果有此需求,請繼續依下一節說明更新
calculateTip()
函式。
更新 calculateTip()
函式將小費四捨五入
請修改 calculateTip()
函式以接受 Boolean
變數,將小費無條件進位至最接近的整數:
- 如要將小費四捨五入,
calculateTip()
函式必須知道切換按鈕的狀態,這是一個Boolean
值。請在calculateTip()
函式中新增Boolean
類型的roundUp
參數:
private fun calculateTip(
amount: Double,
tipPercent: Double = 15.0,
roundUp: Boolean
): String {
//...
}
- 在
calculateTip()
函式的return
陳述式前方,新增if()
條件來檢查roundUp
值。如果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()
可組合函式中新增另一個名為 leadingIcon
的參數,類型為 Int
,並加上 @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. 取得解決方案程式碼
完成程式碼研究室後,如要下載當中用到的程式碼,您可以使用以下 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 主題標記!