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. 觀看程式設計示範影片 (可略過)
如果您想觀看課程老師示範完成此程式碼研究室,請觀看以下影片。
建議您在全螢幕模式下觀看影片 (點選影片右下角的 圖示),以便清楚看見 Android Studio 和程式碼。
您可以跳過這個步驟,也可以不觀看這段影片,立即開始進行程式碼研究室的操作步驟。
3. 開始操作
- 請查看 Google 線上小費計算機。請注意,這只是範例,並非您稍後要在本課程中建立的 Android 應用程式。
- 在「帳單」和「小費」方塊中輸入不同的值。小費和總金額也會隨之改變。
請注意,當您輸入值時,「Tip」和「Total」會隨之更新。完成下列程式碼研究室之後,您將在 Android 中開發類似的小費計算機應用程式。
在本課程中,您將建立一個簡單的小費計算機 Android 應用程式。
開發人員的工作方式通常如下:先簡單開發一款可以正常使用的應用程式 (即使看起來不甚理想),接著再加入更多功能並提升外觀吸引力。
完成本程式碼研究室之後,您的小費計算機應用程式將如以下螢幕截圖所示。當使用者輸入「Cost of Service」後,應用程式就會顯示建議的小費金額。目前,小費百分比的硬式編碼為 15%。在下一個程式碼研究室中,您將繼續建構應用程式並新增更多功能,例如設定自訂小費百分比。
4. 建立專案
在 Android Studio 中,使用空白 Compose 活動範本和必要的字串資源建立專案:
- 在 Android Studio 中,使用「Empty Compose Activity」(空白 Compose 活動) 範本建立專案,然後輸入
Tip Time
做為專案名稱,接著選取「API 21: Android 5.0 (Lollipop)」以上版本做為最低 SDK。載入專案檔案。 - 在「Project」(專案) 窗格中,按一下「res > values > string.xml」。您應該為應用程式名稱設定單一字串資源。
- 在
<resources>
標記之間,輸入下列字串資源:
<string name="calculate_tip">Calculate Tip</string>
<string name="cost_of_service">Cost of Service</string>
<string name="tip_amount">Tip amount: %s</string>
strings.xml
檔案應該如以下程式碼片段所示:
strings.xml
<resources>
<string name="app_name">Tip Time</string>
<string name="calculate_tip">Calculate Tip</string>
<string name="cost_of_service">Cost of Service</string>
<string name="tip_amount">Tip amount: %s</string>
</resources>
5. 新增畫面標題
在本節中,您會使用 Text
可組合函式,在應用程式中新增畫面標題。
刪除 Greeting()
函式並新增 TipTimeScreen()
函式,即可新增應用程式所需的 UI 元素:
- 在
MainActivity.kt
檔案中,刪除Greeting()
函式:
// Delete this.
@Composable
fun Greeting(name: String) {
//...
}
- 在
onCreate()
和DefaultPreview()
函式中,刪除Greeting()
函式呼叫:
// Delete this.
Greeting("Android")
- 在
onCreate()
函式下方,新增TipTimeScreen()
可組合函式來表示應用程式畫面:
@Composable
fun TipTimeScreen() {
}
- 在
onCreate()
函式的Surface()
區塊中,呼叫TipTimeScreen()
函式:
override fun onCreate(savedInstanceState: Bundle?) {
//...
setContent {
TipTimeTheme {
Surface(
//...
) {
TipTimeScreen()
}
}
}
}
- 在
DefaultPreview()
函式的TipTimeTheme
區塊中,呼叫TipTimeScreen()
函式:
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
TipTimeTheme {
TipTimeScreen()
}
}
顯示畫面標題
實作 TipTimeScreen()
函式來顯示畫面標題:
- 在
TipTimeScreen()
函式中,新增Column
元素。這些元素位於垂直欄中,因此您必須使用Column
元素。 - 在
Column
區塊中,將modifier
命名參數設為用於接受32.dp
引數的Modifier.padding
函式:
Column(
modifier = Modifier.padding(32.dp)
) {}
- 匯入以下函式以及這個屬性:
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp
- 在
Column
函式中,傳入verticalArrangement
命名引數並將其設為可接受8.dp
引數的Arrangement.spacedBy
函式:
Column(
modifier = Modifier.padding(32.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {}
這麼做會在子項元素之間新增 8dp
的固定空間。
- 匯入以下函式:
import androidx.compose.foundation.layout.Arrangement
- 新增
Text
元素,讓該元素接受設為stringResource(R.string.
calculate_tip
)
函式的text
命名參數、設為24.sp
值的fontSize
命名參數,以及設為Modifier.align(Alignment.CenterHorizontally)
函式的modifier
命名引數:
Text(
text = stringResource(R.string.calculate_tip),
fontSize = 24.sp,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
- 匯入以下匯入項目:
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.sp
import androidx.compose.ui.Alignment
- 在「設計」窗格中,按一下「Create & Refresh」(建立並重新整理)。畫面上應該會顯示
Calculate Tip
這個畫面標題,也就是您新增的文字元素。
新增 TextField
可組合項
在本節中,您可以新增 UI 元素,讓使用者在應用程式中輸入服務費。外觀如下圖所示:
TextField
可組合函式可讓使用者在應用程式內輸入文字。例如,請注意以下圖片中 Gmail 應用程式登入畫面的文字方塊:
在應用程式中新增 TextField
可組合項:
- 在
Text
元素後方的Column
區塊中,新增Spacer()
可組合函式 (高度為16dp
)。
@Composable
fun TipTimeScreen() {
Column(
modifier = Modifier.padding(32.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
...
)
Spacer(Modifier.height(16.dp))
}
}
畫面標題後方會顯示一個空白的 16dp
空格。
- 匯入以下函式:
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
- 在
MainActivity.kt
檔案中,新增EditNumberField()
可組合函式。 - 在
EditNumberField()
函式的內文中新增TextField
,用於接受設為空字串的value
命名參數,以及設為空白 lambda 運算式的onValueChange
命名參數:
@Composable
fun EditNumberField() {
TextField(
value = "",
onValueChange = {},
)
}
- 請注意您傳遞的參數:
value
參數是文字方塊,其中會顯示您在這裡傳遞的字串值。onValueChange
參數是使用者在文字方塊中輸入文字時觸發的 lambda 回呼。
- 匯入這個函式:
import androidx.compose.material.TextField
- 在
Spacer()
可組合函式後方的行中,呼叫EditNumberField()
函式:
@Composable
fun TipTimeScreen() {
Column(
modifier = Modifier.padding(32.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
...
)
Spacer(Modifier.height(16.dp))
EditNumberField()
}
}
畫面上隨即顯示文字方塊。
- 在「設計」窗格中,按一下「Create & Refresh」(建立並重新整理) 圖示
。您應該會看到
Calculate Tip
這個畫面標題以及一個包含16dp
空間的空白文字方塊。
6. 使用 Compose 中的狀態
應用程式中的狀態指任何可能隨時間變化的值。在這個應用程式中,狀態是服務費。
新增變數,以便儲存狀態:
- 在
EditNumberField()
函式的開頭,使用val
關鍵字新增指派給靜態值"0"
的amountInput
變數:
val amountInput = "0"
這是用於計算服務費的應用程式狀態。
- 將
value
命名參數設為amountInput
值:
TextField(
value = amountInput,
onValueChange = {},
)
- 再次建構並執行應用程式。文字方塊會顯示設為狀態變數的值,如下圖所示:
- 輸入其他值。由於
TextField
可組合項不會自行更新,硬式編碼狀態仍維持不變。當value
參數變更 (設為amountInput
屬性) 時,這個可組合項會隨之更新。
amountInput
變數代表文字方塊的狀態。經過硬式編碼的狀態無法修改,也不會反映使用者輸入內容,因此沒有實用性。當使用者更新服務費時,您必須更新應用程式的狀態。
7. 組成
應用程式中的可組合項描述的 UI 會顯示一欄,當中包含部分文字、一個空格字元和一個文字方塊。文字會顯示 Calculate tip
標題,Spacer
的粗細為 16dp
,文字方塊的值則會顯示 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"
這會使狀態變數可變動。
- 使用
MutableState<String>
類型 (而非經過硬式編碼的String
變數),讓 Compose 知道要追蹤amountInput
狀態並傳入"0"
字串,也就是amountInput
狀態變數的初始預設值:
var amountInput: MutableState<String> = mutableStateOf("0")
amountInput
初始化程序也可以類型推論的方式編寫,如下所示:
var amountInput = mutableStateOf("0")
mutableStateOf()
函式會接受做為參數封裝在 State
物件中的初始值 "0"
,使其 value
變為可觀察狀態。這會導致 Android Studio 發出以下編譯警告,但您很快可以加以修正:
Creating a state object during composition without using remember.
- 在
TextField
可組合函式中,使用amountInput.value
屬性:
TextField(
value = amountInput.value,
onValueChange = { },
)
Compose 會追蹤每個可讀取狀態 value
屬性的可組合項,並在其 value
變更時觸發重新組成。
當文字方塊的輸入內容有所變更時,就會觸發 onValueChange
回呼。在 lambda 運算式中,it
變數包含新值。
- 在
onValueChange
命名參數的 lambda 運算式中,將amountInput.value
屬性設為it
變數:
@Composable
fun EditNumberField() {
var amountInput = mutableStateOf("0")
TextField(
value = amountInput.value,
onValueChange = { amountInput.value = it },
)
}
當 TextField
透過 onValueChange
回呼函式通知您文本發生變更時,您將更新TextField
狀態 (也就是 amountInput
變數)。
- 執行應用程式,並在文字方塊中輸入文字。如下圖所示,文字方塊仍然會顯示
0
值:
使用者在文字方塊中輸入文字時,系統會呼叫 onValueChange
回呼,並將 amountInput
變數更新為新值。amountInput
狀態由 Compose 進行追蹤,因此當其值變更時,Compose 將建立重新組成排程並再次執行 EditNumberField()
可組合函式。在這個可組合函式中,amountInput
變數會重設為初始值 0
。因此,文字方塊會顯示 0
值。
在您新增程式碼後,狀態變更會導致系統建立重新組成排程。
不過,您必須設法在重新組成後保留 amountInput
變數的值,以免 EditNumberField()
函式每次重新組成時,該變數值都會重設為 0
值。您將在下一節中解決這個問題。
8. 使用「記住」功能儲存狀態
執行重新組成期間,您可以多次呼叫可組合方法。如果未儲存,這些可組合項會在重新組成期間重設狀態。
可組合函式可以在重新組成期間使用 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.mutableStateOf
import androidx.compose.runtime.remember
- 手動匯入以下函式:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
新增委派的 getter 和 setter 匯入項目後,您就可以在不參照 MutableState
之 value
屬性的情況下讀取及設定 amountInput
。
更新後的 EditNumberField()
函式應如下所示:
@Composable
fun EditNumberField() {
var amountInput by remember { mutableStateOf("") }
TextField(
value = amountInput,
onValueChange = { amountInput = it },
)
}
- 執行應用程式,並在文字方塊中輸入一些文字。您現在應該可以看到先前輸入的文字。
9. 狀態與重新組成實際使用教學
在本節中,您將設定中斷點並對 EditNumberField()
可組合函式進行偵錯,以便瞭解初始組成和重新組成的運作方式。
設定中斷點,在模擬器或裝置上對應用程式進行偵錯:
- 在
onValueChange
命名參數旁邊的EditNumberField()
函式中,設定行中斷點。 - 在導覽選單中,按一下「Debug 'app'」(對應用程式進行偵錯)。應用程式會在模擬器或裝置上啟動。建立
TextField
元素後,應用程式的執行程序將首次暫停。
- 在「Debug」(偵錯) 窗格中,按一下「Continue Program」(繼續執行程式) 圖示
。文字方塊建立完成。
- 在模擬器或裝置的文字方塊中輸入字母。到達您設定的中斷點時,應用程式就會再次暫停執行程序。
只要您輸入文字,Compose 就會觸發重新組成,而系統會使用新資料呼叫 EditNumberField()
函式中的 onValueChange
回呼,如下圖所示:
- 在「Debug」(偵錯) 窗格中,按一下「Continue Program」(繼續執行程式) 圖示
。在模擬器或裝置上輸入的文字會顯示在包含中斷點的程式碼行旁,如下圖所示:
這就是文字欄位的狀態。
- 按一下「Resume Program」圖示
。您輸入的值會顯示在模擬器或裝置上。
10. 修改外觀
在上一節中,您已經瞭解文字欄位的運作方式。在本節中,您將提升 UI。
在文字方塊中新增標籤
每個文字方塊都應該設定一個標籤,以便使用者瞭解可以輸入哪些資訊。下方第一張範例圖片顯示,標籤文字位於文字欄位中間,並與輸入行對齊。第二張範例圖片顯示,當使用者按一下文字方塊輸入文字時,標籤會移至文字方塊的上方。如要進一步瞭解文字欄位圖解,請參閱圖解。
修改 EditNumberField()
函式,以便在文字欄位中新增標籤:
- 在
EditNumberField()
函式的TextField()
可組合函式中,新增一個設為空白 lambda 運算式的label
命名參數:
TextField(
//...
label = { }
)
- 在 lambda 運算式中,呼叫用於接受
stringResource
(R.string.
cost_of_service
)
的Text()
函式:
label = { Text(stringResource(R.string.cost_of_service)) }
- 在
TextField()
可組合函式中,新增modifier
命名參數並將值設為Modifier.
fillMaxWidth
()
:
TextField(
// Other parameters
modifier = Modifier.fillMaxWidth(),
)
- 匯入下列項目:
import androidx.compose.foundation.layout.fillMaxWidth
- 在
TextField()
可組合函式中,新增singleLine
命名參數並將值設為true
:
TextField(
// Other parameters
singleLine = true,
)
這會將文字方塊從多行壓縮成水平捲動的單行。
- 新增
keyboardOptions
命名參數,並設為KeyboardOptions()
:
TextField(
// Other parameters
keyboardOptions = KeyboardOptions()
)
Android 提供選項讓您設定螢幕上顯示的鍵盤,以便輸入數字、電子郵件地址、網址和密碼等內容。如要進一步瞭解其他鍵盤類型,請參閱 KeyboardType。
- 將鍵盤類型設為數字鍵盤,即可輸入數字。為
KeyboardOptions
函式傳遞KeyboardType.Number
命名參數並將值設為keyboardType
:
TextField(
// Other parameters
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
完成的 EditNumberField()
函式應如下列程式碼片段所示:
@Composable
fun EditNumberField() {
var amountInput by remember { mutableStateOf("") }
TextField(
value = amountInput,
onValueChange = { amountInput = it },
label = { Text(stringResource(R.string.cost_of_service)) },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
- 匯入下列項目:
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.foundation.text.KeyboardOptions
- 執行應用程式。
相關變更如下圖所示:
11. 顯示小費金額
在本節中,您將實作應用程式的主要功能,也就是計算和顯示小費金額的功能。
完成這項工作後,您的應用程式將如下所示:
計算小費金額
定義並實作可接受服務費和小費百分比的函式,然後傳回小費金額:
- 在
EditNumberField()
函式後方的MainActivity.kt
檔案中,新增private
calculateTip()
函式。 - 新增
amount
和tipPercent
命名參數,兩者皆為Double
類型。amount
參數會傳遞服務費。 - 將
tipPercent
參數設為15.0
預設引數值。這會將預設的小費百分比設為 15%。在下一個程式碼研究室中,您將取得使用者提供的消費金額:
private fun calculateTip(
amount: Double,
tipPercent: Double = 15.0
) {
}
- 在函式內文中,使用
val
關鍵字定義tip
變數,將tipPercent
參數除以100
值,然後將結果乘以amount
參數即可計算小費:
private fun calculateTip(
amount: Double,
tipPercent: Double = 15.0
) {
val tip = tipPercent / 100 * amount
}
您的應用程式現在可以計算小費,但您仍需使用 NumberFormat
類別設定小費格式並顯示小費。
- 在
calculateTip()
函式內文的下一行中,呼叫NumberFormat.getCurrencyInstance()
函式:
NumberFormat.getCurrencyInstance()
系統會為您提供數字格式設定工具,以便您將數字格式設為貨幣。
- 呼叫
NumberFormat.getCurrencyInstance()
函式時,鏈結format()
方法並將tip
變數做為參數傳入:
NumberFormat.getCurrencyInstance().format(tip)
- 當 Android Studio 發出提示時,匯入這個類別。
import java.text.NumberFormat
- 最後一個步驟是,從函式傳回格式化字串。修改函式簽名以傳回
String
類型。在NumberFormat
陳述式前面新增return
關鍵字:
private fun calculateTip(
amount: Double,
tipPercent: Double = 15.0
): String {
val tip = tipPercent / 100 * amount
return NumberFormat.getCurrencyInstance().format(tip)
}
現在,函式會傳回格式化字串。
使用 calculateTip()
函式
使用者在文字欄位可組合項中輸入的文字將做為 String
傳回到 onValueChange
回呼函式,即使使用者輸入的是數字也是如此。如要修正這個問題,您必須轉換 amountInput
值,其中包含使用者輸入的金額。
- 在
EditNumberField()
可組合函式中,呼叫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() {
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.cost_of_service)) },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
顯示小費金額計算結果
您已編寫函式來計算小費金額,下一步是新增 Text
可組合項來顯示小費金額的計算結果:
- 在
Column()
區塊結尾的TipTimeScreen()
函式中,新增Spacer()
可組合項並傳入粗細值24.dp
:
@Composable
fun TipTimeScreen() {
Column(
//...
) {
Text(
//...
)
//...
EditNumberField()
Spacer(Modifier.height(24.dp))
}
}
這樣就能在文字欄位後方新增空格。
- 在
Spacer()
可組合項之後,新增下列Text
可組合項:
Text(
text = stringResource(R.string.tip_amount, ""),
modifier = Modifier.align(Alignment.CenterHorizontally),
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
這個程式碼使用 tip_amount
字串資源來設定文字,但不會顯示小費金額;不過,這個問題很快就會得到修正。這會將畫面上的文字置中,大小為 20.sp
,並將字型粗細設為粗體。
- 匯入以下匯入項目:
import androidx.compose.ui.text.font.FontWeight
您需要存取 TipTimeScreen
函式中的 amountInput
變數,才能計算並顯示小費金額。不過,amountInput
變數是 EditNumberField()
可組合函式中定義的文字欄位狀態,因此您目前無法從 TipTimeScreen()
函式呼叫這個變數。下圖說明程式碼的結構:
這個結構不會讓您在新的 Text
可組合項中顯示小費金額,因為 Text
可組合項需要存取使用 amountInput
變數計算的 amount
變數。您必須向 TipTimeScreen()
函式公開 amount
變數。下圖說明讓 EditNumberField()
可組合項處於無狀態所需的程式碼結構:
這個模式稱為「狀態提升」。在下一節中,您將會將可組合項從的狀態提升至無狀態。
12. 狀態提升
在本節中,您將瞭解如何決定狀態的定義位置,以便重複使用及分享可組合項。
在可組合函式中,您可以定義一些變數,以保留要在 UI 中顯示的狀態。例如,您可以在 EditNumberField()
可組合項中將 amountInput
變數定義為狀態。
如果應用程式變得更複雜,而其他可組合項需要存取 EditNumberField()
可組合項中的狀態,則您必須考慮從 EditNumberField()
可組合函式中提升或擷取狀態。
瞭解有狀態與無狀態的可組合項
您必須提升狀態,才能滿足下列需要:
- 使用多個可組合函式分享狀態。
- 建立可以在應用程式中重複使用的無狀態可組合項。
從可組合函式中擷取狀態時,產生的可組合函式稱為無狀態可組合函式。也就是說,您可以從可組合函式中擷取狀態,使其成為無狀態可組合函式。
「無狀態」可組合函式不具有任何狀態,這表示它不會保留、定義或修改新狀態。另一方面,「有狀態」可組合函式則具有某種可隨時間變更的狀態。
「狀態提升」是指將狀態提升到其他函式的一種機制,目的是讓特定的元件處於無狀態。
套用至可組合項時,通常是指將兩個參數加入可組合項:
value: T
參數,這是要顯示的現值。onValueChange: (T) -> Unit
- 回呼 lambda,在值發生變更時就會觸發,以便在其他位置時更新狀態 (例如使用者在文字方塊中輸入文字時)。
使用 EditNumberField()
函式中的狀態:
- 更新
EditNumberField()
函式定義,以便透過新增value
和onValueChange
參數來提升狀態:
fun EditNumberField(
value: String,
onValueChange: (String) -> Unit
)
value
參數的類型是 String
,而 onValueChange
參數的類型則是 (String) -> Unit
,因此這個函式可以接受 String
值做為輸入,且沒有傳回值。onValueChange
參數用於當做 onValueChange
回呼傳遞至 TextField
可組合項。
- 在
EditNumberField()
函式中,更新TextField()
可組合函式以使用傳入的參數:
TextField(
value = value,
onValueChange = onValueChange,
// Rest of the code
)
- 將所記住的狀態從
EditNumberField()
函式移至TipTimeScreen()
函式,以便提升狀態:
@Composable
fun TipTimeScreen() {
var amountInput by remember { mutableStateOf("") }
val amount = amountInput.toDoubleOrNull() ?: 0.0
val tip = calculateTip(amount)
Column(
//...
) {
//...
}
}
- 您已將狀態提升至
TipTimeScreen()
,現在請將其傳遞至EditNumberField()
。在TipTimeScreen()
函式中,將EditNumberField
()
函式呼叫更新為使用提升後的狀態:
EditNumberField(value = amountInput,
onValueChange = { amountInput = it }
)
- 使用
tip
屬性顯示小費金額。更新Text
可組合項的text
參數,以便將tip
變數用做參數。這就是位置格式設定。
Text(
text = stringResource(R.string.tip_amount, tip),
// Rest of the code
)
透過位置格式設定,您可以在字串中顯示動態內容。舉例來說,假設您想讓「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>
// In your Compose code
Text(
text = stringResource(R.string.tip_amount, tip)
)
您可以使用多個不限類型的預留位置引數。string
預留位置是 %s
。在 Compose 中,您必須將設定格式的小費做為引數傳遞至 stringResource()
函式。
已完成的 TipTimeScreen()
和 EditNumberField()
函式應如下列程式碼片段所示:
@Composable
fun TipTimeScreen() {
var amountInput by remember { mutableStateOf("") }
val amount = amountInput.toDoubleOrNull() ?: 0.0
val tip = calculateTip(amount)
Column(
modifier = Modifier.padding(32.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = stringResource(R.string.calculate_tip),
fontSize = 24.sp,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(Modifier.height(16.dp))
EditNumberField(value = amountInput,
onValueChange = { amountInput = it }
)
Spacer(Modifier.height(24.dp))
Text(
text = stringResource(R.string.tip_amount, tip),
modifier = Modifier.align(Alignment.CenterHorizontally),
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
}
}
@Composable
fun EditNumberField(
value: String,
onValueChange: (String) -> Unit
) {
TextField(
value = value,
onValueChange = onValueChange,
label = { Text(stringResource(R.string.cost_of_service)) },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
總結來說,您已將 amountInput
狀態從 EditNumberField()
提升至 TipTimeScreen()
可組合項。為了讓文字方塊照常運作,您必須將兩個引數傳遞給 EditNumberField()
可組合函式:amountInput
值,以及根據使用者輸入內容更新 amountInput
值的 lambda 回呼。這些變更可讓您透過 TipTimeScreen()
中的 amountInput
屬性計算小費,並向使用者顯示小費金額。
- 在模擬器或裝置上執行應用程式,然後在「Cost of Service」文字方塊中輸入值。系統隨即會顯示 15% 的小費金額,如下圖所示:
13. 取得解決方案程式碼
完成程式碼研究室後,如要下載當中用到的程式碼,您可以使用以下 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 檢視。
14. 結語
恭喜!您已成功完成這個程式碼研究室,也已瞭解如何使用 Compose 應用程式中的狀態!
摘要
- 應用程式中的狀態指任何可能隨時間變化的值。
- 「組成」是指 Compose 在執行可組合項時建構的 UI 描述。Compose 應用程式會呼叫可組合函式,將資料轉換為 UI。
- 初始組成是指 Compose 會在第一次執行可組合函式時建立 UI。
- 重新組成指再次執行相同的可組合項元素,以便在資料變更時更新樹狀結構。
- 狀態提升是一種提升狀態的模式,以使元件處於無狀態。