Jetpack Compose 【二】狀態(tài)管理詳解

前言

在 Jetpack Compose 中,狀態(tài)(State)是驅(qū)動 UI 更新的核心概念。理解 Compose 中的狀態(tài)管理機(jī)制,有助于構(gòu)建響應(yīng)式界面,并提升應(yīng)用的穩(wěn)定性與可維護(hù)性。

1. 什么是狀態(tài)?

在 Android 開發(fā)中,狀態(tài)通常指的是界面中隨時(shí)間變化、影響 UI 展示的數(shù)據(jù)。例如:

  • 表單輸入框的文本
  • 按鈕的點(diǎn)擊次數(shù)
  • 加載數(shù)據(jù)的結(jié)果

傳統(tǒng) View 系統(tǒng)通過 findViewById 獲取控件,再手動更新視圖。而在 Compose 中,UI 是由數(shù)據(jù)驅(qū)動的,數(shù)據(jù)變化會觸發(fā) UI 重新繪制(即 重組)。因此,管理和保存這些變化的數(shù)據(jù)成為 Compose 狀態(tài)管理的核心。

2. 為什么需要 mutableStateOfremember?

2.1 引入 mutableStateOf

在 Compose 中,mutableStateOf 是用來創(chuàng)建和管理可變狀態(tài)的工具。它創(chuàng)建的狀態(tài)對象可以在 UI 中觀察,狀態(tài)變化時(shí)會自動觸發(fā) UI 更新。例如,下面的代碼使用 mutableStateOf 來存儲按鈕的點(diǎn)擊次數(shù):

@Composable
fun Counter() {
    // 使用 mutableStateOf 創(chuàng)建可變的狀態(tài)
    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)對象,count 變量持有這個(gè)狀態(tài)的值。每當(dāng)按鈕點(diǎn)擊時(shí),count.value++ 會更新這個(gè)值,并觸發(fā) UI 更新。

然而,在這個(gè)代碼中存在一個(gè)問題:每次 UI 更新(即重組)都會重新執(zhí)行 Counter() 函數(shù),這意味著 count 每次都會被重置為 0。這就導(dǎo)致每次點(diǎn)擊按鈕時(shí),count 始終不變。

2.2 引入 remember

為了避免每次重組時(shí)狀態(tài)丟失,Compose 提供了 remember 函數(shù)。remember 會在同一次重組中保存狀態(tài),使得狀態(tài)數(shù)據(jù)能夠在重組過程中保持不變。我們可以結(jié)合 remembermutableStateOf 來解決這個(gè)問題:

@Composable
fun Counter() {
    // 使用 remember 來保留狀態(tài)
    var count by remember { mutableStateOf(0) }

    Column {
        Text(text = "點(diǎn)擊次數(shù): $count")
        Button(onClick = { count++ }) {
            Text("點(diǎn)擊我")
        }
    }
}

在這個(gè)代碼中,remember { mutableStateOf(0) } 確保 count 在同一次重組過程中保持狀態(tài)。當(dāng)點(diǎn)擊按鈕時(shí),count 會正確增加,而 UI 也會隨著 count 的變化自動更新。

remembermutableStateOf 的底層原理

  • mutableStateOf 是一個(gè) State<T> 對象,內(nèi)部使用了觀察者模式,當(dāng)狀態(tài)變化時(shí),Compose 會通知相關(guān)的 Composable 重新執(zhí)行并更新 UI。
  • remember 本質(zhì)是一個(gè)緩存機(jī)制,能夠在當(dāng)前組合范圍(Composition)內(nèi)保持?jǐn)?shù)據(jù),防止 UI 重組時(shí)丟失狀態(tài)。

3. Compose 重組機(jī)制(Recomposition)

3.1 重組是如何工作的?

在 Compose 中,重組(Recomposition)是指當(dāng)狀態(tài)發(fā)生變化時(shí),Compose 會重新執(zhí)行受影響的 Composable 函數(shù),并重新繪制 UI。重組是 Compose 的核心特性,它使得 UI 動態(tài)響應(yīng)數(shù)據(jù)的變化。

當(dāng)我們修改一個(gè) State 對象的值時(shí)(例如,通過 mutableStateOf),Compose 會檢測到這個(gè)變化,并標(biāo)記需要更新的 Composable。隨著 Composable 被重新執(zhí)行,UI 會根據(jù)新的數(shù)據(jù)重新呈現(xiàn)。

重組與 UI 更新的關(guān)系

在傳統(tǒng)的 Android 開發(fā)中,UI 更新是手動觸發(fā)的,比如調(diào)用 invalidate()setText() 方法。而在 Compose 中,UI 更新由數(shù)據(jù)驅(qū)動,當(dāng)狀態(tài)發(fā)生變化時(shí),UI 會自動更新。

@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)擊時(shí),count 會更新,Compose 會觸發(fā)重組。通過 Log 輸出,我們可以看到每次點(diǎn)擊按鈕時(shí),Counter Composable 會重新執(zhí)行,并在日志中輸出 "Counter 重組"。

3.2 重組的精細(xì)化控制

Compose 的一個(gè)關(guān)鍵優(yōu)勢是高效的重組機(jī)制,即使?fàn)顟B(tài)變化,也不會導(dǎo)致整個(gè) UI 被重新繪制。Compose 會根據(jù)需要更新最小范圍的 UI。

  • 局部更新:Compose 會僅重組受狀態(tài)變化影響的部分 Composables。例如,如果按鈕的點(diǎn)擊次數(shù)變化,只會更新顯示次數(shù)的 Text 組件,而不會重新創(chuàng)建整個(gè) Counter 組件。
  • 避免不必要的重組:Compose 通過智能比較來確定哪些 Composables 需要更新,避免了重復(fù)的計(jì)算和 UI 渲染,優(yōu)化了性能。

3.3 重組的執(zhí)行過程

  1. 觸發(fā)重組:當(dāng) mutableStateOf 的值發(fā)生變化時(shí),Compose 會標(biāo)記這個(gè) Composable 需要重新執(zhí)行。
  2. 計(jì)算新的 UI:Compose 會重新執(zhí)行該 Composable,計(jì)算新的 UI 樹(UI 結(jié)構(gòu))。
  3. 更新 UI:Compose 會將新的 UI 樹與當(dāng)前的 UI 樹進(jìn)行對比,只更新發(fā)生變化的部分,從而高效地呈現(xiàn)更新后的界面。

3.4 為什么要關(guān)注重組?

理解 Compose 的重組機(jī)制對開發(fā)者非常重要,因?yàn)樗軌驇椭悖?/p>

  • 避免性能問題:確保不必要的 UI 更新不會發(fā)生,優(yōu)化性能。
  • 提高響應(yīng)性:確保 UI 始終與狀態(tài)保持同步,用戶體驗(yàn)流暢。

4. remember vs rememberSaveable

  • remember 只能在 內(nèi)存 中保存狀態(tài),適用于短生命周期的數(shù)據(jù)。
  • rememberSaveable 支持持久化,即使在 進(jìn)程被殺死或配置更改(如旋轉(zhuǎn)屏幕)時(shí),也能恢復(fù)狀態(tài)。

4.1 rememberSaveableremember 的對比

rememberrememberSaveable 都用于在 Compose 中保存和恢復(fù)狀態(tài),但它們的區(qū)別在于如何處理配置變化(如屏幕旋轉(zhuǎn))和進(jìn)程銷毀。

remember

remember 用于保存狀態(tài),只在組件重組時(shí)保留狀態(tài)。配置變化(如屏幕旋轉(zhuǎn))或進(jìn)程銷毀時(shí),狀態(tài)會丟失。

示例:

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

    Column {
        Text("點(diǎn)擊次數(shù): $count")
        Button(onClick = { count++ }) {
            Text("點(diǎn)擊我")
        }
    }
}

rememberSaveable

rememberSaveable 類似 remember,但它會將狀態(tài)保存在 Bundle 中,在配置變化時(shí)恢復(fù)狀態(tài)。適用于需要保持狀態(tài)的場景,如表單輸入。

示例:

@Composable
fun Counter() {
    var count by rememberSaveable { mutableStateOf(0) }

    Column {
        Text("點(diǎn)擊次數(shù): $count")
        Button(onClick = { count++ }) {
            Text("點(diǎn)擊我")
        }
    }
}
  • 區(qū)別rememberSaveable 可以在配置變化時(shí)恢復(fù)狀態(tài),而 remember 只在組件重組時(shí)保存狀態(tài)。

rememberSaveable 的原理

rememberSaveable 使用 Bundle 來保存狀態(tài),使得狀態(tài)能在配置變化時(shí)恢復(fù)。當(dāng)屏幕旋轉(zhuǎn)或進(jìn)程銷毀后,狀態(tài)會自動恢復(fù)。

5. 狀態(tài)提升(State Hoisting)

狀態(tài)提升是將狀態(tài)從子組件提取到父組件,使 UI 與狀態(tài)管理解耦。這種做法提升了組件的復(fù)用性、可測試性,并且允許多個(gè)組件共享相同的狀態(tài)。

5.1 狀態(tài)提升的實(shí)際應(yīng)用

為了實(shí)現(xiàn)計(jì)數(shù)器功能且保證狀態(tài)在重組時(shí)不丟失,我們將狀態(tài)提升到父組件中進(jìn)行管理。如下所示:

@Composable
fun ParentComponent() {
    var count by remember { mutableStateOf(0) } // 狀態(tài)提升到父組件

    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 組件管理 count 狀態(tài),并通過 countonIncrement 回調(diào)傳遞給 Counter 組件。
  • Counter 組件僅負(fù)責(zé)展示文本框和響應(yīng)用戶輸入,實(shí)際的狀態(tài)由父組件控制。

這種方式可以確保 Counter 組件的復(fù)用性:無論多少個(gè) Counter 組件,它們都可以通過父組件共享和管理同一個(gè)計(jì)數(shù)器狀態(tài)。

優(yōu)勢:

  • 復(fù)用性Counter 組件變得獨(dú)立且無狀態(tài),能在多個(gè)地方復(fù)用。
  • 解耦性:UI 展示和狀態(tài)管理分離,提升了可維護(hù)性和測試性。

5.2 什么時(shí)候不需要狀態(tài)提升?

并不是所有情況下都需要進(jìn)行狀態(tài)提升。在一些簡單的、狀態(tài)完全局部的組件中,直接在組件內(nèi)部管理狀態(tài)更加簡潔。例如,如果我們有一個(gè)組件用于顯示計(jì)時(shí)器,它的狀態(tài)只在組件內(nèi)部有效,不需要與外部共享,那么就沒有必要提升狀態(tài):

@Composable
fun Timer() {
    var time by remember { mutableStateOf(0) }
    
    LaunchedEffect(true) {
        while (true) {
            delay(1000)
            time++
        }
    }

    Text("計(jì)時(shí)器: $time")
}

在這個(gè)例子中,Timer 組件內(nèi)部管理 time 狀態(tài),它不需要和父組件交互,因此不需要進(jìn)行狀態(tài)提升。狀態(tài)直接管理在 Timer 內(nèi)部就足夠了。

6. Compose 與 ViewModel 狀態(tài)結(jié)合

通常我們通常會使用 ViewModel 來持有和管理狀態(tài),確保數(shù)據(jù)在組件生命周期內(nèi)得以保存。結(jié)合 ComposeViewModel,可以實(shí)現(xiàn)更加靈活和穩(wěn)定的狀態(tài)管理。

6.1 ViewModel + StateFlow / LiveData

ViewModel 用于管理和存儲 UI 相關(guān)的數(shù)據(jù),而 StateFlowLiveData 是在 Compose 中常用的兩種可觀察的數(shù)據(jù)類型。通過 collectAsState(對于 Flow)或 observeAsState(對于 LiveData),Compose 會自動觀察數(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 會自動觀察 StateFlow 數(shù)據(jù),并更新 UI
    val count by viewModel.count.collectAsState()

    Column {
        Text("點(diǎn)擊次數(shù): $count")
        Button(onClick = { viewModel.increment() }) {
            Text("點(diǎn)擊我")
        }
    }
}

在這個(gè)例子中,StateFlow 被用來管理計(jì)數(shù)器的狀態(tài)。collectAsState 會自動監(jiān)聽 StateFlow 的變化并更新 UI。

示例:使用 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 會自動觀察 LiveData 數(shù)據(jù),并更新 UI
    val count by viewModel.count.observeAsState(0)

    Column {
        Text("點(diǎn)擊次數(shù): $count")
        Button(onClick = { viewModel.increment() }) {
            Text("點(diǎn)擊我")
        }
    }
}

在這個(gè)例子中,LiveData 用于管理計(jì)數(shù)器的狀態(tài)。observeAsState 會自動監(jiān)聽 LiveData 的變化,并在數(shù)據(jù)變更時(shí)更新 UI。

  • collectAsState(適用于 StateFlow)和 observeAsState(適用于 LiveData)能夠自動監(jiān)聽數(shù)據(jù)的變化,并將變化及時(shí)反映到 UI 上。
  • StateFlowLiveData 都是響應(yīng)式的,當(dāng)數(shù)據(jù)變化時(shí),它們會自動通知 Compose 來觸發(fā) UI 更新。

7. 總結(jié)

  • 狀態(tài) 是 Compose 的核心,驅(qū)動 UI 更新。
  • 使用 mutableStateOf 創(chuàng)建可變狀態(tài),結(jié)合 remember 來保留狀態(tài),避免重組時(shí)數(shù)據(jù)丟失。
  • rememberSaveable 適用于需要持久化的狀態(tài),如配置更改時(shí)需要保留的數(shù)據(jù)。
  • 采用狀態(tài)提升模式,解耦 UI 與數(shù)據(jù),提升組件復(fù)用性和可測試性。
  • ViewModel 配合使用,可以在復(fù)雜應(yīng)用中保持?jǐn)?shù)據(jù)的長期存活和穩(wěn)定性。

通過理解 Compose 狀態(tài)管理機(jī)制,可以更高效、優(yōu)雅地實(shí)現(xiàn)響應(yīng)式 UI,提升應(yīng)用性能與用戶體驗(yàn)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容