Jetpack Compose狀態(tài)管理深度剖析:構(gòu)建高效響應(yīng)式UI的關(guān)鍵

概述

在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)?(mutableStateOfremember)

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é)合remembermutableStateOf,就能解決上述問(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í)更新。

remembermutableStateOf的底層原理

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 rememberSaveableremember的對(duì)比

rememberrememberSaveable都用于在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ò)countonIncrement回調(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)得以保存。將ComposeViewModel結(jié)合,能實(shí)現(xiàn)更靈活、穩(wěn)定的狀態(tài)管理。

6.1 ViewModel + StateFlow

ViewModel用于管理和存儲(chǔ)與UI相關(guān)的數(shù)據(jù),StateFlowLiveData是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上。StateFlowLiveData都是響應(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)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容