了解什么是 Unidirectional Data Flow — 单向数据流,一种架构模式,数据在封闭循环 State → View → Intent → Reducer → State 中移动,无需反馈。与双向绑定不同,UDF 保证状态变化仅通过显式操作(Intent/Event)发生,使数据流可预测且可追踪。根据 Google I/O 2024,UDF 是 Jetpack Compose 和 SwiftUI 应用程序(具有中高业务逻辑复杂性)的推荐架构。
要点
Unidirectional Data Flow (UDF) — 一种架构模式,数据沿封闭循环单向移动,排除 View 和 Model 之间的反馈。与双向绑定(UI 中的更改立即更新模型)不同,UDF 要求每次状态更改都执行显式操作(Intent、Event、Action)。这使得数据流完全可预测:任何时候都可以确定哪个操作导致了当前状态。
UDF 的概念来自 Web 框架 — Redux(JavaScript,2015)和 Elm(2012)— 并被改编用于移动开发。根据 Google I/O 2024,UDF 已成为 Jetpack Compose 的推荐架构,取代了带有 LiveData 的经典 MVVM。在 iOS 上,类似的方法在 Point-Free 的 The Composable Architecture (TCA) 中实现,根据 Swift Community 调查(2024),超过 15% 的 iOS 开发人员使用它。
UDF 的主要优势 — 单一真实来源 (SSOT):应用程序的整个状态存储在一个位置,并通过严格定义的操作进行更改。这简化了调试、测试和错误重现,因为每次状态更改都会被记录,并且可以通过重新发送相同的 Intent 来重现。
基本的 UDF 循环由四个步骤组成:State(当前状态)显示在 View 中;用户执行的操作转换为 Intent(意图);Intent 在 Reducer(纯函数)中处理,创建新的 State;新状态传递给 View 进行重新绘制。该循环在每次用户或系统事件时重复。
循环的每个元素都有严格的责任:State — 不可变(immutable)对象,描述特定时刻的屏幕状态;View — 显示 State 的函数;Intent — 描述用户意图的值(例如 LoginIntent.Submit);Reducer — 没有副作用的纯函数,接收当前的 State 和 Intent 并返回新的 State。副作用(网络请求、数据库)被移至单独的 Middleware 或 Effect 层。
根据 Google Android Architecture (2024) 文章,Reducer 的纯度是关键要求:如果 Reducer 包含网络调用或数据库写入,则数据流的测试和调试将变得不可能。所有副作用都必须在调用 Reducer 之前在 ViewModel 协程或 Swift Task 中执行,结果作为新的 Intent 发送。
在 Android 中,UDF 的实现基于三个 Jetpack 组件:ViewModel 管理生命周期,StateFlow 提供响应式状态流,Intent (sealed class) 描述所有可能的用户操作。View 通过 Compose 中的 collectAsState() 或 View 系统中的 observe() 订阅 StateFlow。
sealed class LoginIntent {
data object Submit : LoginIntent()
data class UpdateEmail(val value: String) : LoginIntent()
data class UpdatePassword(val value: String) : LoginIntent()
}
data class LoginState(
val email: String = "",
val password: String = "",
val isLoading: Boolean = false,
val error: String? = null
)
class LoginViewModel : ViewModel() {
private val _state = MutableStateFlow(LoginState())
val state: StateFlow<LoginState> = _state.asStateFlow()
fun onIntent(intent: LoginIntent) {
when (intent) {
is LoginIntent.UpdateEmail -> {
_state.update { it.copy(email = intent.value) }
}
is LoginIntent.UpdatePassword -> {
_state.update { it.copy(password = intent.value) }
}
is LoginIntent.Submit -> {
_state.update { it.copy(isLoading = true, error = null) }
loginUseCase(_state.value.email, _state.value.password)
.onSuccess {
_state.update { it.copy(isLoading = false) }
}
.onFailure { e ->
_state.update { it.copy(isLoading = false, error = e.message) }
}
}
}
}
}
@Composable
fun LoginScreen(viewModel: LoginViewModel) {
val state by viewModel.state.collectAsState()
LoginForm(
email = state.email,
onEmailChange = { viewModel.onIntent(LoginIntent.UpdateEmail(it)) },
password = state.password,
onPasswordChange = { viewModel.onIntent(LoginIntent.UpdatePassword(it)) },
isLoading = state.isLoading,
onSubmit = { viewModel.onIntent(LoginIntent.Submit) }
)
}该示例展示了 Android 中的完整 UDF 循环:LoginIntent 描述所有可能的操作(更改电子邮件、密码、提交表单),LoginState — 不可变状态,LoginViewModel 处理 Intent 并更新 StateFlow,Compose 屏幕通过 collectAsState() 订阅状态。每次状态更改都是处理特定 Intent 的结果,这使得数据流完全透明。
在 iOS 中,UDF 通过 Point-Free 的 The Composable Architecture (TCA) 或 iOS 17+ 的原生 Observable 模式实现。TCA 提供现成的 State + Action + Reducer + Store 循环,其中 Store 是唯一的真实来源,View 通过 @Observable 或 ObservableObject 订阅更改。
struct LoginState: Equatable {
var email = ""
var password = ""
var isLoading = false
var error: String?
}
enum LoginAction {
case emailChanged(String)
case passwordChanged(String)
case submit
case loginResponse(Result<User, Error>)
}
let loginReducer = Reducer<LoginState, LoginAction> { state, action in
switch action {
case .emailChanged(let email):
state.email = email
return .none
case .passwordChanged(let password):
state.password = password
return .none
case .submit:
state.isLoading = true
state.error = nil
return .run { send in
let result = await loginUseCase(state.email, state.password)
await send(.loginResponse(result))
}
case .loginResponse(.success):
state.isLoading = false
return .none
case .loginResponse(.failure(let error)):
state.isLoading = false
state.error = error.localizedDescription
return .none
}
}
struct LoginView: View {
let store: StoreOf<LoginReducer>
var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
Form {
TextField("Email", text: viewStore.binding(get: \.email, send: { .emailChanged($0) }))
SecureField("Password", text: viewStore.binding(get: \.password, send: { .passwordChanged($0) }))
Button("登录") { viewStore.send(.submit) }
}
}
}
}reducer loginReducer — 纯函数:不直接执行网络请求,而是返回一个将由 TCA 环境执行的 Effect。这允许隔离测试 reducer,在测试中替换效果。View 通过 WithViewStore 订阅 Store 的更改并通过 send() 发送 Action。TCA 在 Store 销毁时自动处理效果的取消,防止内存泄漏。
MVVM 和 UDF 经常被混淆,但两者之间存在根本区别。MVVM 是一种结构模式,将代码分为三层(Model、View、ViewModel),但不定义数据流的方向。UDF 是一种行为模式,描述数据在此结构内如何移动。在带有 LiveData 的 MVVM 中,既可以有双向绑定也可以有单向流 — UDF 为 MVVM 添加了严格的 Intent 处理规则。
根据 Android Developers (2024) 文档,Compose 的推荐架构是 MVVM 内部的 UDF:ViewModel 存储 State 并处理 Intent,View 订阅 State 并发送 Intent。Google 仅建议将带有 DataBinding 的双向绑定的经典 MVVM 用于没有业务逻辑的简单屏幕。对于 Jetpack Compose,主要场景是带有显式事件处理的 UDF。
比较表:
| 特性 | MVVM(经典) | MVVM + UDF |
|---|---|---|
| 数据流 | 未定义 | 严格的单向 |
| 状态变化 | 直接通过 setText() | 仅通过 Intent → Reducer |
| 单一真实来源 | 否 | 是 |
| Reducer 可测试性 | 低 | 高(纯函数) |
| Google 推荐 | 过时的方法 | Compose 的主要方法 |
最常见的错误 — Reducer 内部的副作用。习惯于 MVVM 的开发人员将网络请求直接放在 Intent 处理程序中,这使 Reducer 成为不纯函数并破坏了可测试性。所有效果都应作为值(Effect/SideEffect)返回并由框架基础结构执行。在 Android 中,使用 ViewModel 中的协程,在 TCA 中 — Effect.run。
第二个错误 — 过于详细的 Intent。每次按键、滑块移动和文本更改都会生成单独的 Intent。对于输入字段,这过于冗余 — 在这种情况下,可以在表单内部使用带有单向流的绑定(本地状态),而全局 Intent 仅在重要操作(提交、导航)时发送。
第三个错误 — 缺少效果取消处理。如果用户离开屏幕,协程或 Task 继续执行,结果可能会应用于已销毁的 View。在 Android 中,使用 viewModelScope.cancel() 或 takeWhileActive();在 TCA 中,效果在 Store 销毁时自动取消。根据 Google Issue Tracker (2024),未完成协程的内存泄漏是 Compose 应用程序崩溃的前五大原因之一。
常见问题
MVI(Model-View-Intent)— UDF 的一种特殊情况,具有三个必需元素:Intent(意图)、Model(状态)、View(显示)。主要区别在于,在 MVI 中,每个屏幕状态都由一个不可变结构(Sealed class)描述,而 View 是从 Model 到 UI 的纯函数。UDF 是一个更广泛的术语,描述任何单向流,包括 Redux 和 Elm。在 Google 文档中,术语 UDF 用作通用名称,而 MVI 用作具体实现。
UDF 对于只有一个无需验证的输入字段的屏幕、静态页面和占位符屏幕来说是多余的。如果屏幕没有业务逻辑且其状态不依赖于用户操作,UDF 会增加不必要的代码而没有好处。对于此类场景,简单单向绑定或 SwiftUI 中的 @State 就足够了。当屏幕可能的状态数超过 3-4 个或存在副作用时,UDF 是合理的。
由于 Reducer 是纯函数,其测试归结为使用不同的 State 和 Intent 组合进行调用,并检查生成的 State 和 Effect。在 Android 中,使用 Turbine 测试 StateFlow:发送 Intent,检查下一个 State 发射。在 TCA 中,有内置的 TestStore,可自动检查 Action 后是否只更改了预期的 State 字段并且只执行了预期的 Effect。
可以,组合是允许的,而且通常是最优的。对于表单内的输入字段,使用本地双向绑定(或 SwiftUI 中的 Binding),以免为每次按键创建 Intent。在提交表单时,发送一个包含收集数据的 Intent,由 Reducer 处理。这种混合方法 — 全局 UDF 与本地双向绑定 — 用于 70% 的商业 SwiftUI 应用程序(Swift Community Survey 2024 数据)。
所有三种模式都实现了具有单一真实来源的单向数据流。Elm(2012)— 一种函数式语言,首次引入了纯 Model → View → Update 循环。Redux (2015) 将 Elm 适配到 JavaScript,引入了 Store、Reducer 和 Action 的概念。UDF 是这些思想在移动开发中的推广。所有三种方法都通过原子(不可分割)状态更新保证更改的可预测性。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。