應用程式中的狀態指的是任何可能隨時間變化的值。這個定義非常廣泛,從 Room 資料庫到某類別中的變數都包含在內。
所有 Android 應用程式都會向使用者顯示狀態。以下列舉幾個 Android 應用程式中的狀態範例:
- 無法建立網路連線時顯示的 Snackbar。
- 網誌文章和相關留言。
- 使用者點選按鈕時會播放的漣漪效果動畫。
- 使用者可繪製在圖片上的貼圖。
Jetpack Compose 供您更清楚瞭解在 Android 應用程式中儲存及使用狀態的位置和方式。本指南的重點是介紹狀態和可組合項之間的關係,以及 Jetpack Compose 提供了哪些 API 來協助您運用狀態。
狀態與組成
Compose 採用宣告式框架,因此只能以新引數呼叫相同的可組合項來進行更新。這些引數是 UI 狀態的表示法。每當狀態更新,系統就會進行「重組」。因此,TextField
之類的項目不會像在以命令式 XML 為基礎的檢視畫面中一樣自動更新。可組合項必須明確得知新狀態,才能據此更新。
@Composable private fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { Text( text = "Hello!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) OutlinedTextField( value = "", onValueChange = { }, label = { Text("Name") } ) } }
如果您執行這段程式碼並嘗試輸入文字,您會發現沒有任何作用。這是因為 TextField
不會自行更新,只有在其中的 value
參數變更時才會更新。這是 Compose 中組成與重組的運作方式所致。
如要進一步瞭解初始組成和重組,請參閱「Compose 中的思維」。
可組合項中的狀態
可組合函式可以使用 remember
API,在記憶體中儲存物件。remember
計算的值會在初始組成期間儲存在「組成」中,並在重新組成時傳回所儲存的值。remember
可用來儲存可變動與不可變動的物件。
mutableStateOf
會建立可觀察的 MutableState<T>
,這是已經與 Compose 執行階段整合的可觀察類型。
interface MutableState<T> : State<T> {
override var value: T
}
如果變更 value
,系統就會為可讀取 value
的所有可組合函式安排重組作業。
下列三種方式皆在可組合項中宣告 MutableState
物件:
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }
這三種宣告作用相等,僅做為語法糖,運用在不同狀態用途中。挑選時,請考量哪種方式能在您編寫的可組合項中產生最簡單易讀的程式碼。
by
委派語法需要下列匯入項目:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
您可以將已儲存的值設為其他可組合項的參數,甚至設為陳述式中的邏輯來變更顯示的可組合項。舉例來說,如果在名稱空白的情況下,您不想顯示問候語,請在 if
陳述式中使用狀態:
@Composable fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { var name by remember { mutableStateOf("") } if (name.isNotEmpty()) { Text( text = "Hello, $name!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) } OutlinedTextField( value = name, onValueChange = { name = it }, label = { Text("Name") } ) } }
雖然 remember
可協助您在各次重組間保留狀態,但只要設定有所變更,狀態就無法保留。針對這種情況,您必須使用 rememberSaveable
。rememberSaveable
會自動儲存可儲存在 Bundle
中的任何值。其他值可在自訂儲存器物件中傳送。
其他支援的狀態類型
Compose 不會要求您使用 MutableState<T>
保留狀態,而是支援其他可觀察的類型。在 Compose 中讀取另一種可觀察類型之前,您必須將其轉換為 State<T>
。這樣一來,在狀態變更時,可組合項就能自動進行重組。
Compose 隨附一些函式,可根據 Android 應用程式中使用的常見可觀察類型建立 State<T>
:使用這些整合項目前,請新增適當的構件,如下所示:
Flow
:collectAsStateWithLifecycle()
collectAsStateWithLifecycle()
會以生命週期感知方式從Flow
收集值,讓應用程式可節省應用程式資源,並代表 ComposeState
中最新發出的值。請使用這個 API 做為在 Android 應用程式上收集資料流的建議方法。build.gradle
檔案必須包含下列依附元件 (應為 2.6.0-beta01 以上版本):
Kotlin
dependencies {
...
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
}
Groovy
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.8.7"
}
-
collectAsState
類似於collectAsStateWithLifecycle
,也會從Flow
收集值,並將資料流轉換成 ComposeState
。請為適用於各種平台的程式碼使用
collectAsState
,collectAsStateWithLifecycle
只適用於 Android。collectAsState
可在compose-runtime
中使用,因此不需要其他依附元件。 -
observeAsState()
會開始觀察這個LiveData
,並透過State
表示其值。build.gradle
檔案中必須包含下列依附元件:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.7.5")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.7.5"
}
-
subscribeAsState()
屬於擴充功能函式,可以將 RxJava2 的回應式串流 (例如Single
、Observable
、Completable
) 轉換成 ComposeState
。build.gradle
檔案中必須包含下列依附元件:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.7.5")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.7.5"
}
-
subscribeAsState()
屬於擴充功能函式,可以將 RxJava3 的回應式串流 (例如Single
、Observable
、Completable
) 轉換成 ComposeState
。build.gradle
檔案中必須包含下列依附元件:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.7.5")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.7.5"
}
有狀態與無狀態
使用 remember
儲存物件的可組合項會建立內部狀態,使該可組合項「有狀態」。舉例來說,HelloContent
就是個有狀態的可組合項,因為這個可組合項會在內部保留並修改自身的 name
狀態。這種做法在呼叫端不需要控制狀態的情況下很有用,不必自行管理狀態也能使用。不過,具有內部狀態的可組合項往往不易重複使用,也更難測試。
「無狀態」可組合項是指不含任何狀態的可組合項。如要達成無狀態,最簡單的方式就是使用狀態提升。
開發可重複使用的可組合項時,通常會想同時提供有狀態和無狀態的版本。有狀態版本對於不考慮狀態的呼叫端來說很方便,而對於需要控制或提升狀態的呼叫端來說,則一定要使用無狀態版本。
狀態提升
Compose 中的狀態提升是指將狀態移至可組合項呼叫端的模式,目的是讓可組合項變成無狀態。在 Jetpack Compose 中進行狀態提升的常見做法,是將狀態變數替換成兩個參數:
value: T
:目前顯示的值onValueChange: (T) -> Unit
:要求變更值的事件,其中T
是提議的新值
不過,您並未受限於使用 onValueChange
。如果該可組合項比較適合較特定的事件,請使用 lambda 定義事件。
以這種方式提升的狀態具備下列重要屬性:
- 單一真實資訊來源:採用移動而非複製的方式處理狀態,以確保真實資訊來源只有一個。這有助於避免錯誤。
- 封裝:必須是「有狀態」的可組合項才能修改狀態。這完全屬於內部。
- 可共用:提升過的狀態可讓多個可組合項共用。使用提升即可在其他可組合項中讀取
name
。 - 可攔截:無狀態可組合項的呼叫端可在變更狀態前決定忽略或修改事件。
- 已分離:無狀態可組合函式的狀態可以儲存在任何位置。舉例來說,現在可以將
name
移至ViewModel
中。
在這個例子裡,您從 HelloContent
中擷取出 name
和 onValueChange
,然後將兩者往上層移至呼叫 HelloContent
的 HelloScreen
可組合項。
@Composable fun HelloScreen() { var name by rememberSaveable { mutableStateOf("") } HelloContent(name = name, onNameChange = { name = it }) } @Composable fun HelloContent(name: String, onNameChange: (String) -> Unit) { Column(modifier = Modifier.padding(16.dp)) { Text( text = "Hello, $name", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") }) } }
將狀態從 HelloContent
中提升出來,就能更輕鬆地分析可組合項、在不同情境中重複使用可組合項,以及進行測試。HelloContent
已從儲存其狀態的方式中分離出來。「分離」的意思是,當您修改或取代 HelloScreen
時,不需要調整 HelloContent
的實作方式。
當狀態向下移動而事件向上移動時,這種模式稱為「單向資料流」。在這種情況下,狀態會從 HelloScreen
下降至 HelloContent
,而事件則從 HelloContent
上升至 HelloScreen
。跟隨單向資料流,即可從應用程式中儲存及變更狀態的部分,分離出在 UI 中顯示狀態的可組合項。
詳情請參閱「在何種情況下提升狀態」頁面。
在 Compose 中還原狀態
rememberSaveable
API 的行為與 remember
類似,因為它會在重新組成期間保留狀態,也會在使用已儲存的執行個體狀態機制重新建立活動或程序時保留狀態。舉例來說,當螢幕旋轉時就會發生這種情況。
儲存狀態的方式
所有新增至 Bundle
的資料類型都會自動儲存。如果想儲存無法新增至 Bundle
的項目,請參考以下幾種方法。
Parcelize
最簡單的解決方法是在物件中新增 @Parcelize
註解。物件會變得可包裝 (parcel) 且可組合 (bundle)。舉例來說,以下程式碼會產生一個可包裝的 City
資料類型,並儲存到狀態中。
@Parcelize data class City(val name: String, val country: String) : Parcelable @Composable fun CityScreen() { var selectedCity = rememberSaveable { mutableStateOf(City("Madrid", "Spain")) } }
MapSaver
如果 @Parcelize
因故不再適用,可改用 mapSaver
自行定義規則,將物件轉換為一組可讓系統儲存至 Bundle
的值。
data class City(val name: String, val country: String) val CitySaver = run { val nameKey = "Name" val countryKey = "Country" mapSaver( save = { mapOf(nameKey to it.name, countryKey to it.country) }, restore = { City(it[nameKey] as String, it[countryKey] as String) } ) } @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
ListSaver
如果不想為地圖定義索引鍵,也可以使用 listSaver
,並使用其索引當做索引鍵:
data class City(val name: String, val country: String) val CitySaver = listSaver<City, Any>( save = { listOf(it.name, it.country) }, restore = { City(it[0] as String, it[1] as String) } ) @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
Compose 中的狀態容器
可組合函式本身可用於管理簡易的狀態提升。不過,如果要追蹤的狀態數量增加,或是在可組合函式中產生要執行的邏輯,建議的做法是將邏輯和狀態責任委派給其他類別:狀態容器。
詳情請參閱 Compose 說明文件中的狀態提升頁面,或架構指南中更廣泛的「狀態容器和 UI 狀態」頁面。
在索引鍵變更時重新觸發 remember 計算作業
remember
API 經常與 MutableState
搭配使用:
var name by remember { mutableStateOf("") }
在這裡使用 remember
函式,可讓 MutableState
值在重組後繼續有效。
一般來說,remember
會採用 calculation
lambda 參數。初次執行 remember
時,系統會叫用 calculation
lambda 並儲存相關結果。而在重組期間,remember
會傳回上次儲存的值。
除了用來快取狀態之外,您也可以使用 remember
在組合中儲存作業的所有物件或結果,這些項目的初始化/計算費用十分昂貴。因此,您可能不會在每次重組時重複這個計算程序。舉例來說,建立 ShaderBrush
物件就是一項所費不貲的作業:
val brush = remember { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) }
remember
會儲存這個值,直到離開組合為止。然而,有一種方法可使快取值失效。由於 remember
API 也會使用 key
或 keys
參數,「如果其中有任何索引鍵發生異動,下次函式重組時」,remember
就會「讓快取失效,並再次執行 lambda 區塊的計算作業」。此機制可讓您控管物件在組合內的生命週期。請放心,計算作業的效力會持續到輸入內容變更為止,而非儲存的值離開組合為止。
以下舉例說明此機制的運作方式。
這個程式碼片段會建立 ShaderBrush
,並將其做為 Box
可組合元件的背景繪製。remember
則會儲存 ShaderBrush
例項,因為其重建成本較高 (如前文所述)。此外,remember
也會使用 avatarRes
做為 key1
參數,也就是所選的背景圖片。如果 avatarRes
有所變更,筆刷會隨新圖片重組,並重新套用至 Box
。當使用者從挑選器中選取其他圖片做為背景時,就可能會發生這種情況。
@Composable private fun BackgroundBanner( @DrawableRes avatarRes: Int, modifier: Modifier = Modifier, res: Resources = LocalContext.current.resources ) { val brush = remember(key1 = avatarRes) { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) } Box( modifier = modifier.background(brush) ) { /* ... */ } }
在下一個程式碼片段中,狀態會提升至純狀態容器類別 MyAppState
。此類別會公開 rememberMyAppState
函式,以便使用 remember
初始化類別的例項。公開這類函式,建立能在重組後持續有效的例項,是 Compose 中常見的模式。rememberMyAppState
函式會接收 windowSizeClass
,後者可做為 remember
的 key
參數。如果此參數有所變更,應用程式就需要利用最新的值重新建立純狀態容器類別。舉例來說,當使用者旋轉裝置時就可能發生這種情況。
@Composable private fun rememberMyAppState( windowSizeClass: WindowSizeClass ): MyAppState { return remember(windowSizeClass) { MyAppState(windowSizeClass) } } @Stable class MyAppState( private val windowSizeClass: WindowSizeClass ) { /* ... */ }
Compose 會利用類別的 equals 實作成果來判定索引鍵是否已變更,並使儲存的值失效。
透過索引鍵儲存狀態以在重組後繼續運作
rememberSaveable
API 是 remember
周圍的包裝函式,可將資料儲存在 Bundle
中。這個 API 不僅可讓狀態在重組後繼續運作,還能在活動重建和系統發起的程序終止時持續有效。rememberSaveable
接收 input
參數的目的與 remember
接收 keys
相同。「如有任何輸入內容變更,快取就會失效」。下次函式重組時,rememberSaveable
會重新執行 lambda 區塊的計算作業。
在以下範例中,rememberSaveable
會儲存 userTypedQuery
,直到 typedQuery
變更為止:
var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) { mutableStateOf( TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length)) ) }
瞭解詳情
如要進一步瞭解狀態與 Jetpack Compose,請參閱下列額外資源。
範例
程式碼研究室
影片
網誌
為您推薦
- 注意:系統會在 JavaScript 關閉時顯示連結文字
- 構建您的 Compose UI
- 在 Compose 中儲存 UI 狀態
- Compose 中的連帶效果