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()函式呼叫,以便透過byrememberKotlin 資源委派初始化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。
 - 重新組成是指再次執行相同可組合函式的程序,可在資料變更時更新樹狀結構。
 - 狀態提升是將狀態移往呼叫端的模式,可讓元件變成無狀態。
 
  
 未加入自訂主題。
 加入自訂主題。