Jetpack Compose 【二】狀態管理詳解

前言

在 Jetpack Compose 中,狀態(State)是驅動 UI 更新的核心概念。理解 Compose 中的狀態管理機制,有助于構建響應式界面,并提升應用的穩定性與可維護性。

1. 什么是狀態?

在 Android 開發中,狀態通常指的是界面中隨時間變化、影響 UI 展示的數據。例如:

  • 表單輸入框的文本
  • 按鈕的點擊次數
  • 加載數據的結果

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

2. 為什么需要 mutableStateOfremember?

2.1 引入 mutableStateOf

在 Compose 中,mutableStateOf 是用來創建和管理可變狀態的工具。它創建的狀態對象可以在 UI 中觀察,狀態變化時會自動觸發 UI 更新。例如,下面的代碼使用 mutableStateOf 來存儲按鈕的點擊次數:

@Composable
fun Counter() {
    // 使用 mutableStateOf 創建可變的狀態
    var count = mutableStateOf(0)

    Column {
        Text(text = "點擊次數: ${count.value}")
        Button(onClick = { count.value++ }) {
            Text("點擊我")
        }
    }
}

在這個例子中,mutableStateOf(0) 創建了一個可觀察的狀態對象,count 變量持有這個狀態的值。每當按鈕點擊時,count.value++ 會更新這個值,并觸發 UI 更新。

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

2.2 引入 remember

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

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

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

在這個代碼中,remember { mutableStateOf(0) } 確保 count 在同一次重組過程中保持狀態。當點擊按鈕時,count 會正確增加,而 UI 也會隨著 count 的變化自動更新。

remembermutableStateOf 的底層原理

  • mutableStateOf 是一個 State<T> 對象,內部使用了觀察者模式,當狀態變化時,Compose 會通知相關的 Composable 重新執行并更新 UI。
  • remember 本質是一個緩存機制,能夠在當前組合范圍(Composition)內保持數據,防止 UI 重組時丟失狀態。

3. Compose 重組機制(Recomposition)

3.1 重組是如何工作的?

在 Compose 中,重組(Recomposition)是指當狀態發生變化時,Compose 會重新執行受影響的 Composable 函數,并重新繪制 UI。重組是 Compose 的核心特性,它使得 UI 動態響應數據的變化。

當我們修改一個 State 對象的值時(例如,通過 mutableStateOf),Compose 會檢測到這個變化,并標記需要更新的 Composable。隨著 Composable 被重新執行,UI 會根據新的數據重新呈現。

重組與 UI 更新的關系

在傳統的 Android 開發中,UI 更新是手動觸發的,比如調用 invalidate()setText() 方法。而在 Compose 中,UI 更新由數據驅動,當狀態發生變化時,UI 會自動更新。

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

    Log.d("Compose", "Counter 重組")

    Column {
        Text("點擊次數: $count")
        Button(onClick = { count++ }) {
            Text("點擊我")
        }
    }
}

在這個例子中,每次按鈕被點擊時,count 會更新,Compose 會觸發重組。通過 Log 輸出,我們可以看到每次點擊按鈕時,Counter Composable 會重新執行,并在日志中輸出 "Counter 重組"。

3.2 重組的精細化控制

Compose 的一個關鍵優勢是高效的重組機制,即使狀態變化,也不會導致整個 UI 被重新繪制。Compose 會根據需要更新最小范圍的 UI。

  • 局部更新:Compose 會僅重組受狀態變化影響的部分 Composables。例如,如果按鈕的點擊次數變化,只會更新顯示次數的 Text 組件,而不會重新創建整個 Counter 組件。
  • 避免不必要的重組:Compose 通過智能比較來確定哪些 Composables 需要更新,避免了重復的計算和 UI 渲染,優化了性能。

3.3 重組的執行過程

  1. 觸發重組:當 mutableStateOf 的值發生變化時,Compose 會標記這個 Composable 需要重新執行。
  2. 計算新的 UI:Compose 會重新執行該 Composable,計算新的 UI 樹(UI 結構)。
  3. 更新 UI:Compose 會將新的 UI 樹與當前的 UI 樹進行對比,只更新發生變化的部分,從而高效地呈現更新后的界面。

3.4 為什么要關注重組?

理解 Compose 的重組機制對開發者非常重要,因為它能夠幫助你:

  • 避免性能問題:確保不必要的 UI 更新不會發生,優化性能。
  • 提高響應性:確保 UI 始終與狀態保持同步,用戶體驗流暢。

4. remember vs rememberSaveable

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

4.1 rememberSaveableremember 的對比

rememberrememberSaveable 都用于在 Compose 中保存和恢復狀態,但它們的區別在于如何處理配置變化(如屏幕旋轉)和進程銷毀。

remember

remember 用于保存狀態,只在組件重組時保留狀態。配置變化(如屏幕旋轉)或進程銷毀時,狀態會丟失。

示例:

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

    Column {
        Text("點擊次數: $count")
        Button(onClick = { count++ }) {
            Text("點擊我")
        }
    }
}

rememberSaveable

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

示例:

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

    Column {
        Text("點擊次數: $count")
        Button(onClick = { count++ }) {
            Text("點擊我")
        }
    }
}
  • 區別rememberSaveable 可以在配置變化時恢復狀態,而 remember 只在組件重組時保存狀態。

rememberSaveable 的原理

rememberSaveable 使用 Bundle 來保存狀態,使得狀態能在配置變化時恢復。當屏幕旋轉或進程銷毀后,狀態會自動恢復。

5. 狀態提升(State Hoisting)

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

5.1 狀態提升的實際應用

為了實現計數器功能且保證狀態在重組時不丟失,我們將狀態提升到父組件中進行管理。如下所示:

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

    Counter(count, onIncrement = { count++ })
}

@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
    Column {
        Text("點擊次數: $count")
        Button(onClick = onIncrement) {
            Text("點擊我")
        }
    }
}

在這個例子中:

  • ParentComponent 組件管理 count 狀態,并通過 countonIncrement 回調傳遞給 Counter 組件。
  • Counter 組件僅負責展示文本框和響應用戶輸入,實際的狀態由父組件控制。

這種方式可以確保 Counter 組件的復用性:無論多少個 Counter 組件,它們都可以通過父組件共享和管理同一個計數器狀態。

優勢:

  • 復用性Counter 組件變得獨立且無狀態,能在多個地方復用。
  • 解耦性:UI 展示和狀態管理分離,提升了可維護性和測試性。

5.2 什么時候不需要狀態提升?

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

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

    Text("計時器: $time")
}

在這個例子中,Timer 組件內部管理 time 狀態,它不需要和父組件交互,因此不需要進行狀態提升。狀態直接管理在 Timer 內部就足夠了。

6. Compose 與 ViewModel 狀態結合

通常我們通常會使用 ViewModel 來持有和管理狀態,確保數據在組件生命周期內得以保存。結合 ComposeViewModel,可以實現更加靈活和穩定的狀態管理。

6.1 ViewModel + StateFlow / LiveData

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

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

在這個例子中,StateFlow 被用來管理計數器的狀態。collectAsState 會自動監聽 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 數據,并更新 UI
    val count by viewModel.count.observeAsState(0)

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

在這個例子中,LiveData 用于管理計數器的狀態。observeAsState 會自動監聽 LiveData 的變化,并在數據變更時更新 UI。

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

7. 總結

  • 狀態 是 Compose 的核心,驅動 UI 更新。
  • 使用 mutableStateOf 創建可變狀態,結合 remember 來保留狀態,避免重組時數據丟失。
  • rememberSaveable 適用于需要持久化的狀態,如配置更改時需要保留的數據。
  • 采用狀態提升模式,解耦 UI 與數據,提升組件復用性和可測試性。
  • ViewModel 配合使用,可以在復雜應用中保持數據的長期存活和穩定性。

通過理解 Compose 狀態管理機制,可以更高效、優雅地實現響應式 UI,提升應用性能與用戶體驗。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容