Android Compose UI 狀態提升 (State Hoisting) 與單向資料流 (Unidirectional Data Flow)
狀態提升 (State Hoisting) 是 Compose 開發中最重要的模式之一。
什麼是狀態提升?
「Hoisting」的意思是升起。狀態提升就是將 Composable 內部的狀態移出,改由參數傳入。
通常需要兩個參數:
- value: T:當前的狀態值。
- 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")
}
}
為什麼要狀態提升?
- 單一資料來源 (Single Source of Truth, SSOT):狀態只在一個地方管理(通常是 ViewModel 或頂層 Composable),避免多個元件狀態不一致。
- 可重用性 (Reusability):Stateless 元件可以被重複使用在不同場景,因為它不綁定特定狀態。
- 可測試性 (Testability):測試 Stateless 元件非常簡單,以此輸入參數,驗證輸出介面即可,不需要模擬複雜的狀態變化。
- 解耦 (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 down:
val count從 ViewModel -> Screen -> Content。 - Events flow up:
onClick->onIncrement->viewModel.increment()。
這種清晰的資料流向,讓除錯變得容易。如果你發現 UI 顯示錯誤,一定是傳下來的 State 有誤;如果按鈕沒反應,一定是傳上去的 Event 沒有被正確處理。
小結
- 盡量讓 Composable 保持 Stateless。
- 如果一個 Composable 需要狀態,考慮將其提升。
- 使用 ViewModel 作為最終的狀態持有者。