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) — архитектурен модел, описан от John Gossman през 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 срещу StateFlow — LiveData (2017) — първият реактивен компонент на Jetpack, оптимизиран за жизнения цикъл на Activity: автоматично отписване при onStop. StateFlow (2021) — реализация на Kotlin Flow, независима от жизнения цикъл, но изискваща ръчно отписване чрез 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 срещу @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 (legacy).
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 | Чист клас без зависимости |
| Реактивност | Callback-и в Presenter | LiveData/StateFlow/Combine |
Недостатъци на MVVM — сложност при дебъгване на реактивни вериги и риск от изтичане на памет при неправилен абонамент. LiveData решава проблема с безопасността на жизнения цикъл, 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 и по-ниски). За legacy проекти, където цялата кодова база вече е на 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 е по-прост и безопасен за жизнения цикъл, но StateFlow е по-мощен: работи с корутини, поддържа flatMap, combine, filter, не изисква анотация @Nullable. Единственият сценарий, където LiveData е за предпочитане — работа с Java код, където StateFlow (Kotlin Flow-API) не е достъпен. Google препоръчва StateFlow за нови проекти на Kotlin.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също