UDF = Unidirectional Data Flow(單向資料流)
這是一種 UI 架構設計模式,核心概念是:資料永遠只往一個方向流動。
State → UI → Event → ViewModel → State(更新)→ UI(重繪)
不只 Android,前端框架如 React、Vue 也廣泛採用這個模式。
相關關鍵字
| 關鍵字 | 說明 |
|---|---|
| UDF | Unidirectional Data Flow(單向資料流) |
| MVI | Model-View-Intent,UDF 的一種實作方式 |
| State Hoisting | Compose 的狀態提升,也是 UDF 概念 |
| SSOT | Single Source of Truth(單一資料來源) |
❌ 傳統寫法:分散的狀態
class MainViewModel : ViewModel() {
// 狀態分散在多個 Flow 中
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
private val _userName = MutableStateFlow("")
val userName: StateFlow<String> = _userName.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
fun incrementCount() {
_count.value = _count.value + 1
}
}
// UI 層:分別收集多個 Flow
@Composable
fun MainScreen(viewModel: MainViewModel = viewModel()) {
val count by viewModel.count.collectAsStateWithLifecycle()
val userName by viewModel.userName.collectAsStateWithLifecycle()
val isLoading by viewModel.isLoading.collectAsStateWithLifecycle()
// ...
}
問題:
✅ UDF 架構:集中的狀態
// 1. 定義 UiState data class
data class MainUiState(
val count: Int = 0,
val userName: String = "",
val isLoading: Boolean = false,
val error: String? = null
)
// 2. ViewModel 只暴露單一 StateFlow
class MainViewModel : ViewModel() {
private val _uiState = MutableStateFlow(MainUiState())
val uiState = _uiState.asStateFlow()
fun incrementCount() {
_uiState.update { it.copy(count = it.count + 1) }
}
}
// 3. UI 層:只收集一個 Flow
@Composable
fun MainScreen(viewModel: MainViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
MainContent(
uiState = uiState,
onIncrementClick = viewModel::incrementCount
)
}
MainViewModel.kt
// UiState:封裝所有 UI 狀態
data class MainUiState(
val count: Int = 0
)
class MainViewModel : ViewModel() {
private val _uiState = MutableStateFlow(MainUiState())
val uiState = _uiState.asStateFlow()
fun incrementCount() {
_uiState.update { it.copy(count = it.count + 1) }
}
}
MainScreen.kt
// Preview:直接傳入假資料
@Preview(showSystemUi = true, showBackground = true)
@Composable
fun MainScreenPreview() {
MyApplicationTheme {
MainContent(
uiState = MainUiState(count = 5),
onIncrementClick = {}
)
}
}
// Screen 層:連接 ViewModel 與 UI
@Composable
fun MainScreen(viewModel: MainViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
MainContent(
uiState = uiState,
onIncrementClick = viewModel::incrementCount
)
}
// Content 層:純 UI(Stateless)
@Composable
fun MainContent(
uiState: MainUiState,
onIncrementClick: () -> Unit = {}
) {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
Text(text = "count: ${uiState.count}")
Button(onClick = onIncrementClick) {
Text(text = "Click me")
}
}
}
}
┌─────────────────────────────────────────────────────────────┐
│ ViewModel │
│ │
│ data class MainUiState(count, userName, isLoading...) │
│ │ │
│ ┌────────────┴────────────┐ │
│ │ uiState: StateFlow │ │
│ └────────────┬────────────┘ │
└───────────────────────────┼─────────────────────────────────┘
↓ 單一 collect
┌───────────────────────────┴─────────────────────────────────┐
│ MainScreen │
│ val uiState = viewModel.uiState.collect() │
│ │ │
│ ↓ │
│ MainContent │
│ (Stateless, 只負責渲染) │
└─────────────────────────────────────────────────────────────┘
User 點擊按鈕
↓
MainContent: onClick = onIncrementClick
↓
MainScreen: viewModel.incrementCount()
↓
ViewModel: _uiState.update { it.copy(count = count + 1) }
↓
StateFlow emit 新的 MainUiState
↓
MainScreen: collectAsStateWithLifecycle() 收到更新
↓
MainContent(uiState = uiState) → UI 重繪
將狀態從子元件「提升」到父元件,子元件只接收參數:
// ✅ 好:Stateless,方便測試和 Preview
@Composable
fun MainContent(
uiState: MainUiState, // 狀態由外部傳入
onIncrementClick: () -> Unit // 事件回傳給外部
)
// ❌ 不好:自己管理狀態
@Composable
fun MainContent() {
var count by remember { mutableStateOf(0) } // 內部狀態
}
Compose 的智能重組機制會比較各欄位的值:
data class MainUiState(
val count: Int = 0,
val userName: String = ""
)
// 當只有 count 變化時
_uiState.update { it.copy(count = it.count + 1) }
// Text("count: ${uiState.count}") → 會重繪(值變了)
// Text("user: ${uiState.userName}") → 不會重繪(值沒變)
@Preview
@Composable
fun MainScreenPreview() {
MainContent(
uiState = MainUiState(count = 99), // 直接傳假資料
onIncrementClick = {}
)
}
| 項目 | 傳統寫法 | UDF 架構 |
|---|---|---|
| 狀態管理 | 分散在多個 Flow | 集中在單一 UiState |
| 狀態一致性 | 可能不一致 | 保證一致 |
| 可測試性 | 需 mock 多個 Flow | 只需準備 UiState |
| Preview | 困難 | 直接傳入假資料 |
| 除錯 | 追蹤多個狀態 | 單一狀態快照 |
| 擴展性 | 加欄位要加 Flow | 只需修改 data class |