MVVM(Model-View-ViewModel)——一种架构模式,其中ViewModel替代Presenter并使用反应式机制与View通信:SwiftUI中的ObservableObject,Android中的LiveData/StateFlow。ViewModel没有对View的引用——数据通过订阅传递,这消除了对ViewContract接口的需求,并使测试更加简单。Apple自2019年起推荐MVVM与SwiftUI结合使用,Google则推荐MVVM与Jetpack作为Android的官方架构。更多信息请参阅Android Architecture Guide。
要点
MVVM(Model-View-ViewModel)——一种由John Gossman于2005年为Microsoft的Windows Presentation Foundation(WPF)描述的架构模式。ViewModel是包含屏幕状态和业务逻辑的核心组件,但不引用View。数据通过反应式绑定机制传递:View订阅ViewModel的变化,并在数据变化时自动重新渲染。
MVVM与MVP的关键区别——没有ViewContract。在MVP中,Presenter调用view.showUser(data)等方法,即Presenter主动将数据“推送”到View。在MVVM中,View通过订阅从ViewModel“拉取”数据:ViewModel不知道是否有订阅者。这消除了分离View的问题——如果Activity在旋转时被销毁,ViewModel继续工作,新的Activity只需订阅当前数据。从2020年开始,我们在IT Sectr的所有新项目中使用MVVM——代码变得更可预测,测试更稳定。
| 组件 | 职责 | 平台 |
|---|---|---|
| Model | 数据、业务逻辑、仓库 | Android/iOS |
| View | 显示、订阅ViewModel | Activity/Composable、UIView/SwiftUI View |
| ViewModel | 屏幕状态、逻辑、导航 | ViewModel(Jetpack)、ObservableObject |
反应式绑定——MVVM的基础。在Android中,LiveData(Jetpack的一部分)是一个可观察的数据存储。Activity通过observe()进行订阅:viewModel.user.observe(this) { user -> binding.name.text = user.name }。当user发生变化时,所有订阅者自动收到新值。在iOS中,SwiftUI使用ViewModel中的@Published属性——变化会自动重新渲染View。这消除了MVP中所需的手动showUser/hideLoading调用。
来自Jetpack的ViewModel——Google用于实现MVVM的官方组件。ViewModel在屏幕旋转后依然存活:配置更改时,Activity被销毁并重新创建,而ViewModel保留在内存中。新的Activity实例通过ViewModelProvider获取相同的ViewModel。ViewModel没有引用Activity、Context或View——它是纯粹的,可以在没有Robolectric的情况下通过单元测试进行测试。
// 使用StateFlow的ViewModel——MVVM的现代实现
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _state = MutableStateFlow<UserState>(UserState.Loading)
val state: StateFlow<UserState> = _state.asStateFlow()
fun loadUser(userId: Int) {
viewModelScope.launch {
_state.value = UserState.Loading
repository.getUser(userId)
.onSuccess { user ->
_state.value = UserState.Success(user)
}
.onFailure { e ->
_state.value = UserState.Error(e.message ?: "Unknown")
}
}
}
}
sealed interface UserState {
data object Loading : UserState
data class Success(val user: User) : UserState
data class Error(val message: String) : UserState
}
// View(Activity)订阅state
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.state.onEach { state ->
when (state) {
is UserState.Loading -> /* 显示加载 */
is UserState.Success -> /* 显示数据 */
is UserState.Error -> /* 显示错误 */
}
}.launchIn(lifecycleScope)
viewModel.loadUser(42)
}
}
LiveData与StateFlow——LiveData(2017)是第一个反应式Jetpack组件,针对Activity生命周期进行了优化:在onStop时自动取消订阅。StateFlow(2021)是Kotlin Flow的实现,不绑定生命周期,但需要通过lifecycleScope手动取消订阅。StateFlow支持coroutines、concat、map以及其他LiveData中没有的Flow操作符。在IT Sectr,我们为所有新的ViewModel使用StateFlow——它更短、更强大,并且与协程集成得更好。
DataBinding和ViewBinding——DataBinding通过@{viewModel.user.name}直接在布局中将ViewModel与XML绑定,消除了Activity中的代码。ViewBinding生成类型安全的类用于访问View。Google推荐简单项目使用ViewBinding,复杂数据绑定的项目使用DataBinding。在Jetpack Compose中,不需要DataBinding——@Composable函数在State变化时自动重新渲染。
iOS中的MVVM通过Combine中的ObservableObject实现。ViewModel是一个继承ObservableObject的类,带有@Published字段。SwiftUI View通过@ObservedObject或@StateObject订阅ViewModel。当@Published属性发生变化时,SwiftUI自动重新渲染依赖于该属性的View。Apple于2019年在WWDC上与Combine一起发布了SwiftUI——从那时起,MVVM成为iOS官方推荐的模式。
import SwiftUI
import Combine
// ViewModel——带@Published字段的ObservableObject
final class UserViewModel: ObservableObject {
@Published private(set) var state: UserState = .loading
private let service: UserService
init(service: UserService) {
self.service = service
}
func loadUser(id: Int) {
state = .loading
service.fetchUser(id: id) { [weak self] result in
guard let self else { return }
switch result {
case .success(let user):
self.state = .success(user)
case .failure(let error):
self.state = .error(error.localizedDescription)
}
}
}
}
enum UserState {
case loading
case success(User)
case error(String)
}
// SwiftUI View——订阅ViewModel
struct UserView: View {
@StateObject private var viewModel: UserViewModel
var body: some View {
switch viewModel.state {
case .loading:
ProgressView()
case .success(let user):
VStack {
Text(user.name).font(.title)
Text(user.email).font(.body)
}
case .error(let message):
Text(message).foregroundColor(.red)
}
}
}
@StateObject与@ObservedObject——@StateObject创建ViewModel并管理其生命周期(在View的生命周期内只创建一次)。@ObservedObject——ViewModel在外部创建并传递给View。WWDC 2022推荐使用@StateObject进行创建,使用@ObservedObject在Views之间传递ViewModel。在iOS 17(2023)中出现了@Observable——一个自动订阅并消除@Published注解的宏。@Observable是Combine的演进,使iOS开发更接近Kotlin Flow的反应式特性。
UIKit + MVVM——对于基于UIKit(没有SwiftUI)的项目,MVVM通过Combine和@Published在UIViewController中使用sink()进行订阅来实现。ViewModel相同,View是订阅了@Published的UIViewController。Combine从iOS 13(2019)开始可用,并且内置于系统中——不需要额外的依赖。根据Apple Developer Survey(2025),45%的iOS项目即使在UIKit中也使用Combine,35%使用SwiftUI + Combine,20%使用RxSwift(遗留系统)。
MVVM在三个关键方面优于MVP:没有ViewContract接口、自动订阅管理以及屏幕旋转后的存活。在MVP中,每个屏幕需要ViewContract接口 + Presenter类 + 在onStart/onStop中进行订阅/取消订阅。在MVVM中,只创建ViewModel——在Activity中的订阅通过observe()完成,无需手动detach()。
| 标准 | MVP | MVVM |
|---|---|---|
| ViewContract接口 | 每个屏幕1个 | 不需要 |
| 订阅管理 | 手动attach/detach | 自动(生命周期感知) |
| 屏幕旋转 | Retain-fragment | ViewModel存活 |
| 测试 | Mock ViewContract | 无依赖的纯净类 |
| 反应式 | Presenter中的回调 | LiveData/StateFlow/Combine |
MVVM的缺点——调试反应式链的复杂性以及错误订阅导致内存泄漏的风险。LiveData解决了生命周期安全问题,StateFlow需要lifecycleScope,Combine需要带有AnyCancellable的sink。在MVP中,所有调用都是显式的(view.showUser),而在MVVM中,数据通过反应式流到达——跟踪需要在subscribe闭包中设置调试断点。在具有多个StateFlow的大型ViewModel中,如果View没有订阅特定的Flow,可能会错过UI更新。
何时MVP仍然更好——在最低Android版本低于API 21(Android 5)的项目中,Jetpack ViewModel在没有AndroidX的情况下不可用,以及在纯UIKit且没有Combine(iOS 12及以下)的项目中。对于整个代码库已经基于MVP的遗留项目,完全迁移到MVVM并不总是合理的——逐步将逻辑提取到服务中并维护MVP比在3个月内重写100个屏幕更便宜。
ViewModel通过单元测试进行测试,没有平台依赖——这是支持MVVM的主要论据。在Android中,ViewModel不包含Activity、Context或View——所有依赖(Repository、UseCase)通过构造函数传递,并被模拟对象替换。在iOS中,ObservableObject通过XCTest进行测试,无需启动应用程序,这提供了测试执行的稳定性和速度。
// 使用MockK的Android ViewModel单元测试
class UserViewModelTest {
private val repository = mockk<UserRepository>()
private val viewModel = UserViewModel(repository)
@Test
fun loadUser_success_updatesState() = runTest {
val user = User(1, "John", "john@test.com")
coEvery { repository.getUser(1) } returns Result.success(user)
viewModel.loadUser(1)
assertEquals(UserState.Success(user), viewModel.state.value)
}
@Test
fun loadUser_error_updatesErrorState() = runTest {
val error = RuntimeException("Network error")
coEvery { repository.getUser(1) } returns Result.failure(error)
viewModel.loadUser(1)
val state = viewModel.state.value
assertTrue(state is UserState.Error)
assertEquals("Network error", (state as UserState.Error).message)
}
}
iOS ViewModel以类似方式进行测试:我们注入模拟的UserService,调用loadUser,通过XCTestExpectation检查状态。Combine Publisher通过XCTestCase使用wait(for: expectations, timeout: 1.0)进行测试。UserState结构——带有associated values的枚举——允许在操作后检查屏幕的确切状态。
代码覆盖率在IT Sectr的MVVM项目中,ViewModel和Repository的覆盖率为75-90%。ViewModel由单元测试覆盖,Repository由集成测试使用测试数据库覆盖。SwiftUI和Jetpack Compose中的View通过UI测试(XCUITest、Compose Test)覆盖关键场景。其余UI通过截图测试(Snapshot Testing)进行检查——这比UI测试更快,并且对显示正确性提供95%的信心。
常见问题
在MVVM中,ViewModel没有对View的引用——数据通过反应式机制(LiveData、StateFlow、@Published)传递。在MVP中,Presenter通过ViewContract接口直接调用View的方法。MVVM消除了ViewContract和手动的attach/detach,但需要理解反应式流。ViewModel在Android中屏幕旋转后仍然存活,Presenter需要retain-fragment。
最小集合:lifecycle-viewmodel-ktx(ViewModel)、lifecycle-livedata-ktx或kotlinx-coroutines-core(StateFlow)。依赖注入——Hilt或Koin。异步操作——Kotlin Coroutines。复杂数据绑定——DataBinding。在Jetpack Compose(Google从2022年推荐)中,compose-runtime和lifecycle-viewmodel-compose就足够了。
SwiftUI(2019)是为反应式架构设计的:@State和@Published在数据变化时自动重新渲染View。MVVM是SwiftUI的自然匹配:View是@ViewBuilder,ViewModel是ObservableObject。Apple不强制MVVM作为唯一模式,但自2019年以来所有培训材料都使用ViewModel + SwiftUI。对于UIKit,Apple推荐MVC或Coordinator。
Android:viewModelScope在清理ViewModel时自动取消协程。iOS:Combine中的AnyCancellable在持有它的对象被释放时自动取消订阅。SwiftUI @StateObject自动管理生命周期。主要规则:不要在ViewModel中存储对View/Context的引用,在清理时取消长时间运行的操作,在闭包中使用weak self。
StateFlow是现代选择。LiveData更简单且生命周期安全,但StateFlow更强大:与协程一起工作,支持flatMap、combine、filter,不需要@Nullable注解。唯一LiveData更受青睐的场景是与Java代码一起工作,因为StateFlow(Kotlin Flow-API)不可用。Google推荐在Kotlin新项目中使用StateFlow。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。