應用程式中的狀態指的是任何可能隨時間變化的值。範圍相當廣泛 並包含從 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 中的思維」。
可組合項中的狀態
可組合函式可以使用 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
採用生命週期感知方式,讓應用程式能: 並節省應用程式資源代表 撰寫State
。使用此 API 做為收集資料流的建議方式 Android 應用程式。build.gradle
檔案必須包含下列依附元件 (應為 2.6.0-beta01 以上版本):
Kotlin
dependencies {
...
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.2")
}
Groovy
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.2"
}
-
collectAsState
類似於collectAsStateWithLifecycle
,也會從Flow
收集值,並將資料流轉換成 ComposeState
。請為適用於各種平台的程式碼使用
collectAsState
,collectAsStateWithLifecycle
只適用於 Android。collectAsState
可在compose-runtime
中使用,因此不需要其他依附元件。 -
observeAsState()
會開始觀察這個LiveData
,並透過State
表示其值。build.gradle
檔案中必須包含下列依附元件:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.6.8")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.6.8"
}
-
subscribeAsState()
屬於擴充功能函式,可以將 RxJava2 的回應式串流 (例如Single
、Observable
、Completable
) 轉換成 ComposeState
。build.gradle
檔案中必須包含下列依附元件:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.6.8")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.6.8"
}
-
subscribeAsState()
屬於擴充功能函式,可以將 RxJava3 的回應式串流 (例如Single
、Observable
、Completable
) 轉換成 ComposeState
。build.gradle
檔案中必須包含下列依附元件:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.6.8")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.6.8"
}
有狀態與無狀態
使用 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
類似,因為 API
在重新組成之間保留狀態,以及在活動或程序之間保留狀態
使用已儲存的執行個體狀態機制重建。舉例來說
當使用者旋轉螢幕時
儲存狀態的方式
所有新增至 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 中的連帶效果