1. 事前準備
在這個程式碼研究室中,我們會說明一些與狀態相關的知識,以及如何透過 Jetpack Compose 使用及管控狀態。
從核心角度而言,應用程式中的狀態指任何可能隨時間變化的值。這個定義相當廣泛,包含應用程式中從資料庫到變數等一切元素。後續單元將進一步介紹資料庫,但目前您需要瞭解的是,資料庫是經過精心整理的結構化資訊集,例如電腦上的檔案。
所有 Android 應用程式都會向使用者顯示狀態。舉例來說,Android 應用程式可能會顯示以下幾種狀態:
- 無法建立網路連線時顯示的訊息。
- 表單,例如註冊表單。狀態可能是「已填寫」和「已提交」。
- 觸控式控制項,例如按鈕。狀態可能是「未輕觸」、「輕觸中」(顯示動畫) 或「已輕觸」(
onClick
動作)。
在本程式碼研究室中,您將探索如何使用 Compose,並思考使用 Compose 時的狀態。為此,您可以使用下列內建的 Compose UI 元素,建構 Tip Time 小費計算機應用程式:
- 用於輸入及編輯文字的
TextField
可組合函式。 - 用於顯示文字的
Text
可組合函式。 - 用於在 UI 元素之間顯示空白空間的
Spacer
可組合函式。
完成這個程式碼研究室時,您就會建構出互動式小費計算機,可在輸入服務金額時自動計算小費金額。最終應用程式的外觀如下圖所示:
必要條件
- 瞭解 Compose 的基本知識,例如
@Composable
註解。 - 熟悉 Compose 版面配置的基本知識,例如
Row
和Column
版面配置可組合函式。 - 熟悉修飾符的基本知識,例如
Modifier.padding()
函式。 - 熟悉
Text
可組合函式。
課程內容
- 如何考量 UI 中的狀態。
- Compose 如何使用狀態顯示資料。
- 如何在應用程式中加入文字方塊。
- 如何提升狀態。
建構項目
- 小費計算機應用程式 Tip Time,可根據服務金額計算小費金額。
軟硬體需求
- 一台可連上網際網路並具備網路瀏覽器的電腦
- Kotlin 知識
- 最新版 Android Studio
2. 立即開始
- 請查看 Google 線上小費計算機。請注意,這只是範例,並非您稍後要在本課程中建立的 Android 應用程式。
- 在「Bill」和「Tip %」方塊中輸入不同的值。小費和總金額也會隨之改變。
請注意,當您輸入值時,「Tip」和「Total」會隨之更新。完成下列程式碼研究室之後,您將在 Android 中開發類似的小費計算機應用程式。
在本課程中,您將建立一個簡單的小費計算機 Android 應用程式。
開發人員的工作方式通常如下:先簡單開發一款可以正常使用的應用程式 (即使看起來不甚理想),接著再加入更多功能並提升外觀吸引力。
完成本程式碼研究室之後,您的小費計算機應用程式將如以下螢幕截圖所示。使用者輸入帳單金額時,應用程式就會顯示建議的小費金額。目前,小費百分比是以硬式編碼設為 15%。在下一個程式碼研究室中,您將可以繼續使用應用程式並新增更多功能,例如設定自訂小費百分比。
3. 取得範例程式碼
範例程式碼是預先編寫的程式碼,可用來開始新專案,也可以協助您專注在本程式碼研究室介紹的新概念。
從這裡下載範例程式碼,即可開始操作:
或者,您也可以複製 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 starter
您可以瀏覽 TipTime
GitHub 存放區中的範例程式碼。
範例應用程式總覽
若要瞭解範例程式碼,請完成下列步驟:
- 在 Android Studio 中開啟含有範例程式碼的專案。
- 在 Android 裝置或模擬器上執行應用程式。
- 您會注意到兩個文字元件,一個用於標籤,另一個用於顯示小費金額。
範例程式碼逐步操作說明
範例程式碼含有文字可組合函式。在本課程中,您將新增文字欄位,方便使用者輸入內容。以下是部分檔案的簡要逐步操作說明,可協助您快速上手。
res > values > 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="tip_amount">Tip Amount: %s</string>
</resources>
這是資源中的 string.xml
檔案,其中包含您將用於此應用程式的所有字串。
MainActivity
此檔案主要包含範本產生的程式碼和下列函式。
TipTimeLayout()
函式包含Column
元素,且具有兩個文字可組合函式,如螢幕截圖所示。此外,這也具有spacer
可組合函式,可加入空白來提升美感。calculateTip()
函式可接受帳單金額,並計算 15% 小費金額。tipPercent
參數已設為15.0
預設引數值。這會將預設小費百分比設為 15%。在下一個程式碼研究室中,您將取得使用者提供的小費金額:
@Composable
fun TipTimeLayout() {
Column(
modifier = Modifier
.statusBarsPadding()
.padding(horizontal = 40.dp)
.verticalScroll(rememberScrollState())
.safeDrawingPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = stringResource(R.string.calculate_tip),
modifier = Modifier
.padding(bottom = 16.dp, top = 40.dp)
.align(alignment = Alignment.Start)
)
Text(
text = stringResource(R.string.tip_amount, "$0.00"),
style = MaterialTheme.typography.displaySmall
)
Spacer(modifier = Modifier.height(150.dp))
}
}
private fun calculateTip(amount: Double, tipPercent: Double = 15.0): String {
val tip = tipPercent / 100 * amount
return NumberFormat.getCurrencyInstance().format(tip)
}
在 onCreate()
函式的 Surface()
區塊中,系統會呼叫 TipTimeLayout()
函式,即可在裝置或模擬器中顯示應用程式的版面配置。
override fun onCreate(savedInstanceState: Bundle?) {
//...
setContent {
TipTimeTheme {
Surface(
//...
) {
TipTimeLayout()
}
}
}
}
在 TipTimeLayoutPreview()
函式的 TipTimeTheme
區塊,系統會呼叫 TipTimeLayout()
函式,以便在「Design」和「Split」窗格中顯示應用程式的版面配置。
@Preview(showBackground = true)
@Composable
fun TipTimeLayoutPreview() {
TipTimeTheme {
TipTimeLayout()
}
}
取得使用者輸入內容
在本節中,您可以新增 UI 元素,讓使用者在應用程式中輸入帳單金額。這項 UI 元素的外觀如下圖所示:
您的應用程式採用自訂樣式和主題。
樣式和主題是一組屬性,可指定單一 UI 元素的外觀。樣式可指定各種屬性,例如字型顏色、字型大小、背景顏色等,也能套用到整個應用程式。後續程式碼研究室將介紹如何在應用程式中實作這些屬性。目前已經替您完成這些實作項目,讓應用程式更美觀。
為協助您進一步瞭解,以下會並排比較加入/未加入自訂主題的應用程式解決方案版本。
未加入自訂主題。 | 加入自訂主題。 |
TextField
可組合函式可讓使用者在應用程式內輸入文字。例如,請注意下圖中 Gmail 應用程式登入畫面的文字方塊:
在應用程式中新增 TextField
可組合函式:
- 在
MainActivity.kt
檔案中,新增採用Modifier
參數的EditNumberField()
可組合函式。 - 在
TipTimeLayout()
下方EditNumberField()
函式的內文中新增TextField
,用於接受設為空字串的value
參數,以及設為空白 lambda 運算式的onValueChange
參數:
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
TextField(
value = "",
onValueChange = {},
modifier = modifier
)
}
- 請注意您傳遞的參數:
value
參數是文字方塊,會顯示您在這裡傳遞的字串值。onValueChange
參數是 lambda 回呼,會在使用者於文字方塊中輸入文字時觸發。
- 匯入這個函式:
import androidx.compose.material3.TextField
- 在
TipTimeLayout()
可組合函式中第一個文字可組合函式後方該行,呼叫EditNumberField()
函式並傳遞下列修飾符。
import androidx.compose.foundation.layout.fillMaxWidth
@Composable
fun TipTimeLayout() {
Column(
modifier = Modifier
.statusBarsPadding()
.padding(horizontal = 40.dp)
.verticalScroll(rememberScrollState())
.safeDrawingPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
...
)
EditNumberField(modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth())
Text(
...
)
...
}
}
畫面上會隨即顯示文字方塊。
- 在「Design」窗格中,您應該會看到
Calculate Tip
文字、空白文字方塊和Tip Amount
文字可組合函式。
4. 使用 Compose 中的狀態
應用程式中的狀態是指任何可能隨時間變化的值。在這個應用程式中,狀態是帳單金額。
為儲存狀態,請新增變數:
- 在
EditNumberField()
函式的開頭,使用val
關鍵字新增amountInput
變數,並設為"0"
值:
val amountInput = "0"
這是用於帳單金額的應用程式狀態。
- 將
value
參數設為amountInput
值:
TextField(
value = amountInput,
onValueChange = {},
)
- 查看預覽畫面。文字方塊會顯示設定的狀態變數值,如下圖所示:
- 在模擬器中執行應用程式,嘗試輸入不同的值。
TextField
可組合函式不會自行更新,因此硬式編碼狀態仍維持不變。當value
參數變更 (設為amountInput
屬性) 時,這個可組合函式會隨之更新。
amountInput
變數代表文字方塊的狀態。硬式編碼狀態無法修改,也無法反映使用者輸入內容,因此並不實用。當使用者更新帳單金額時,您需要更新應用程式的狀態。
5. 組成
應用程式中可組合函式描述的 UI 會顯示一欄,當中包含部分文字、一個空格字元和一個文字方塊。該文字會顯示 Calculate Tip
文字,文字方塊則顯示 0
值或任何預設值。
Compose 是宣告式 UI 架構,代表您可以在程式碼中「宣告」UI 的外觀。如果您想讓文字方塊一開始就顯示 100
值,請在程式碼中將可組合函式的初始值設為 100
值。
如果您希望在應用程式執行期間或使用者與應用程式互動時變更 UI,該怎麼做?例如,如果您想將 amountInput
變數更新為使用者輸入的值,並在文字方塊中顯示這個值,該怎麼做?這時,您需要重新組合這個過程來更新應用程式的組成。
「組成」是指 Compose 在執行可組合函式時建構的 UI 描述。Compose 應用程式會呼叫可組合函式,將資料轉換為 UI。如果狀態變更,Compose 會以新的狀態重新執行受影響的可組合函式,進而建立新的 UI,這個過程稱為「重新組成」。Compose 會為您建立「重新組成」排程。
當 Compose 在「初始組成」期間首次執行可組合函式時,系統會追蹤您呼叫的可組合函式,以便在組成內容中描述您的 UI。「重新組成」是指 Compose 重新執行可能因資料變更而發生變化的可組合函式,然後更新組成以反映任何變更。
組成只能由「初始組成」產生,並由「重新組成」更新。修改組成的唯一方式是執行「重新組成」。因此,Compose 必須知道目標追蹤狀態,以便在收到更新時建立重新組成排程。這時,您要追蹤的狀態為 amountInput
變數,因此每當值改變時,Compose 都會建立重新組成排程。
您可以在 Compose 中使用 State
和 MutableState
類型,以允許 Compose 觀察或追蹤應用程式中的狀態。State
類型不可變動,因此您只能讀取當中的值,而 MutableState
類型可變動。您可以使用 mutableStateOf()
函式建立可觀察的 MutableState
。該函式會接收做為參數封裝在 State
物件中的初始值,使 value
變為可觀察狀態。
mutableStateOf()
函式傳回的值:
- 會保留狀態,也就是帳單金額。
- 可變動,因此這個值可以變更。
- 可觀察,也就是 Compose 會觀察這個值發生的任何變更,並觸發重新組成以更新 UI。
新增服務費狀態:
- 在
EditNumberField()
函式中,將amountInput
狀態變數之前的val
關鍵字變更為var
關鍵字:
var amountInput = "0"
這會將 amountInput
設為可變動。
- 使用
MutableState<String>
類型 (而非硬式編碼的String
變數),讓 Compose 知道要追蹤amountInput
狀態並傳入"0"
字串,也就是amountInput
狀態變數的初始預設值:
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
var amountInput: MutableState<String> = mutableStateOf("0")
amountInput
初始化程序也能以類型推論的方式編寫,如下所示:
var amountInput = mutableStateOf("0")
mutableStateOf()
函式會接受初始值 "0"
做為引數,接著這個引數將讓 amountInput
變成可觀察狀態。這會導致 Android Studio 發出以下編譯警告,但您很快就能修正問題:
Creating a state object during composition without using remember.
- 在
TextField
可組合函式中,使用amountInput.value
屬性:
TextField(
value = amountInput.value,
onValueChange = {},
modifier = modifier
)
Compose 會追蹤每個可讀取狀態 value
屬性的可組合函式,並在 value
變更時觸發重新組成。
當文字方塊的輸入內容有所變更時,就會觸發 onValueChange
回呼。在 lambda 運算式中,it
變數包含新值。
- 在
onValueChange
命名參數的 lambda 運算式中,將amountInput.value
屬性設為it
變數:
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
var amountInput = mutableStateOf("0")
TextField(
value = amountInput.value,
onValueChange = { amountInput.value = it },
modifier = modifier
)
}
當 TextField
透過 onValueChange
回呼函式通知您文本發生變更時,您將更新TextField
狀態 (也就是 amountInput
變數)。
- 執行應用程式,並在文字方塊中輸入文字。如下圖所示,文字方塊仍然會顯示
0
值:
使用者在文字方塊中輸入文字時,系統會呼叫 onValueChange
回呼,並將 amountInput
變數更新為新值。amountInput
狀態由 Compose 進行追蹤,因此當其值變更時,Compose 將建立重新組成排程並再次執行 EditNumberField()
可組合函式。在這個可組合函式中,amountInput
變數會重設為初始值 0
。因此,文字方塊會顯示 0
值。
在您新增程式碼後,狀態變更會導致系統建立重新組成排程。
不過,您必須設法在重新組成後保留 amountInput
變數的值,以免 EditNumberField()
函式每次重新組成時,該變數值都會重設為 0
值。您將在下一節中解決這個問題。
6. 使用 remember 函式儲存狀態
執行重新組成期間,您可以多次呼叫可組合方法。如果未儲存,這些可組合函式會在重新組成期間重設狀態。
可組合函式可以在重新組成期間使用 remember
儲存物件。remember
函式計算的值會在初始組成期間儲存在「組成」中,並在重新組成時傳回所儲存的值。您通常可以在可組合函式中搭配使用 remember
和 mutableStateOf
函式,使狀態及其更新內容正確反映在 UI 中。
在 EditNumberField()
函式中,使用 remember
函式:
- 在
EditNumberField()
函式中,使用remember
括住mutableStateOf
()
函式呼叫,以便透過by
remember
Kotlin 資源委派初始化amountInput
變數。 - 在
mutableStateOf
()
函式中,傳入空字串 (而非靜態"0"
字串):
var amountInput by remember { mutableStateOf("") }
現在,空字串是 amountInput
變數的初始預設值。by
是 Kotlin 資源委派。amountInput
屬性的預設 getter 和 setter 函式會分別委派至 remember
類別的 getter 和 setter 函式。
- 匯入以下函式:
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
新增委派的 getter 和 setter 匯入項目後,不必參照 MutableState
的 value
屬性,即可讀取及設定 amountInput
。
更新後的 EditNumberField()
函式應如下所示:
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
var amountInput by remember { mutableStateOf("") }
TextField(
value = amountInput,
onValueChange = { amountInput = it },
modifier = modifier
)
}
- 執行應用程式,並在文字方塊中輸入一些文字。您現在應該可以看到輸入的文字。
7. 狀態與重新組成實際使用教學
在本節中,您將設定中斷點並對 EditNumberField()
可組合函式進行偵錯,以便瞭解初始組成和重新組成的運作方式。
設定中斷點,在模擬器或裝置上對應用程式進行偵錯:
- 在
onValueChange
命名參數旁邊的EditNumberField()
函式中,設定行中斷點。 - 在導覽選單中,按一下「Debug 'app'」(對應用程式進行偵錯)。應用程式會在模擬器或裝置上啟動。應用程式會在建立
TextField
元素後首次暫停執行。
- 在「Debug」(偵錯) 窗格中,按一下「Continue Program」(繼續執行程式) 圖示 。文字方塊建立完成。
- 在模擬器或裝置的文字方塊中輸入字母。到達您設定的中斷點時,應用程式就會再次暫停執行程序。
您輸入文字時,系統會呼叫 onValueChange
回呼。在 lambda it
中具有您在鍵盤輸入的新值。
將「it」的值指派給 amountInput
後,由於可觀測值有所變更,Compose 會以新資料觸發重組。
- 在「Debug」(偵錯) 窗格中,按一下「Continue Program」(繼續執行程式) 圖示 。在模擬器或裝置上輸入的文字會顯示在包含中斷點的程式碼行旁,如下圖所示:
這就是文字欄位的狀態。
- 按一下「Resume Program」圖示 。輸入的值會顯示在模擬器或裝置上。
8. 修改外觀
在上一節中,您已經瞭解文字欄位的運作方式。在本節中,您將提升 UI。
在文字方塊中新增標籤
每個文字方塊都應該設定一個標籤,以便使用者瞭解可以輸入哪些資訊。下方第一張範例圖片顯示,標籤文字位於文字欄位中間,並與輸入行對齊。第二張範例圖片顯示,當使用者按一下文字方塊輸入文字時,標籤會移至文字方塊的上方。如要進一步瞭解文字欄位圖解,請參閱圖解。
修改 EditNumberField()
函式,以便在文字欄位中新增標籤:
- 在
EditNumberField()
函式的TextField()
可組合函式中,新增一個設為空白 lambda 運算式的label
命名參數:
TextField(
//...
label = { }
)
- 在 lambda 運算式中,呼叫用於接受
stringResource
(R.string.
bill_amount
)
的Text()
函式:
label = { Text(stringResource(R.string.bill_amount)) },
- 在
TextField()
可組合函式中新增singleLine
參數,並將值設為true
:
TextField(
// ...
singleLine = true,
)
這會將文字方塊從多行壓縮成水平捲動的單行。
- 新增
keyboardOptions
命名參數,並設為KeyboardOptions()
:
import androidx.compose.foundation.text.KeyboardOptions
TextField(
// ...
keyboardOptions = KeyboardOptions(),
)
Android 提供選項讓您設定螢幕上顯示的鍵盤,以便輸入數字、電子郵件地址、網址和密碼等內容。如要進一步瞭解其他鍵盤類型,請參閱 KeyboardType。
- 將鍵盤類型設為數字鍵盤,即可輸入數字。為
KeyboardOptions
函式傳遞KeyboardType.Number
命名參數並將值設為keyboardType
:
import androidx.compose.ui.text.input.KeyboardType
TextField(
// ...
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
)
完成的 EditNumberField()
函式應如下列程式碼片段所示:
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
var amountInput by remember { mutableStateOf("") }
TextField(
value = amountInput,
onValueChange = { amountInput = it },
singleLine = true,
label = { Text(stringResource(R.string.bill_amount)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = modifier
)
}
- 執行應用程式。
您可以在此螢幕截圖中查看撥號鍵盤的變化:
9. 顯示小費金額
在本節中,您將實作應用程式的主要功能,也就是計算和顯示小費金額的功能。
在 MainActivity.kt
檔案中,範例程式碼包含 private
calculateTip()
函式。您將使用此函式計算小費金額:
private fun calculateTip(amount: Double, tipPercent: Double = 15.0): String {
val tip = tipPercent / 100 * amount
return NumberFormat.getCurrencyInstance().format(tip)
}
在上述方法中,您會使用 NumberFormat
將小費格式顯示為貨幣。
現在應用程式可以計算小費,但您仍需使用類別設定小費格式並顯示小費。
使用 calculateTip()
函式
使用者在文字欄位可組合函式中輸入的文字將做為 String
傳回到 onValueChange
回呼函式,即使使用者輸入的是數字也是如此。為修正這個問題,您需要轉換 amountInput
值,而該值包含使用者輸入的金額。
- 在
EditNumberField()
可組合函式中,於amountInput
定義後方建立名為amount
的新變數。呼叫amountInput
變數上的toDoubleOrNull
函式,將String
轉換為Double
:
val amount = amountInput.toDoubleOrNull()
toDoubleOrNull()
函式是預先定義的 Kotlin 函式,可將字串剖析為 Double
數字並傳回結果;如果字串並非有效數值,則會傳回 null
。
- 請在陳述式結尾新增
?:
Elvis 運算子,以便在amountInput
為空值時傳回0.0
值:
val amount = amountInput.toDoubleOrNull() ?: 0.0
- 在
amount
變數後,建立另一個名為tip
的val
變數。使用calculateTip()
初始化這個變數,並傳遞amount
參數。
val tip = calculateTip(amount)
EditNumberField()
函式應如下列程式碼片段所示:
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
var amountInput by remember { mutableStateOf("") }
val amount = amountInput.toDoubleOrNull() ?: 0.0
val tip = calculateTip(amount)
TextField(
value = amountInput,
onValueChange = { amountInput = it },
label = { Text(stringResource(R.string.bill_amount)) },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
顯示小費金額計算結果
您已編寫計算小費金額的函式,下一步就是顯示小費金額計算結果:
- 在
Column()
區塊結尾的TipTimeLayout()
函式中,請注意顯示$0.00
的文字可組合函式。您必須將這個值更新為小費金額計算結果。
@Composable
fun TipTimeLayout() {
Column(
modifier = Modifier
.statusBarsPadding()
.padding(horizontal = 40.dp)
.verticalScroll(rememberScrollState())
.safeDrawingPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// ...
Text(
text = stringResource(R.string.tip_amount, "$0.00"),
style = MaterialTheme.typography.displaySmall
)
// ...
}
}
您需要存取 TipTimeLayout()
函式中的 amountInput
變數,才能計算並顯示小費金額。不過,amountInput
變數是 EditNumberField()
可組合函式中定義的文字欄位狀態,因此您目前無法從 TipTimeLayout()
函式呼叫這個變數。下圖說明程式碼的結構:
這個結構不會讓您在新的 Text
可組合函式中顯示小費金額,因為 Text
可組合函式需要存取使用 amountInput
變數計算的 amount
變數。您必須向 TipTimeLayout()
函式公開 amount
變數。下圖為所需的程式碼結構,可將 EditNumberField()
可組合函式設為無狀態:
這個模式稱為「狀態提升」。在下一節中,您會將可組合函式的狀態提升至無狀態。
10. 狀態提升
在本節中,您將瞭解如何決定狀態的定義位置,以便重複使用及分享可組合函式。
在可組合函式中,您可以定義一些變數,以保留要在 UI 中顯示的狀態。例如,您可以在 EditNumberField()
可組合函式中將 amountInput
變數定義為狀態。
如果應用程式變得更複雜,而其他可組合函式需要存取 EditNumberField()
可組合函式中的狀態,則您必須考慮從 EditNumberField()
可組合函式中提升或擷取狀態。
瞭解有狀態與無狀態的可組合函式
您必須提升狀態,才能滿足下列需要:
- 使用多個可組合函式分享狀態。
- 建立可以在應用程式中重複使用的無狀態可組合函式。
從可組合函式中擷取狀態時,產生的可組合函式稱為無狀態可組合函式。也就是說,您可以從可組合函式中擷取狀態,使可組合函式成為無狀態。
「無狀態」可組合函式不具有狀態,也就是說,這種可組合函式不會保留、定義或修改新狀態。另一方面,「有狀態」可組合函式則具有可隨時間變更的狀態。
狀態提升是將狀態移往呼叫端的模式,可讓元件變成無狀態。
套用至可組合函式時,通常是指將兩個參數加入可組合函式:
value: T
參數,這是要顯示的現值。onValueChange: (T) -> Unit
- 回呼 lambda,這會在值變更時觸發,方便在其他位置更新狀態,例如使用者在文字方塊中輸入文字時。
使用 EditNumberField()
函式中的狀態:
- 更新
EditNumberField()
函式定義,以便透過新增value
和onValueChange
參數來提升狀態:
@Composable
fun EditNumberField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
//...
value
參數的類型是 String
,而 onValueChange
參數的類型則是 (String) -> Unit
,因此這個函式可以接受 String
值做為輸入,且沒有傳回值。onValueChange
參數是用來當做 onValueChange
回呼,傳遞至 TextField
可組合函式。
- 在
EditNumberField()
函式中更新TextField()
可組合函式,以便使用傳入的參數:
TextField(
value = value,
onValueChange = onValueChange,
// Rest of the code
)
- 將所記住的狀態從
EditNumberField()
函式移至TipTimeLayout()
函式,以便提升狀態:
@Composable
fun TipTimeLayout() {
var amountInput by remember { mutableStateOf("") }
val amount = amountInput.toDoubleOrNull() ?: 0.0
val tip = calculateTip(amount)
Column(
//...
) {
//...
}
}
- 您已將狀態提升至
TipTimeLayout()
,現在請將其傳遞至EditNumberField()
。在TipTimeLayout()
函式中,將EditNumberField
()
函式呼叫更新為使用提升後的狀態:
EditNumberField(
value = amountInput,
onValueChange = { amountInput = it },
modifier = Modifier
.padding(bottom = 32.dp)
.fillMaxWidth()
)
這會將 EditNumberField
變成無狀態。您已將 UI 狀態提升至祖系 TipTimeLayout()
。TipTimeLayout()
現在是狀態 (amountInput
) 擁有者。
位置格式設定
位置格式設定可用來在字串中顯示動態內容。舉例來說,假設您想讓「Tip amount」文字方塊顯示 xx.xx
值,且該值可以是由函式計算並設定格式的任何金額。如要在 strings.xml
檔案中完成這項作業,您需要使用預留位置引數來定義字串資源,如以下程式碼片段所示:
// No need to copy.
// In the res/values/strings.xml file
<string name="tip_amount">Tip Amount: %s</string>
在 Compose 程式碼中,您可以使用多個任何類型的預留位置引數。string
預留位置是 %s
。
請注意 TipTimeLayout()
中的文字可組合函式,您會以格式化的小費做為引數,傳遞至 stringResource()
函式。
// No need to copy
Text(
text = stringResource(R.string.tip_amount, "$0.00"),
style = MaterialTheme.typography.displaySmall
)
- 在函式
TipTimeLayout()
中,請使用tip
屬性顯示小費金額。請更新Text
可組合函式的text
參數,將tip
變數用做參數。
Text(
text = stringResource(R.string.tip_amount, tip),
// ...
已完成的 TipTimeLayout()
和 EditNumberField()
函式應如下列程式碼片段所示:
@Composable
fun TipTimeLayout() {
var amountInput by remember { mutableStateOf("") }
val amount = amountInput.toDoubleOrNull() ?: 0.0
val tip = calculateTip(amount)
Column(
modifier = Modifier
.statusBarsPadding()
.padding(horizontal = 40.dp)
.verticalScroll(rememberScrollState())
.safeDrawingPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = stringResource(R.string.calculate_tip),
modifier = Modifier
.padding(bottom = 16.dp, top = 40.dp)
.align(alignment = Alignment.Start)
)
EditNumberField(
value = amountInput,
onValueChange = { amountInput = 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))
}
}
@Composable
fun EditNumberField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
TextField(
value = value,
onValueChange = onValueChange,
singleLine = true,
label = { Text(stringResource(R.string.bill_amount)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = modifier
)
}
總結來說,您已將 amountInput
狀態從 EditNumberField()
提升至 TipTimeLayout()
可組合函式。為了讓文字方塊照常運作,您必須將兩個引數傳入 EditNumberField()
可組合函式:amountInput
值,以及根據使用者輸入內容更新 amountInput
值的 lambda 回呼。這些變更可讓您透過 TipTimeLayout()
中的 amountInput
屬性計算小費,並向使用者顯示小費計算結果。
- 在模擬器或裝置上執行應用程式,然後在「Bill Amount」文字方塊中輸入值。應用程式隨即會顯示帳單金額 15% 的小費金額,如下圖所示:
11. 取得解決方案程式碼
完成程式碼研究室後,如要下載當中用到的程式碼,可以使用這些 Git 指令:
$ 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
另外,您也能以 ZIP 檔案格式下載存放區,再將檔案解壓縮,然後在 Android Studio 中開啟。
如要查看解決方案程式碼,請前往 GitHub 檢視。
12. 結語
恭喜!您已成功完成這個程式碼研究室,也已瞭解如何使用 Compose 應用程式中的狀態!
摘要
- 應用程式中的狀態指任何可能隨時間變化的值。
- 「組成」是指 Compose 在執行可組合函式時建構的 UI 描述。Compose 應用程式會呼叫可組合函式,將資料轉換為 UI。
- 初始組成是指 Compose 會在第一次執行可組合函式時建立 UI。
- 重新組成是指再次執行相同可組合函式的程序,可在資料變更時更新樹狀結構。
- 狀態提升是將狀態移往呼叫端的模式,可讓元件變成無狀態。