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 не знає, чи є в неї підписник. Це усуває проблему від'єднаного 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, перевіряємо стан через 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, скасовувати довготривалі операції при очищенні, використовувати weak self в замиканнях.
StateFlow — сучасний вибір. LiveData простіший і lifecycle-безпечний, але StateFlow потужніший: працює з корутинами, підтримує flatMap, combine, filter, не потребує анотації @Nullable. Єдиний сценарій, де LiveData кращий — робота з Java-кодом, де StateFlow (Kotlin Flow-API) недоступний. Google рекомендує StateFlow для нових проєктів на Kotlin.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також