Android Compose UI 狀態提升 (State Hoisting) 與單向資料流 (Unidirectional Data Flow)

狀態提升 (State Hoisting) 是 Compose 開發中最重要的模式之一。

什麼是狀態提升?

「Hoisting」的意思是升起。狀態提升就是將 Composable 內部的狀態移出,改由參數傳入。

通常需要兩個參數:

  1. value: T:當前的狀態值。
  2. onValueChange: (T) -> Unit:當狀態需要改變時觸發的事件。

範例:從 Stateful 到 Stateless

Before (Stateful):

@Composable
fun Counter() {
    // 狀態鎖死在內部,外部無法控制,也無法測試
    var count by remember { mutableStateOf(0) }
    
    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}

After (Stateless - Hoisted):

@Composable
fun Counter(
    count: Int,              // 狀態由上面傳下來
    onIncrement: () -> Unit  // 事件往上傳
) {
    Button(onClick = onIncrement) {
        Text("Count: $count")
    }
}

為什麼要狀態提升?

  1. 單一資料來源 (Single Source of Truth, SSOT):狀態只在一個地方管理(通常是 ViewModel 或頂層 Composable),避免多個元件狀態不一致。
  2. 可重用性 (Reusability):Stateless 元件可以被重複使用在不同場景,因為它不綁定特定狀態。
  3. 可測試性 (Testability):測試 Stateless 元件非常簡單,以此輸入參數,驗證輸出介面即可,不需要模擬複雜的狀態變化。
  4. 解耦 (Decoupling):UI 邏輯與業務邏輯分離。

實戰:整合 ViewModel

通常我們會將狀態提升到 ViewModel 中。

// 1. ViewModel 持有狀態與邏輯
class CounterViewModel : ViewModel() {
    private val _count = MutableStateFlow(0)
    val count = _count.asStateFlow()

    fun increment() {
        _count.value++
    }
}

// 2. Screen 層負責連接 ViewModel (State Holder)
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
    val count by viewModel.count.collectAsStateWithLifecycle()
    
    // 將狀態與事件傳遞給 Stateless 元件
    CounterContent(
        count = count,
        onIncrement = { viewModel.increment() }
    )
}

// 3. 純 UI 元件 (Stateless)
@Composable
fun CounterContent(
    count: Int, 
    onIncrement: () -> Unit
) {
    Button(onClick = onIncrement) {
        Text("Count: $count")
    }
}

單向資料流 (Unidirectional Data Flow, UDF)

狀態提升完美體現了單向資料流的精神:

  • State flows downval count 從 ViewModel -> Screen -> Content。
  • Events flow uponClick -> onIncrement -> viewModel.increment()

這種清晰的資料流向,讓除錯變得容易。如果你發現 UI 顯示錯誤,一定是傳下來的 State 有誤;如果按鈕沒反應,一定是傳上去的 Event 沒有被正確處理。

小結

  • 盡量讓 Composable 保持 Stateless
  • 如果一個 Composable 需要狀態,考慮將其提升
  • 使用 ViewModel 作為最終的狀態持有者。