MVVM (Model-View-ViewModel) — архитектурный паттерн, в котором ViewModel заменяет Presenter и использует реактивные механизмы для связи с View: ObservableObject в SwiftUI, LiveData/StateFlow в Android. ViewModel не имеет ссылки на View — данные передаются через подписку, что устраняет необходимость в интерфейсах ViewContract и делает тестирование ещё проще. Apple рекомендует MVVM со SwiftUI с 2019 года, Google — MVVM с Jetpack как официальную архитектуру Android. Подробнее — в Android Architecture Guide.
Главное
MVVM (Model-View-ViewModel) — архитектурный паттерн, описанный Джоном Госсманом в 2005 году для Windows Presentation Foundation (WPF) от Microsoft. ViewModel — центральный компонент, который содержит состояние экрана и бизнес-логику, но не имеет ссылки на View. Данные передаются через механизмы реактивного связывания: View подписывается на изменения ViewModel и автоматически перерисовывается, когда данные меняются.
Ключевое отличие MVVM от MVP — отсутствие ViewContract. В MVP Presenter вызывает методы view.showUser(data), то есть Presenter активно «толкает» данные во View. В MVVM View сама «тянет» данные из ViewModel через подписку: ViewModel не знает, есть ли у неё подписчик. Это устраняет проблему detached View — если Activity уничтожена при повороте, ViewModel продолжает работу, а новая Activity просто подписывается на актуальные данные. В IT Sectr мы используем MVVM во всех новых проектах с 2020 года — код стал предсказуемее, тесты стабильнее.
| Компонент | Ответственность | Платформа |
|---|---|---|
| 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 использует @Published-свойства в ViewModel — изменения автоматически перерисовывают View. Это устраняет ручные вызовы showUser/hideLoading, которые требуются в MVP.
ViewModel из Jetpack — официальный компонент от Google для реализации MVVM. ViewModel переживает поворот экрана: при изменении конфигурации Activity уничтожается и пересоздаётся, а ViewModel остаётся в памяти. Новый экземпляр Activity получает ту же ViewModel через ViewModelProvider. ViewModel не имеет ссылок на Activity, Context или View — она чистая и тестируется unit-тестами без Robolectric.
// ViewModel с StateFlow — современная реализация 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 vs StateFlow — LiveData (2017) — первый реактивный компонент Jetpack, оптимизированный для Activity lifecycle: автоматическая отписка при onStop. StateFlow (2021) — Kotlin Flow-реализация, не привязанная к lifecycle, но требующей ручной отписки через lifecycleScope. StateFlow поддерживает coroutines, concat, map и другие операторы Flow, чего нет в LiveData. В IT Sectr мы используем StateFlow для всех новых ViewModel — он короче, мощнее и лучше интегрируется с корутинами.
DataBinding и ViewBinding — DataBinding связывает ViewModel с XML через @{viewModel.user.name} прямо в разметке, устраняя код в Activity. ViewBinding генерирует типобезопасный класс для доступа к View. Google рекомендует ViewBinding для простых проектов и DataBinding для проектов со сложной привязкой данных. В Jetpack Compose DataBinding не нужен — @Composable функции автоматически перерисовываются при изменении State.
MVVM в iOS реализуется через ObservableObject из Combine. ViewModel — класс, наследующий ObservableObject, с полями @Published. SwiftUI View подписывается на ViewModel через @ObservedObject или @StateObject. При изменении @Published-свойства SwiftUI автоматически перерисовывает View, которая зависит от этого свойства. Apple представила SwiftUI в 2019 году на WWDC вместе с Combine — с этого момента MVVM стал официально рекомендуемым паттерном для iOS.
import SwiftUI
import Combine
// ViewModel — ObservableObject с @Published полями
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 vs @ObservedObject — @StateObject создаёт ViewModel и управляет её жизненным циклом (один раз за время жизни View). @ObservedObject — ViewModel создаётся снаружи и передаётся в View. WWDC 2022 рекомендует @StateObject для создания и @ObservedObject для передачи ViewModel между Views. В iOS 17 (2023) появился @Observable — макрос, автоматизирующий подписку и устраняющий @Published-аннотации. @Observable — эволюция Combine, приближающая iOS-разработку к реактивности Kotlin Flow.
UIKit + MVVM — для проектов на UIKit (без SwiftUI) MVVM реализуется через Combine и @Published с подпиской в UIViewController через sink(). ViewModel та же, View — UIViewController с подписками на @Published. Combine доступен с iOS 13 (2019) и встроен в систему — не требует дополнительных зависимостей. По данным Apple Developer Survey (2025), 45% iOS-проектов используют Combine даже при UIKit, 35% используют SwiftUI + Combine, 20% — RxSwift (легаси).
MVVM выигрывает у MVP в трёх ключевых аспектах: отсутствие интерфейсов ViewContract, автоматическое управление подписками и переживание поворота экрана. В MVP каждый экран требует интерфейс ViewContract + класс Presenter + подписку/отписку в onStart/onStop. В MVVM создаётся только ViewModel — подписка в Activity выполняется через observe() без ручного detach().
| Критерий | MVP | MVVM |
|---|---|---|
| Интерфейсы ViewContract | 1 на экран | Не нужны |
| Управление подписками | Ручное attach/detach | Автоматическое (lifecycle-aware) |
| Поворот экрана | Retain-фрагмент | ViewModel переживает поворот |
| Тестирование | Mock ViewContract | Чистый класс без зависимостей |
| Реактивность | Колбэки в Presenter | LiveData/StateFlow/Combine |
Недостатки MVVM — сложность отладки реактивных цепочек и риск утечки памяти при неправильной подписке. LiveData решает проблему lifecycle-безопасности, StateFlow требует lifecycleScope, Combine — sink с AnyCancellable. В MVP все вызовы явные (view.showUser), в MVVM данные приходят через реактивный поток — трассировка требует debug-брейкпоинтов в subscribe-замыкании. В больших ViewModel с множеством StateFlow можно пропустить обновление UI, если View не подписана на конкретный Flow.
Когда MVP всё ещё лучше — в проектах с минимальной версией Android ниже API 21 (Android 5), где Jetpack ViewModel недоступен без AndroidX, и в проектах на чистом UIKit без Combine (iOS 12 и ниже). Для легаси-проектов, где вся кодовая база уже на MVP, полный переход на MVVM не всегда оправдан — дешевле поддерживать MVP с постепенным выносом логики в сервисы, чем переписывать 100 экранов за 3 месяца.
ViewModel тестируется unit-тестами без платформенных зависимостей — это главный аргумент в пользу MVVM. На Android ViewModel не содержит Activity, Context или View — все зависимости (Repository, UseCase) передаются через конструктор и заменяются mock-объектами. На iOS ObservableObject тестируется через XCTest без запуска приложения, что даёт стабильность и скорость выполнения тестов.
// Unit-тест Android ViewModel с 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 тестируется аналогично: инжектим mock UserService, вызываем loadUser, проверяем state через XCTestExpectation. Combine Publisher тестируется через XCTestCase с wait(for: expectations, timeout: 1.0). Структура UserState — enum с associated values — позволяет проверить точное состояние экрана после операции.
Покрытие кода в проектах IT Sectr на MVVM составляет 75–90% для ViewModel и Repository. ViewModel покрывается unit-тестами, Repository — интеграционными тестами с тестовой БД. View в SwiftUI и Jetpack Compose тестируется UI-тестами (XCUITest, Compose Test) для критических сценариев. Остальной UI проверяется скриншотными тестами (Snapshot Testing) — это быстрее, чем UI-тесты, и даёт 95% уверенности в корректности отображения.
Часто задаваемые вопросы
В MVVM ViewModel не имеет ссылки на View — данные передаются через реактивные механизмы (LiveData, StateFlow, @Published). В MVP Presenter напрямую вызывает методы View через интерфейс ViewContract. MVVM устраняет ViewContract и ручное attach/detach, но требует понимания реактивных потоков. ViewModel переживает поворот экрана на Android, Presenter требует retain-фрагмент.
Минимальный набор: 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: AnyCancellable из Combine автоматически отписывается при освобождении хранящего его объекта. SwiftUI @StateObject управляет жизненным циклом автоматически. Основные правила: не хранить ссылки на View/Context в ViewModel, отменять long-running операции при очистке, использовать weak self в замыканиях.
StateFlow — современный выбор. LiveData проще и lifecycle-безопасен, но StateFlow мощнее: работает с корутинами, поддерживает flatMap, combine, filter, не требует аннотации @Nullable. Единственный сценарий, где LiveData предпочтительнее — работа с Java-кодом, где StateFlow (Kotlin Flow-API) недоступен. Google рекомендует StateFlow для новых проектов на Kotlin.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также