Compose 中的狀態簡介

1. 事前須知

在這個程式碼研究室中,我們會說明一些與狀態相關的知識,以及如何透過 Jetpack Compose 使用及管控狀態。

從核心角度而言,應用程式中的狀態指任何可能隨時間變化的值。這個定義相當廣泛,包含應用程式中的資料庫和變數等一切元素。稍後我們將進一步介紹資料庫,但目前您需要瞭解的是,資料庫是經過精心整理的結構化資訊的集合,例如電腦上的檔案。

所有 Android 應用程式都會向使用者顯示狀態。以下列舉幾個 Android 應用程式中的狀態範例:

  • 無法建立網路連線時顯示的訊息。
  • 表單,例如註冊表單。您可以填寫並提交狀態。
  • 觸控式控制項,例如按鈕。狀態可能是「未輕觸」、「輕觸中」(顯示動畫) 或「已輕觸」(onClick 動作)。

在這個程式碼研究室中,您將探索如何使用 Compose,並思考使用 Compose 時的狀態。為此,您可以使用下列內建的 Compose UI 元素來建構 Tip Time 小費計算機應用程式:

  • 用於輸入和編輯文字的 TextField 可組合項。
  • 用於顯示文字的 Text 可組合項。
  • 用於在 UI 元素之間顯示空白空間的 Spacer 可組合項。

在這個程式碼研究室結束時,您將建立一個互動式小費計算機,系統會在您輸入服務金額時自動計算小費金額。這張圖片顯示最終應用程式的外觀:

761df483de663721.png

必要條件

  • 瞭解 Compose 的基本知識,例如 @Composable 註解。
  • 熟悉 Compose 版面配置的基本知識,例如 RowColumn 版面配置可組合項。
  • 熟悉修飾符的基本知識,例如 Modifier.padding() 函式。
  • 熟悉 Text 可組合項。

課程內容

  • 如何考量 UI 中的狀態。
  • Compose 如何使用狀態顯示資料。
  • 如何在應用程式中加入文字方塊。
  • 如何提升狀態。

建構項目

  • 一款小費計算機應用程式 Tip Time,可根據服務金額計算小費金額。

軟硬體需求

  • 一台可連上網際網路並具備網路瀏覽器的電腦
  • Kotlin 知識
  • Android Studio

2. 觀看程式設計示範影片 (可略過)

如果您想觀看課程老師示範完成此程式碼研究室,請觀看以下影片。

建議您在全螢幕模式下觀看影片 (點選影片右下角的 該符號以醒目顯示的矩形方框標出 4 個角落,表示其處於全螢幕模式。 圖示),以便清楚看見 Android Studio 和程式碼。

您可以跳過這個步驟,也可以不觀看這段影片,立即開始執行程式碼研究室的操作步驟。

3. 開始操作

  1. 請查看 Google 線上小費計算機。請注意,這只是範例,並非您稍後要在本課程中建立的 Android 應用程式。

46bf4366edc1055f.png 18da3c120daa0759.png

  1. 在「帳單」和「小費」方塊中輸入不同的值。小費和總金額也會隨之改變。

c0980ba3e9ebba02.png

請注意,當您輸入值時,「Tip」(小費) 和「Total」(總金額) 會隨之更新。完成下列程式碼研究室之後,您將在 Android 中開發類似的小費計算機應用程式。

在本課程中,您將建立一個簡單的小費計算機 Android 應用程式。

開發人員的工作方式通常如下:先簡單開發一款可以正常使用的應用程式 (即使看起來不甚理想),接著再加入更多功能並提升外觀吸引力。

完成本程式碼研究室之後,您的小費計算機應用程式將如以下螢幕截圖所示。當使用者輸入服務費時,應用程式就會顯示建議的小費金額。目前,小費百分比的硬式編碼為 15%。在下一個程式碼研究室中,您將繼續建構應用程式並新增更多功能,例如設定自訂小費百分比。

aaf86be8d13431f5.png

761df483de663721.png

4. 建立專案

在 Android Studio 中,使用空白 Compose 活動範本和必要的字串資源建立專案:

  1. 在 Android Studio 中,使用「Empty Compose Activity」(空白 Compose 活動) 範本建立專案,然後輸入 Tip Time 做為專案名稱,接著為「Mininum SDK」(最低 SDL) 選取「API 21: Android 5.0 (Lollipop)」以上版本。載入專案檔案。
  2. 在「Project」(專案) 窗格中,按一下「res > values > string.xml」。您應該為應用程式名稱設定單一字串資源。
  3. <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 元素:

  1. MainActivity.kt 檔案中,刪除 Greeting() 函式:
// Delete this.
@Composable
fun Greeting(name: String) {
   //...
}
  1. onCreate()DefaultPreview() 函式中,刪除 Greeting() 函式呼叫:
// Delete this.
Greeting("Android")
  1. onCreate() 函式下方,新增 TipTimeScreen() 可組合函式來表示應用程式畫面:
@Composable
fun TipTimeScreen() {
}
  1. onCreate() 函式的 Surface() 區塊中,呼叫 TipTimeScreen() 函式:
override fun onCreate(savedInstanceState: Bundle?) {
   //...
   setContent {
       TipTimeTheme {
           Surface(
           //...
           ) {
               TipTimeScreen()
           }
       }
   }
}
  1. DefaultPreview() 函式的 TipTimeTheme 區塊中,呼叫 TipTimeScreen() 函式:
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
   TipTimeTheme {
       TipTimeScreen()
   }
}

顯示畫面標題

實作 TipTimeScreen() 函式來顯示畫面標題:

  1. TipTimeScreen() 函式中,新增 Column 元素。這些元素位於垂直欄中,因此您必須使用 Column 元素。
  2. Column 區塊中,將 modifier 命名參數設為用於接受 32.dp 引數的 Modifier.padding 函式:
Column(
   modifier = Modifier.padding(32.dp)
) {}
  1. 匯入以下函式以及這個屬性:
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp
  1. Column 函式中,將 verticalArrangement 命名引數設為用於接受 8.dp 引數的 Arrangement.spacedBy 函式:
Column(
   modifier = Modifier.padding(32.dp),
   verticalArrangement = Arrangement.spacedBy(8.dp)
) {}

這麼做會在子項元素之間新增 8dp 的固定空間

  1. 匯入以下函式:
import androidx.compose.foundation.layout.Arrangement
  1. 新增 Text 元素,在其中將 text 命名參數設為 stringResource(R.string.calculate_tip) 函式、將 fontSize 命名參數設為 24.sp 值,以及將 modifier 命名引數設為 Modifier.align(Alignment.CenterHorizontally) 函式:
Text(
   text = stringResource(R.string.calculate_tip),
   fontSize = 24.sp,
   modifier = Modifier.align(Alignment.CenterHorizontally)
)
  1. 匯入以下匯入項目:
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.sp
import androidx.compose.ui.Alignment
  1. 在「設計」窗格中,按一下「Create & Refresh」(建立並重新整理)。畫面上應該會顯示 Calculate Tip 這個畫面標題,也就是您新增的文字元素。

da56236494529e77.png

新增 TextField 可組合項

在本節中,您可以新增 UI 元素,讓使用者在應用程式中輸入服務費。外觀如下圖所示:

58671affa01fb9e1.png

TextField 可組合函式可讓使用者在應用程式內輸入文字。例如,請注意以下圖片中 Gmail 應用程式登入畫面的文字方塊:

30d9c9123b5d26fe.png

在應用程式中新增 TextField 可組合項:

  1. 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 空格。

  1. 匯入以下函式:
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
  1. MainActivity.kt 檔案中,新增 EditNumberField() 可組合函式。
  2. EditNumberField() 函式的內文中新增 TextField,用於接受將 value 命名參數設為空字串,並將 onValueChange 命名參數設為空白 lambda 運算式:
@Composable
fun EditNumberField() {
   TextField(
      value = "",
      onValueChange = {},
   )
}
  1. 請注意您傳遞的參數:
  • value 參數是文字方塊,其中會顯示您在這裡傳遞的字串值。
  • onValueChange 參數是使用者在文字方塊中輸入文字時觸發的 lambda 回呼。
  1. 匯入這個函式:
import androidx.compose.material.TextField
  1. Spacer() 可組合函式後方的行中,呼叫 EditNumberField() 函式:
@Composable
fun TipTimeScreen() {
   Column(
       modifier = Modifier.padding(32.dp),
       verticalArrangement = Arrangement.spacedBy(8.dp)
   ) {
       Text(
           ...
       )
       Spacer(Modifier.height(16.dp))
       EditNumberField()
   }
}

畫面上隨即顯示文字方塊。

  1. 在「設計」窗格中,按一下「Create & Refresh」(建立並重新整理) 圖示 be24da86724b252c.png。您應該會看到 Calculate Tip 這個畫面標題以及一個空白文字方塊,中間有一個 16dp 空格。

1ff60ec32d3b15c1.png

6. 使用 Compose 中的狀態

應用程式中的狀態指任何可能隨時間變化的值。在這個應用程式中,狀態是服務費。

新增變數,以便儲存狀態:

  1. EditNumberField() 函式的開頭,使用 val 關鍵字新增指派給靜態 "0" 值的 amountInput 變數:
val amountInput = "0"

這是用於計算服務費的應用程式狀態。

  1. value 命名參數設為 amountInput 值:
TextField(
   value = amountInput,
   onValueChange = {},
)
  1. 再次建構並執行應用程式。文字方塊會顯示設為狀態變數的值,如下圖所示:

ba0f07ef1162855b.png

  1. 輸入其他值。由於 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 中使用 StateMutableState 類型,以讓 Compose 允許應用程式觀察或追蹤狀態。State 類型不可變動,因此您只能讀取當中的值,而 MutableState 類型則可變動。您可以使用 mutableStateOf 函式建立可觀察的 MutableState。該函式會接收做為參數封裝在 State 物件中的初始值,使其 value 變為可觀察狀態。

mutableStateOf() 函式傳回的值:

  • 保留狀態,也就是服務費。
  • 可變動,也就是可以變更這個值。
  • 可觀察,也就是 Compose 會觀察這個值發生的任何變更,並觸發重新組成以更新 UI。

新增服務費狀態:

  1. EditNumberField() 函式中,將 amountInput 狀態變數之前的 val 關鍵字變更為 var 關鍵字:
var amountInput = "0"

這會使狀態變數可變動。

  1. 使用 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.
  1. TextField 可組合函式中,使用 amountInput.value 屬性:
TextField(
   value = amountInput.value,
   onValueChange = { },
)

Compose 會追蹤每個可讀取狀態 value 屬性的可組合函式,並在其 value 變更時觸發重新組成。

當文字方塊的輸入內容有所變更時,就會觸發 onValueChange 回呼。在 lambda 運算式中,it 變數包含新值。

  1. onValueChange 命名參數的 lambda 運算式中,將 amountInput.value 屬性設為 it 變數:
@Composable
fun EditNumberField() {
   var amountInput = mutableStateOf("0")
   TextField(
       value = amountInput.value,
       onValueChange = { amountInput.value = it },
   )
}

您在更新 TextField 的狀態 (也就是 amountInput 變數) 時,TextField 會通知您文字中出現透過 onValueChange 回呼函式產生的變更。

  1. 執行應用程式,並在文字方塊中輸入文字。如下圖所示,文字方塊仍然會顯示 0 值:

6cb691703cc7ecbf.gif

使用者在文字方塊中輸入文字時,系統會呼叫 onValueChange 回呼,並將 amountInput 變數更新為新值。amountInput 狀態由 Compose 進行追蹤,因此當其值變更時,Compose 將建立重新組成排程並再次執行 EditNumberField() 可組合函式。在這個可組合函式中,amountInput 變數會重設為初始 0 值。因此,文字方塊會顯示 0 值。

在您新增程式碼後,狀態變更會導致系統建立重新組成排程。

不過,您必須設法在重新組成後保留 amountInput 變數的值,以免 EditNumberField() 函式每次重新組成時,該變數值都重設為 0 值。您將在下一節中解決這個問題。

8. 使用「記住」功能儲存狀態

執行重新組成期間,您可以多次呼叫可組合方法。如果未儲存,這些可組合方法會在重新組成期間重設其狀態。

可組合函式可以在重新組成期間使用 remember 儲存物件。remember 函式計算的值會在初始組成期間儲存在「組成」中,並在重新組成時傳回所儲存的值。您通常可以在可組合函式中搭配使用 remembermutableStateOf 函式,使狀態及其更新內容正確反映在 UI 中。

EditNumberField() 函式中,使用 remember 函式:

  1. EditNumberField() 函式中,使用 remember 呼叫 mutableStateOf() 函式,以便透過 by remember Kotlin 資源委派初始化 amountInput 變數。
  2. mutableStateOf() 函式中,傳入空字串 (而非靜態 "0" 字串):
var amountInput by remember { mutableStateOf("") }

現在,空字串是 amountInput 變數的初始預設值。byKotlin 資源委派amountInput 屬性的預設 getter 和 setter 函式會分別委派至 remember 類別的 getter 和 setter 函式。

  1. 匯入以下函式:
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
  1. 手動匯入以下函式:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

新增委派的 getter 和 setter 匯入項目後,您就可以在不參照 MutableStatevalue 屬性的情況下讀取及設定 amountInput

更新後的 EditNumberField() 函式應如下所示:

@Composable
fun EditNumberField() {
   var amountInput by remember { mutableStateOf("") }
   TextField(
       value = amountInput,
       onValueChange = { amountInput = it },
   )
}
  1. 執行應用程式,並在文字方塊中輸入一些文字。您現在應該可以看到先前輸入的文字。

270943a84f18572d.png

9. 狀態與重新組成實際使用教學

在本節中,您將設定中斷點並對 EditNumberField() 可組合函式進行偵錯,以便瞭解初始組成和重新組成的運作方式。

設定中斷點,在模擬器或裝置上對應用程式進行偵錯:

  1. onValueChange 命名參數旁邊的 EditNumberField() 函式中,設定行中斷點。
  2. 在導覽選單中,按一下「Debug 'app'」(對應用程式進行偵錯)。應用程式會在模擬器或裝置上啟動。建立 TextField 元素後,應用程式的執行程序將首次暫停。

e225f2d67e9f2c40.png

  1. 在「Debug」(偵錯) 窗格中,按一下「Continue Program」(繼續執行程式) 圖示 2a29a3bad712bec.png。文字方塊建立完成。
  2. 在模擬器或裝置的文字方塊中輸入字母。到達您設定的中斷點時,應用程式就會再次暫停執行程序。

只要您輸入文字,Compose 就會觸發重新組成,而系統會透過新資料呼叫 EditNumberField() 函式中的 onValueChange 回呼,如下圖所示:

1d5e08d32052d02e.png

  1. 在「Debug」(偵錯) 窗格中,按一下「Continue Program」(繼續執行程式) 圖示 2a29a3bad712bec.png。在模擬器或裝置上輸入的文字會顯示在包含中斷點的那一行旁,如下圖所示:

1f5db6ab5ca5b477.png

這就是文字欄位的狀態。

  1. 按一下「Resume Program」(繼續執行程式) 圖示 2a29a3bad712bec.png。您輸入的值會顯示在模擬器或裝置上。

10. 修改外觀

在上一節中,您已經瞭解文字欄位的運作方式。在本節中,您將增強 UI 使用效果。

在文字方塊中新增標籤

每個文字方塊都應該設定一個標籤,以便使用者瞭解可以輸入哪些資訊。下方第一張範例圖片顯示,標籤文字位於文字欄位中間,並與輸入行對齊。第二張範例圖片顯示,當使用者按一下文字方塊輸入文字時,標籤會移至文字方塊的上半部分。如要進一步瞭解文字欄位圖解,請參閱圖解

a2afd6c7fc547b06.png

修改 EditNumberField() 函式,以便在文字欄位中新增標籤:

  1. EditNumberField() 函式的 TextField() 可組合函式中,新增一個設為空白 lambda 運算式的 label 命名參數:
TextField(
//...
   label = { }
)
  1. 在 lambda 運算式中,呼叫用於接受 stringResource(R.string.cost_of_service)Text() 函式:
label = { Text(stringResource(R.string.cost_of_service)) }
  1. TextField() 可組合函式中,新增 modifier 命名參數並將值設為 Modifier.fillMaxWidth()
TextField(
  // Other parameters
   modifier = Modifier.fillMaxWidth(),
)
  1. 匯入下列項目:
import androidx.compose.foundation.layout.fillMaxWidth
  1. TextField() 可組合函式中,新增 singleLine 命名參數並將值設為 true
TextField(
  // Other parameters
   singleLine = true,
)

這會將文字方塊從多行壓縮成水平捲動的單行。

  1. 新增 keyboardOptions 命名參數,並將值設為 KeyboardOptions()
TextField(
  // Other parameters
   keyboardOptions = KeyboardOptions()
)

Android 提供選項讓您設定螢幕上顯示的鍵盤,以便輸入數字、電子郵件地址、網址和密碼等內容。如要進一步瞭解其他鍵盤類型,請參閱 KeyboardType

  1. 將鍵盤類型設為數字鍵盤,即可輸入數字。為 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)

   )
}
  1. 匯入下列項目:
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.foundation.text.KeyboardOptions
  1. 執行應用程式。

相關變更如下圖所示:

48368bf5df67af37.png

11. 顯示小費金額

在本節中,您將實作應用程式的主要功能,也就是計算和顯示小費金額的功能。

完成這項工作後,您的應用程式將如下所示:

aaf86be8d13431f5.png

計算小費金額

定義並實作用於接受服務費和小費百分比的函式,然後傳回小費金額:

  1. EditNumberField() 函式後方的 MainActivity.kt 檔案中,新增 private calculateTip() 函式。
  2. 新增 amounttipPercent 命名參數,兩者皆為 Double 類型。amount 參數會傳遞服務費。
  3. tipPercent 參數設為 15.0 預設引數值。這會將預設的小費百分比設為 15%。在下一個程式碼研究室中,您將取得使用者提供的消費金額:
private fun calculateTip(
   amount: Double,
   tipPercent: Double = 15.0
) {
}
  1. 在函式內文中,使用 val 關鍵字定義 tip 變數,將 tipPercent 參數除以 100 值,然後將結果乘以 amount 參數來計算小費:
private fun calculateTip(
   amount: Double,
   tipPercent: Double = 15.0
) {
   val tip = tipPercent / 100 * amount
}

您的應用程式現在可以計算小費,但您仍需使用 NumberFormat 類別設定小費格式並顯示小費。

  1. calculateTip() 函式內文的下一行中,呼叫 NumberFormat.getCurrencyInstance() 函式:
NumberFormat.getCurrencyInstance()

系統會為您提供數字格式設定工具,以便您將數字格式設為貨幣。

  1. 呼叫 NumberFormat.getCurrencyInstance() 函式時,鏈結 format() 方法並將 tip 變數作為參數進行傳遞:
NumberFormat.getCurrencyInstance().format(tip)
  1. 當 Android Studio 發出提示時,匯入這個類別。
import java.text.NumberFormat
  1. 最後一個步驟是,從函式傳回格式化字串。修改函式簽名以傳回 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 值,其中包含使用者輸入的金額。

  1. EditNumberField() 可組合函式中,呼叫 amountInput 變數中的 toDoubleOrNull 函式,以便將 String 轉換為 Double
val amount = amountInput.toDoubleOrNull()

toDoubleOrNull() 函式是預先定義的 Kotlin 函式,可將字串分析為 Double 數字;如果字串並非代表有效數字,則傳回 null

  1. 請在陳述式結尾新增 ?: Elvis 運算子,以便在 amountInput 為空值時傳回 0.0 值:
val amount = amountInput.toDoubleOrNull() ?: 0.0
  1. amount 變數後,建立另一個名為 tipval 變數。使用 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 可組合項來顯示小費金額的計算結果:

97734d91a3844d22.png

  1. Column() 區塊結尾的 TipTimeScreen() 函式中,新增 Spacer() 可組合項並傳入粗細值 24.dp
@Composable
fun TipTimeScreen() {
   Column(
       //...
   ) {
       Text(
           //...
       )
       //...
       EditNumberField()
       Spacer(Modifier.height(24.dp))
   }
}

這樣就能在文字欄位後方新增空格。

  1. Spacer() 可組合項之後,新增下列 Text 可組合項:
Text(
   text = stringResource(R.string.tip_amount, ""),
   modifier = Modifier.align(Alignment.CenterHorizontally),
   fontSize = 20.sp,
   fontWeight = FontWeight.Bold
)

這個程式碼使用 tip_amount 字串資源來設定文字,但不會顯示小費金額;不過,這個問題很快就會得到修正。這會將畫面上的文字置中,大小為 20.sp,並將字型粗細設為粗體。

  1. 匯入以下匯入項目:
import androidx.compose.ui.text.font.FontWeight

您必須存取 TipTimeScreen 函式中的 amountInput 變數,才能計算並顯示小費金額。不過,amountInput 變數是 EditNumberField() 可組合函式中定義的文字欄位狀態,因此您目前無法從 TipTimeScreen() 函式呼叫這個變數。下圖說明程式碼的結構:

5ec86acdbfa1907b.png

這個結構不會讓您在新的 Text 可組合項中顯示小費金額,因為 Text 可組合項必須存取使用 amountInput 變數計算的 amount 變數。您必須向 TipTimeScreen() 函式公開 amount 變數。下圖說明讓 EditNumberField() 可組合項處於無狀態所需的程式碼結構:

e11d5bba4d8abd0d.png

這個模式稱為「狀態提升」。在下一節中,您將會將可組合項從的狀態提升至無狀態

12. 狀態提升

在本節中,您將瞭解如何決定狀態的定義位置,以便重複使用及分享可組合函式。

在可組合函式中,您可以將保留狀態的變數定義為在 UI 中顯示。例如,您可以將 amountInput 變數定義為 EditNumberField() 可組合項中的狀態。

如果應用程式變得更複雜,而其他可組合項必須存取 EditNumberField() 可組合項中的狀態,則您必須先考慮提升狀態,或從 EditNumberField() 可組合函式中擷取這個狀態。

瞭解有狀態與無狀態的可組合項

您必須提升狀態,才能滿足下列需要:

  • 使用多個可組合函式分享狀態。
  • 建立可以在應用程式中重複使用的無狀態可組合項。

從可組合函式中擷取狀態時,將會產生無狀態的可組合函式。也就是說,您可以透過從可組合函式中擷取狀態,使其成為無狀態可組合函式。

「無狀態」可組合函式不具有任何狀態,這表示它不會保留、定義或修改新狀態。另一方面,「有狀態」可組合函式則具有某種可隨時間變更的狀態。

「提升狀態」是指將狀態提升到其他函式的一種機制,目的是讓特定的元件處於無狀態。

套用至可組合項時,通常是指將兩個參數加入可組合項:

  • value: T 參數,這是要顯示的現值。
  • onValueChange: (T) -> Unit - 回呼 lambda;值發生變更時就會觸發,以便在其他位置時更新狀態 (例如使用者在文字方塊中輸入文字時)。

使用 EditNumberField() 函式中的狀態:

  1. 更新 EditNumberField() 函式定義,以便透過新增 valueonValueChange 參數來提升狀態:
fun EditNumberField(
   value: String,
   onValueChange: (String) -> Unit
)

value 參數的類型是 String,而 onValueChange 參數的類型則是 (String) -> Unit,因此這個函式會以 String 值做為輸入值且不含傳回值。onValueChange 參數用於當做 onValueChange 回呼傳遞至 TextField 可組合項。

  1. EditNumberField() 函式中,更新 TextField() 可組合函式以使用傳入的參數:
TextField(
   value = value,
   onValueChange = onValueChange,
   // Rest of the code
)
  1. 將所記住的狀態從 EditNumberField() 函式移至 TipTimeScreen() 函式,以便提升狀態:
@Composable
fun TipTimeScreen() {
   var amountInput by remember { mutableStateOf("") }

   val amount = amountInput.toDoubleOrNull() ?: 0.0
   val tip = calculateTip(amount)

   Column(
       //...
   ) {
       //...
   }
}
  1. 您已將狀態提升至 TipTimeScreen(),現在請將其傳遞至 EditNumberField()。在 TipTimeScreen() 函式中,將 EditNumberField() 函式呼叫更新為使用提升後的狀態:
EditNumberField(value = amountInput,
   onValueChange = { amountInput = it }
)
  1. 使用 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 屬性計算小費,並向使用者顯示小費金額。

  1. 在模擬器或裝置上執行應用程式,然後在「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 第一次執行可組合函式時,由 Compose 建立的 UI。
  • 重新組成指再次執行相同的可組合項元素,以便在資料變更時更新樹狀結構。
  • 狀態提升是一種提升狀態的模式,以使元件處於無狀態。

瞭解詳情