概述
在Jetpack Compose的世界里,狀態(tài)(State)作為驅(qū)動(dòng)UI更新的核心,其管理機(jī)制的掌握程度,直接關(guān)系到能否構(gòu)建出響應(yīng)迅速、穩(wěn)定且易于維護(hù)的界面。深入理解并熟練運(yùn)用Compose的狀態(tài)管理,是開(kāi)發(fā)者提升技能、打造優(yōu)質(zhì)應(yīng)用的必經(jīng)之路。
1.Compose 狀態(tài)是什么?
在Android開(kāi)發(fā)場(chǎng)景中,狀態(tài)指代那些隨時(shí)間推移發(fā)生變化,并對(duì)UI展示效果產(chǎn)生影響的數(shù)據(jù)。就像輸入框里不斷變化的文本內(nèi)容、按鈕被點(diǎn)擊的累計(jì)次數(shù),以及加載數(shù)據(jù)后得到的結(jié)果等,都屬于狀態(tài)的范疇。
傳統(tǒng)的View系統(tǒng)通過(guò)findViewById獲取控件,再手動(dòng)更新視圖,操作較為繁瑣。而Compose則截然不同,它以數(shù)據(jù)驅(qū)動(dòng)UI,數(shù)據(jù)一旦發(fā)生改變,UI便會(huì)自動(dòng)重新繪制,也就是進(jìn)行重組。這使得管理和保存這些動(dòng)態(tài)變化的數(shù)據(jù),成為Compose狀態(tài)管理的重中之重。那我們?nèi)绾喂芾砗蛣?chuàng)建狀態(tài)?
2.Compose框架如何管理和創(chuàng)建狀態(tài)?(mutableStateOf和remember)
2.1 mutableStateOf
在Compose中,mutableStateOf是管理可變狀態(tài)的得力工具。它創(chuàng)建的狀態(tài)對(duì)象能在UI中被觀察到,一旦狀態(tài)有變動(dòng),UI就會(huì)自動(dòng)更新。比如下面這段代碼,用mutableStateOf來(lái)記錄按鈕的點(diǎn)擊次數(shù):
@Composable
fun Counter() {
// 使用mutableStateOf創(chuàng)建可變的狀態(tài),通俗講:就是將數(shù)據(jù)保存到狀態(tài)中,我們一般直接把狀態(tài)當(dāng)成了數(shù)據(jù)。
var count = mutableStateOf(0)
Column {
Text(text = "點(diǎn)擊次數(shù): ${count.value}")
Button(onClick = { count.value++ }) {Text("點(diǎn)擊我")}
}
}
在這個(gè)示例里,mutableStateOf(0)創(chuàng)建了一個(gè)可觀察的狀態(tài)對(duì)象,count變量存儲(chǔ)著該狀態(tài)的值。每當(dāng)按鈕被點(diǎn)擊,count.value++更新值,進(jìn)而觸發(fā)UI更新。通俗講,Compose其實(shí)是將數(shù)據(jù)保存到狀態(tài)中,當(dāng)然我們一般把狀態(tài)(State)當(dāng)成數(shù)據(jù)。
然而,這段代碼存在一個(gè)潛在問(wèn)題:每次UI更新(即重組)時(shí),Counter()函數(shù)都會(huì)重新執(zhí)行,這就導(dǎo)致count每次都會(huì)被重置為0。所以每次點(diǎn)擊按鈕,count看起來(lái)都沒(méi)有變化。如何解決這個(gè)問(wèn)題?
2.2 remember
為了避免狀態(tài)在重組過(guò)程中丟失,Compose提供了remember函數(shù)。remember能夠在同一次重組中保存狀態(tài),確保狀態(tài)數(shù)據(jù)在重組時(shí)不會(huì)被重置。結(jié)合remember和mutableStateOf,就能解決上述問(wèn)題:
@Composable
fun Counter() {
// 使用mutableStateOf創(chuàng)建可變的狀態(tài),通俗講:就是將數(shù)據(jù)保存到狀態(tài)中,我們一般直接把狀態(tài)當(dāng)成了數(shù)據(jù)。
var count by remember { mutableStateOf(0) }
Column {
Text(text = "點(diǎn)擊次數(shù): $count")
Button(onClick = { count++ }) {Text("點(diǎn)擊我") }
}
}
這里,remember { mutableStateOf(0) }保證了count在同一次重組中維持狀態(tài)。當(dāng)按鈕點(diǎn)擊時(shí),count會(huì)正常增加,UI也會(huì)隨之實(shí)時(shí)更新。
remember和mutableStateOf的底層原理
mutableStateOf本質(zhì)是一個(gè)State<T>對(duì)象,內(nèi)部運(yùn)用了觀察者模式。一旦狀態(tài)改變,Compose會(huì)通知相關(guān)的Composable函數(shù)重新執(zhí)行,從而更新UI。
remember則是基于緩存機(jī)制實(shí)現(xiàn)的,它能在當(dāng)前組合范圍(Composition)內(nèi)緩存數(shù)據(jù),有效防止UI重組時(shí)狀態(tài)丟失。
上面的例子可以翻譯成下面這樣子。currentComposer就是當(dāng)前Activity的Compose
//緩存到當(dāng)前Activity的Composer中即:currentCompose
var count by currentComposer.cache(false){mutableStateOf(0) }
下篇文章會(huì)詳細(xì)講currentCompose 和currentRecompsoe
3. Compose重組機(jī)制(Recomposition)
3.1 重組是如何工作的?
重組是Compose的核心特性,當(dāng)狀態(tài)發(fā)生變化時(shí),Compose會(huì)重新執(zhí)行受影響的Composable函數(shù),并重新繪制UI,以此實(shí)現(xiàn)UI對(duì)數(shù)據(jù)變化的動(dòng)態(tài)響應(yīng)。
當(dāng)修改State對(duì)象的值(比如通過(guò)mutableStateOf修改),Compose會(huì)檢測(cè)到變化,并標(biāo)記需要更新的Composable。隨著這些Composable重新執(zhí)行,UI會(huì)依據(jù)新數(shù)據(jù)重新呈現(xiàn)。
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Log.d("Compose", "Counter重組")
Column {
Text("點(diǎn)擊次數(shù): $count")
Button(onClick = { count++ }) {
Text("點(diǎn)擊我")
}
}
}
在這個(gè)例子中,每次按鈕點(diǎn)擊使count更新,Compose就會(huì)觸發(fā)重組。通過(guò)Log輸出可以看到,每次點(diǎn)擊按鈕,Counter Composable都會(huì)重新執(zhí)行,并在日志中輸出"Counter重組"。
與傳統(tǒng)Android開(kāi)發(fā)中手動(dòng)調(diào)用invalidate()或setText()方法觸發(fā)UI更新不同,Compose的UI更新完全由數(shù)據(jù)驅(qū)動(dòng),狀態(tài)變化時(shí)UI自動(dòng)更新。
3.2 重組的精細(xì)化控制
Compose高效的重組機(jī)制是其一大優(yōu)勢(shì),即使?fàn)顟B(tài)變化,也不會(huì)導(dǎo)致整個(gè)Composable函數(shù)UI重新繪制,而是僅更新最小范圍的UI。
-
局部更新:Compose只會(huì)重組受狀態(tài)變化影響的部分Composables。比如按鈕點(diǎn)擊次數(shù)變化時(shí),只有顯示次數(shù)的
Text組件會(huì)更新,而不會(huì)重新創(chuàng)建整個(gè)Counter組件。 - 避免不必要的重組:Compose通過(guò)智能比較,精準(zhǔn)判斷哪些Composables需要更新,有效避免了重復(fù)計(jì)算和UI渲染,極大地優(yōu)化了性能。
比如上述Conuter的例子,狀態(tài)變化依然會(huì)導(dǎo)致整個(gè)Compose函數(shù)UI重新繪制的,因?yàn)橹亟M作用域是根據(jù)距離狀態(tài)變量最近的read代碼處的非inline函數(shù)和非inline lambda塊 。inline 函數(shù)或lambda比如 Column和Row,由于Column是inline函數(shù),往上就是更新整個(gè)Compose函數(shù)了,如果我們加一層wrapper,那么重組作用域就是wrapper函數(shù)內(nèi)。
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Log.d("Compose", "Counter重組")
Column {
TextWrapper{
Text("點(diǎn)擊次數(shù): $count")
}
Button(onClick = { count++ }) {
Text("點(diǎn)擊我")
}
}
}
fun TextWrapper(content:@Composable ()->Unit){
content()
}
3.3 重組的執(zhí)行過(guò)程
-
觸發(fā)重組:當(dāng)
mutableStateOf的值改變,Compose會(huì)標(biāo)記(Recorder)對(duì)應(yīng)的Composable需要重新執(zhí)行。 - 計(jì)算新的UI:Compose重新執(zhí)行該Composable,生成新的UI樹(shù)(UI結(jié)構(gòu))。
- 更新UI:Compose將新UI樹(shù)和當(dāng)前UI樹(shù)對(duì)比,僅更新有變化的部分,高效呈現(xiàn)更新后的界面。
3.4 為什么要關(guān)注重組?
深入理解Compose的重組機(jī)制對(duì)開(kāi)發(fā)者意義重大:
- 避免性能問(wèn)題:確保不會(huì)出現(xiàn)不必要的UI更新,優(yōu)化應(yīng)用性能,比如:TextWrapper盡量縮小重組作用域。
- 提高響應(yīng)性:保證UI始終與狀態(tài)同步,為用戶帶來(lái)流暢的體驗(yàn)。
4. remember vs rememberSaveable區(qū)別
remember只能在內(nèi)存中保存狀態(tài),適用于短生命周期的數(shù)據(jù)。而rememberSaveable支持持久化,即使進(jìn)程被殺死或發(fā)生配置更改(如旋轉(zhuǎn)屏幕),也能恢復(fù)狀態(tài)。
4.1 rememberSaveable與remember的對(duì)比
remember和rememberSaveable都用于在Compose中保存和恢復(fù)狀態(tài),但在處理配置變化和進(jìn)程銷(xiāo)毀的方式上有所不同。
-
remember:僅在組件重組時(shí)保留狀態(tài),遇到配置變化(如屏幕旋轉(zhuǎn))或進(jìn)程銷(xiāo)毀,狀態(tài)會(huì)丟失。
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Column {
Text("點(diǎn)擊次數(shù): $count")
Button(onClick = { count++ }) {
Text("點(diǎn)擊我")
}
}
}
-
rememberSaveable:類(lèi)似remember,但它會(huì)將狀態(tài)保存在Bundle中,在配置變化時(shí)恢復(fù)狀態(tài),適用于需要持久保存狀態(tài)的場(chǎng)景,如表單輸入。
@Composable
fun Counter() {
var count by rememberSaveable { mutableStateOf(0) }
Column {
Text("點(diǎn)擊次數(shù): $count")
Button(onClick = { count++ }) {
Text("點(diǎn)擊我")
}
}
}
兩者的關(guān)鍵區(qū)別就在于,rememberSaveable可以在配置變化時(shí)恢復(fù)狀態(tài),而remember僅在組件重組時(shí)保存狀態(tài)。
rememberSaveable的原理
rememberSaveable借助Bundle來(lái)保存狀態(tài),這使得狀態(tài)在配置變化時(shí)能夠恢復(fù)。比如屏幕旋轉(zhuǎn)或進(jìn)程銷(xiāo)毀后重新啟動(dòng),狀態(tài)會(huì)自動(dòng)恢復(fù)。
4.1 與 remember 的區(qū)別
為了方便大家快速查閱,下面以表格形式梳理了rememberSaveable與 remember 的區(qū)別:
| 特性 | remember | rememberSaveable |
|---|---|---|
| 生命周期 | 僅在組合期間保持狀態(tài) | 在配置更改后也能恢復(fù)狀態(tài) |
| 狀態(tài)保存 | 不支持 | 支持通過(guò) Bundle 保存和恢復(fù)狀態(tài) |
| 適用場(chǎng)景 | 短暫狀態(tài)(如動(dòng)畫(huà)、臨時(shí)變量) | 長(zhǎng)期狀態(tài)(如表單數(shù)據(jù)、分頁(yè)位置) |
5. 狀態(tài)提升(State Hoisting)
狀態(tài)提升是把狀態(tài)從子組件提取到父組件,實(shí)現(xiàn)UI與狀態(tài)管理的解耦。這樣做能提升組件的復(fù)用性、可測(cè)試性,還能讓多個(gè)組件共享相同狀態(tài)。
5.1 狀態(tài)提升的實(shí)際應(yīng)用
以計(jì)數(shù)器功能為例,為確保狀態(tài)在重組時(shí)不丟失,將狀態(tài)提升到父組件管理:
@Composable
fun ParentComponent() {
var count by remember { mutableStateOf(0) }
Counter(count, onIncrement = { count++ })
}
@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
Column {
Text("點(diǎn)擊次數(shù): $count")
Button(onClick = onIncrement) {Text("點(diǎn)擊我")}
}
}
在這個(gè)示例中:
-
ParentComponent組件負(fù)責(zé)管理count狀態(tài),并通過(guò)count和onIncrement回調(diào)傳遞給Counter組件。 -
Counter組件只負(fù)責(zé)展示文本框和響應(yīng)用戶輸入,實(shí)際狀態(tài)由父組件控制。
這種方式大大提升了Counter組件的復(fù)用性,無(wú)論有多少個(gè)Counter組件,都能通過(guò)父組件共享和管理同一個(gè)計(jì)數(shù)器狀態(tài)。
狀態(tài)提升的優(yōu)勢(shì)明顯:
-
復(fù)用性:
Counter組件變得獨(dú)立且無(wú)狀態(tài),可在多個(gè)地方復(fù)用。 - 解耦性:UI展示和狀態(tài)管理分離,增強(qiáng)了可維護(hù)性和可測(cè)試性。
5.2 什么時(shí)候不需要狀態(tài)提升?
并非所有場(chǎng)景都適合狀態(tài)提升。在一些簡(jiǎn)單的、狀態(tài)僅在組件內(nèi)部有效的情況下,直接在組件內(nèi)部管理狀態(tài)會(huì)更簡(jiǎn)潔。比如顯示計(jì)時(shí)器的組件,其狀態(tài)只在組件內(nèi)部使用,無(wú)需與外部共享,就沒(méi)必要進(jìn)行狀態(tài)提升:
@Composable
fun Timer() {
var time by remember { mutableStateOf(0) }
LaunchedEffect(true) {
while (true) {
delay(1000)
time++
}
}
Text("計(jì)時(shí)器: $time")
}
這里,Timer組件內(nèi)部管理time狀態(tài),無(wú)需與父組件交互,在內(nèi)部管理狀態(tài)就足夠了。
6. Compose與ViewModel結(jié)合
通常會(huì)借助ViewModel來(lái)持有和管理狀態(tài),確保數(shù)據(jù)在組件生命周期內(nèi)得以保存。將Compose和ViewModel結(jié)合,能實(shí)現(xiàn)更靈活、穩(wěn)定的狀態(tài)管理。
6.1 ViewModel + StateFlow
ViewModel用于管理和存儲(chǔ)與UI相關(guān)的數(shù)據(jù),StateFlow和LiveData是Compose中常用的可觀察數(shù)據(jù)類(lèi)型。通過(guò)collectAsState(針對(duì)Flow)或observeAsState(針對(duì)LiveData),Compose能自動(dòng)觀察數(shù)據(jù)變更并更新UI。
- 示例:使用
StateFlow
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count
fun increment() {
_count.value++
}
}
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
// collectAsState會(huì)自動(dòng)觀察StateFlow數(shù)據(jù),并更新UI
val count by viewModel.count.collectAsState()
Column {
Text("點(diǎn)擊次數(shù): $count")
Button(onClick = { viewModel.increment() }) {
Text("點(diǎn)擊我")
}
}
}
此例中,StateFlow用于管理計(jì)數(shù)器狀態(tài),collectAsState自動(dòng)監(jiān)聽(tīng)StateFlow變化并更新UI。
- ViewModel + LiveData
class CounterViewModel : ViewModel() {
private val _count = MutableLiveData(0)
val count: LiveData<Int> = _count
fun increment() {
_count.value = (_count.value ?: 0) + 1
}
}
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
// observeAsState會(huì)自動(dòng)觀察LiveData數(shù)據(jù),并更新UI
val count by viewModel.count.observeAsState(0)
Column {
Text("點(diǎn)擊次數(shù): $count")
Button(onClick = { viewModel.increment() }) {
Text("點(diǎn)擊我")
}
}
}
這里,LiveData管理計(jì)數(shù)器狀態(tài),observeAsState自動(dòng)監(jiān)聽(tīng)LiveData變化并更新UI。
collectAsState(適用于StateFlow)和observeAsState(適用于LiveData)都能自動(dòng)監(jiān)聽(tīng)數(shù)據(jù)變化,并及時(shí)將變化反映到UI上。StateFlow和LiveData都是響應(yīng)式的,數(shù)據(jù)變化時(shí)會(huì)自動(dòng)通知Compose觸發(fā)UI更新。
7. 總結(jié)
狀態(tài)是Compose的核心要素,直接驅(qū)動(dòng)UI更新。在實(shí)際開(kāi)發(fā)中,用mutableStateOf創(chuàng)建可變狀態(tài),結(jié)合remember保留狀態(tài),防止重組時(shí)數(shù)據(jù)丟失。rememberSaveable適用于需要持久化的狀態(tài)場(chǎng)景。狀態(tài)提升模式能解耦UI與數(shù)據(jù),提升組件復(fù)用性和可測(cè)試性。與ViewModel配合使用,則能在復(fù)雜應(yīng)用中保障數(shù)據(jù)的長(zhǎng)期存活和穩(wěn)定性。
深入理解Compose狀態(tài)管理機(jī)制,有助于開(kāi)發(fā)者更高效、優(yōu)雅地構(gòu)建響應(yīng)式UI,提升應(yīng)用性能和用戶體驗(yàn),在Android開(kāi)發(fā)的道路上邁出堅(jiān)實(shí)的步伐。
其實(shí)我們開(kāi)發(fā)Compose 遵循以下4要點(diǎn):
- 獲取數(shù)據(jù)并處理成想要的數(shù)據(jù);
- 把數(shù)據(jù)保存到狀態(tài)(State);
- 講狀態(tài)(State)關(guān)聯(lián)到Compose UI。
- 專(zhuān)注更新?tīng)顟B(tài)(State)