MVVM (Model-View-ViewModel) is an architectural pattern in which ViewModel replaces Presenter and uses reactive mechanisms to communicate with View: ObservableObject in SwiftUI, LiveData/StateFlow in Android. ViewModel has no reference to View — data is passed through subscription, which eliminates the need for ViewContract interfaces and makes testing even simpler. Apple has recommended MVVM with SwiftUI since 2019, Google recommends MVVM with Jetpack as the official Android architecture. Learn more in Android Architecture Guide.
Key Takeaways
MVVM (Model-View-ViewModel) is an architectural pattern described by John Gossman in 2005 for Windows Presentation Foundation (WPF) from Microsoft. ViewModel is the central component that contains screen state and business logic but has no reference to View. Data is passed through reactive binding mechanisms: View subscribes to ViewModel changes and automatically re-renders when data changes.
Key difference between MVVM and MVP — absence of ViewContract. In MVP, Presenter calls view.showUser(data) methods, meaning Presenter actively "pushes" data to View. In MVVM, View itself "pulls" data from ViewModel through subscription: ViewModel does not know whether it has a subscriber. This eliminates the detached View problem — if Activity is destroyed on rotation, ViewModel continues working, and the new Activity simply subscribes to the current data. At IT Sectr, we have been using MVVM in all new projects since 2020 — the code has become more predictable, tests more stable.
| Component | Responsibility | Platform |
|---|---|---|
| Model | Data, business logic, repositories | Android/iOS |
| View | Display, subscription to ViewModel | Activity/Composable, UIView/SwiftUI View |
| ViewModel | Screen state, logic, navigation | ViewModel (Jetpack), ObservableObject |
Reactive binding — the foundation of MVVM. In Android, LiveData (part of Jetpack) is an observable data holder. Activity subscribes via observe(): viewModel.user.observe(this) { user -> binding.name.text = user.name }. When user changes, all subscribers receive the new value automatically. On iOS, SwiftUI uses @Published properties in ViewModel — changes automatically re-render View. This eliminates manual showUser/hideLoading calls required in MVP.
ViewModel from Jetpack — the official component from Google for implementing MVVM. ViewModel survives screen rotation: when configuration changes, Activity is destroyed and recreated, while ViewModel remains in memory. The new Activity instance gets the same ViewModel through ViewModelProvider. ViewModel has no references to Activity, Context or View — it is clean and testable with unit tests without Robolectric.
// ViewModel with StateFlow — modern MVVM implementation
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) subscribes to 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 -> /* show loading */
is UserState.Success -> /* display data */
is UserState.Error -> /* show error */
}
}.launchIn(lifecycleScope)
viewModel.loadUser(42)
}
}
LiveData vs StateFlow — LiveData (2017) — the first reactive component of Jetpack, optimized for Activity lifecycle: automatic unsubscription on onStop. StateFlow (2021) — Kotlin Flow implementation, not tied to lifecycle, but requiring manual unsubscription via lifecycleScope. StateFlow supports coroutines, concat, map and other Flow operators, which LiveData lacks. At IT Sectr, we use StateFlow for all new ViewModels — it is shorter, more powerful, and better integrates with coroutines.
DataBinding and ViewBinding — DataBinding binds ViewModel to XML via @{viewModel.user.name} directly in the layout, eliminating code in Activity. ViewBinding generates a type-safe class for accessing views. Google recommends ViewBinding for simple projects and DataBinding for projects with complex data binding. In Jetpack Compose, DataBinding is not needed — @Composable functions automatically re-render when State changes.
MVVM in iOS is implemented through ObservableObject from Combine. ViewModel is a class inheriting ObservableObject, with @Published properties. SwiftUI View subscribes to ViewModel via @ObservedObject or @StateObject. When a @Published property changes, SwiftUI automatically re-renders the View that depends on this property. Apple introduced SwiftUI in 2019 at WWDC along with Combine — since then MVVM has become the officially recommended pattern for iOS.
import SwiftUI
import Combine
// ViewModel — ObservableObject with @Published properties
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 — subscribes to 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 vs @ObservedObject — @StateObject creates ViewModel and manages its lifecycle (once per View lifetime). @ObservedObject — ViewModel is created externally and passed to View. WWDC 2022 recommends @StateObject for creation and @ObservedObject for passing ViewModel between Views. In iOS 17 (2023), @Observable macro appeared — automating subscription and eliminating @Published annotations. @Observable is the evolution of Combine, bringing iOS development closer to Kotlin Flow reactivity.
UIKit + MVVM — for UIKit projects (without SwiftUI), MVVM is implemented through Combine and @Published with subscription in UIViewController via sink(). ViewModel is the same, View is UIViewController with subscriptions to @Published. Combine has been available since iOS 13 (2019) and is built into the system — no additional dependencies required. According to Apple Developer Survey (2025), 45% of iOS projects use Combine even with UIKit, 35% use SwiftUI + Combine, 20% use RxSwift (legacy).
MVVM wins over MVP in three key aspects: absence of ViewContract interfaces, automatic subscription management, and surviving screen rotation. In MVP, each screen requires a ViewContract interface + Presenter class + subscription/unsubscription in onStart/onStop. In MVVM, only ViewModel is created — subscription in Activity is done via observe() without manual detach().
| Criterion | MVP | MVVM |
|---|---|---|
| ViewContract Interfaces | 1 per screen | Not needed |
| Subscription Management | Manual attach/detach | Automatic (lifecycle-aware) |
| Screen Rotation | Retain-fragment | ViewModel survives rotation |
| Testing | Mock ViewContract | Clean class without dependencies |
| Reactivity | Callbacks in Presenter | LiveData/StateFlow/Combine |
Disadvantages of MVVM — complexity of debugging reactive chains and risk of memory leaks with improper subscription. LiveData solves lifecycle safety, StateFlow requires lifecycleScope, Combine requires sink with AnyCancellable. In MVP, all calls are explicit (view.showUser), in MVVM data comes through a reactive stream — tracing requires debug breakpoints in subscribe closures. In large ViewModels with multiple StateFlows, you might miss UI updates if View is not subscribed to a specific Flow.
When MVP is still better — in projects with a minimum Android version below API 21 (Android 5), where Jetpack ViewModel is unavailable without AndroidX, and in projects using pure UIKit without Combine (iOS 12 and below). For legacy projects where the entire codebase is already on MVP, a full transition to MVVM is not always justified — it is cheaper to maintain MVP with gradual extraction of logic into services than to rewrite 100 screens in 3 months.
ViewModel is tested with unit tests without platform dependencies — this is the main argument in favor of MVVM. On Android, ViewModel does not contain Activity, Context or View — all dependencies (Repository, UseCase) are passed through the constructor and replaced with mock objects. On iOS, ObservableObject is tested through XCTest without launching the application, providing stability and test execution speed.
// Android ViewModel unit test with MockK
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 is tested similarly: inject mock UserService, call loadUser, check state through XCTestExpectation. Combine Publisher is tested through XCTestCase with wait(for: expectations, timeout: 1.0). The UserState structure — enum with associated values — allows checking the exact screen state after an operation.
Code coverage in IT Sectr projects using MVVM is 75–90% for ViewModel and Repository. ViewModel is covered by unit tests, Repository by integration tests with a test database. View in SwiftUI and Jetpack Compose is tested with UI tests (XCUITest, Compose Test) for critical scenarios. The rest of the UI is checked with screenshot tests (Snapshot Testing) — this is faster than UI tests and provides 95% confidence in display correctness.
Frequently Asked Questions
In MVVM, ViewModel has no reference to View — data is passed through reactive mechanisms (LiveData, StateFlow, @Published). In MVP, Presenter directly calls View methods through the ViewContract interface. MVVM eliminates ViewContract and manual attach/detach, but requires understanding of reactive streams. ViewModel survives screen rotation on Android, Presenter requires a retain-fragment.
Minimum set: lifecycle-viewmodel-ktx (ViewModel), lifecycle-livedata-ktx or kotlinx-coroutines-core (StateFlow). For injection — Hilt or Koin. For asynchronous operations — Kotlin Coroutines. For complex data binding — DataBinding. In Jetpack Compose (recommended by Google since 2022), compose-runtime and lifecycle-viewmodel-compose are sufficient.
SwiftUI (2019) is designed for reactive architecture: @State and @Published automatically re-render View when data changes. MVVM is a natural fit for SwiftUI: View — @ViewBuilder, ViewModel — ObservableObject. Apple does not impose MVVM as the only pattern, but all training materials since 2019 use ViewModel + SwiftUI. For UIKit, Apple recommends MVC or Coordinator.
Android: viewModelScope automatically cancels coroutines when ViewModel is cleared. iOS: AnyCancellable from Combine automatically unsubscribes when the holding object is deallocated. SwiftUI @StateObject manages lifecycle automatically. Main rules: do not store references to View/Context in ViewModel, cancel long-running operations on cleanup, use weak self in closures.
StateFlow is the modern choice. LiveData is simpler and lifecycle-safe, but StateFlow is more powerful: works with coroutines, supports flatMap, combine, filter, does not require @Nullable annotation. The only scenario where LiveData is preferable — working with Java code where StateFlow (Kotlin Flow API) is unavailable. Google recommends StateFlow for new Kotlin projects.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also