MVVM (Model-View-ViewModel) — un pattern arhitectural în care ViewModel înlocuiește Presenter și utilizează mecanisme reactive pentru comunicarea cu View: ObservableObject în SwiftUI, LiveData/StateFlow în Android. ViewModel nu are referință la View — datele sunt transmise prin abonare, ceea ce elimină necesitatea interfețelor ViewContract și face testarea și mai simplă. Apple recomandă MVVM cu SwiftUI din 2019, Google — MVVM cu Jetpack ca arhitectură oficială Android. Mai multe detalii în Android Architecture Guide.
Esențial
MVVM (Model-View-ViewModel) — un pattern arhitectural descris de John Gossman în 2005 pentru Windows Presentation Foundation (WPF) de la Microsoft. ViewModel — componenta centrală care conține starea ecranului și logica de business, dar nu are referință la View. Datele sunt transmise prin mecanisme de legare reactivă: View se abonează la modificările ViewModel și se redesenează automat când datele se schimbă.
Diferența cheie între MVVM și MVP — absența ViewContract. În MVP, Presenter apelează metodele view.showUser(data), adică Presenter „împinge” activ datele în View. În MVVM, View însăși „trage” datele din ViewModel prin abonare: ViewModel nu știe dacă are un abonat. Aceasta elimină problema View detașat — dacă Activity este distrus la rotire, ViewModel continuă lucrul, iar noul Activity pur și simplu se abonează la datele actuale. La IT Sectr folosim MVVM în toate proiectele noi din 2020 — codul a devenit mai predictibil, testele mai stabile.
| Componentă | Responsabilitate | Platformă |
|---|---|---|
| Model | Date, logică de business, repository-uri | Android/iOS |
| View | Afișare, abonare la ViewModel | Activity/Composable, UIView/SwiftUI View |
| ViewModel | Starea ecranului, logică, navigare | ViewModel (Jetpack), ObservableObject |
Legarea reactivă — fundamentul MVVM. În Android, LiveData (parte din Jetpack) este un depozit de date observabil. Activity se abonează prin observe(): viewModel.user.observe(this) { user -> binding.name.text = user.name }. La modificarea user, toți abonații primesc automat noua valoare. În iOS, SwiftUI utilizează proprietăți @Published în ViewModel — modificările redesenează automat View. Aceasta elimină apelurile manuale showUser/hideLoading, care sunt necesare în MVP.
ViewModel din Jetpack — componenta oficială de la Google pentru implementarea MVVM. ViewModel supraviețuiește rotației ecranului: la modificarea configurației, Activity este distrus și recreat, iar ViewModel rămâne în memorie. Noua instanță Activity primește același ViewModel prin ViewModelProvider. ViewModel nu are referințe la Activity, Context sau View — este pur și se testează cu teste unitare fără Robolectric.
// ViewModel cu StateFlow — implementarea modernă 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) se abonează la 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 -> /* afișează încărcarea */
is UserState.Success -> /* afișează datele */
is UserState.Error -> /* afișează eroarea */
}
}.launchIn(lifecycleScope)
viewModel.loadUser(42)
}
}
LiveData vs StateFlow — LiveData (2017) — prima componentă reactivă Jetpack, optimizată pentru ciclul de viață Activity: dezabonare automată la onStop. StateFlow (2021) — implementare Kotlin Flow, nelegată de ciclul de viață, dar care necesită dezabonare manuală prin lifecycleScope. StateFlow suportă coroutines, concat, map și alți operatori Flow, care nu există în LiveData. La IT Sectr folosim StateFlow pentru toate ViewModel-urile noi — este mai scurt, mai puternic și se integrează mai bine cu corutinele.
DataBinding și ViewBinding — DataBinding leagă ViewModel de XML prin @{viewModel.user.name} direct în layout, eliminând codul din Activity. ViewBinding generează o clasă tip-securizată pentru accesul la View. Google recomandă ViewBinding pentru proiecte simple și DataBinding pentru proiecte cu legare complexă a datelor. În Jetpack Compose, DataBinding nu este necesar — funcțiile @Composable se redesenează automat la modificarea State.
MVVM în iOS se implementează prin ObservableObject din Combine. ViewModel — o clasă care moștenește ObservableObject, cu câmpuri @Published. SwiftUI View se abonează la ViewModel prin @ObservedObject sau @StateObject. La modificarea unei proprietăți @Published, SwiftUI redesenează automat View-ul care depinde de această proprietate. Apple a prezentat SwiftUI în 2019 la WWDC împreună cu Combine — din acel moment MVVM a devenit pattern-ul oficial recomandat pentru iOS.
import SwiftUI
import Combine
// ViewModel — ObservableObject cu câmpuri @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 — se abonează la 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 creează ViewModel și gestionează ciclul său de viață (o dată pe durata vieții View). @ObservedObject — ViewModel este creat extern și transmis în View. WWDC 2022 recomandă @StateObject pentru creare și @ObservedObject pentru transmiterea ViewModel între Views. În iOS 17 (2023) a apărut @Observable — un macro care automatizează abonarea și elimină adnotările @Published. @Observable — evoluția Combine, care apropie dezvoltarea iOS de reactivitatea Kotlin Flow.
UIKit + MVVM — pentru proiecte pe UIKit (fără SwiftUI), MVVM se implementează prin Combine și @Published cu abonare în UIViewController prin sink(). ViewModel este același, View — UIViewController cu abonări la @Published. Combine este disponibil din iOS 13 (2019) și este încorporat în sistem — nu necesită dependențe suplimentare. Conform Apple Developer Survey (2025), 45% din proiectele iOS folosesc Combine chiar și cu UIKit, 35% folosesc SwiftUI + Combine, 20% — RxSwift (legacy).
MVVM câștigă față de MVP în trei aspecte cheie: absența interfețelor ViewContract, gestionarea automată a abonărilor și supraviețuirea rotației ecranului. În MVP, fiecare ecran necesită o interfață ViewContract + o clasă Presenter + abonare/dezabonare în onStart/onStop. În MVVM se creează doar ViewModel — abonarea în Activity se face prin observe() fără detach manual.
| Criteriu | MVP | MVVM |
|---|---|---|
| Interfețe ViewContract | 1 per ecran | Nu sunt necesare |
| Gestionarea abonărilor | Manual attach/detach | Automată (lifecycle-aware) |
| Rotația ecranului | Retain-fragment | ViewModel supraviețuiește |
| Testare | Mock ViewContract | Clasă pură fără dependențe |
| Reactivitate | Callback-uri în Presenter | LiveData/StateFlow/Combine |
Dezavantajele MVVM — complexitatea depanării lanțurilor reactive și riscul de scurgere de memorie la abonarea incorectă. LiveData rezolvă problema securității ciclului de viață, StateFlow necesită lifecycleScope, Combine — sink cu AnyCancellable. În MVP, toate apelurile sunt explicite (view.showUser), în MVVM datele vin printr-un flux reactiv — urmărirea necesită breakpoint-uri de debug în closure-ul subscribe. În ViewModel-uri mari cu multiple StateFlow, se poate rata actualizarea UI dacă View nu este abonat la un anumit Flow.
Când MVP este încă mai bun — în proiecte cu versiunea minimă Android sub API 21 (Android 5), unde Jetpack ViewModel nu este disponibil fără AndroidX, și în proiecte pe UIKit pur fără Combine (iOS 12 și mai jos). Pentru proiecte legacy, unde întreaga bază de cod este deja pe MVP, tranziția completă la MVVM nu este întotdeauna justificată — este mai ieftin să menții MVP cu mutarea treptată a logicii în servicii, decât să rescrii 100 de ecrane în 3 luni.
ViewModel se testează cu teste unitare fără dependențe de platformă — acesta este principalul argument în favoarea MVVM. Pe Android, ViewModel nu conține Activity, Context sau View — toate dependențele (Repository, UseCase) sunt transmise prin constructor și înlocuite cu obiecte mock. Pe iOS, ObservableObject se testează prin XCTest fără a porni aplicația, ceea ce oferă stabilitate și viteză de execuție a testelor.
// Test unitar Android ViewModel cu 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 se testează similar: injectăm un mock UserService, apelăm loadUser, verificăm starea prin XCTestExpectation. Combine Publisher se testează prin XCTestCase cu wait(for: expectations, timeout: 1.0). Structura UserState — enum cu associated values — permite verificarea stării exacte a ecranului după operație.
Acoperirea codului în proiectele IT Sectr pe MVVM este de 75–90% pentru ViewModel și Repository. ViewModel este acoperit cu teste unitare, Repository — cu teste de integrare cu o bază de date de test. View în SwiftUI și Jetpack Compose se testează cu teste UI (XCUITest, Compose Test) pentru scenarii critice. Restul UI este verificat prin teste de captură ecran (Snapshot Testing) — este mai rapid decât testele UI și oferă 95% încredere în corectitudinea afișării.
Întrebări frecvente
În MVVM, ViewModel nu are referință la View — datele sunt transmise prin mecanisme reactive (LiveData, StateFlow, @Published). În MVP, Presenter apelează direct metodele View prin interfața ViewContract. MVVM elimină ViewContract și attach/detach manual, dar necesită înțelegerea fluxurilor reactive. ViewModel supraviețuiește rotației ecranului pe Android, Presenter necesită retain-fragment.
Setul minim: lifecycle-viewmodel-ktx (ViewModel), lifecycle-livedata-ktx sau kotlinx-coroutines-core (StateFlow). Pentru injectare — Hilt sau Koin. Pentru operații asincrone — Kotlin Coroutines. Pentru legare complexă a datelor — DataBinding. În Jetpack Compose (recomandat de Google din 2022) sunt suficiente compose-runtime și lifecycle-viewmodel-compose.
SwiftUI (2019) a fost proiectat pentru arhitectură reactivă: @State și @Published redesenează automat View la modificarea datelor. MVVM — potrivirea naturală pentru SwiftUI: View — @ViewBuilder, ViewModel — ObservableObject. Apple nu impune MVVM ca singur pattern, dar toate materialele educaționale din 2019 folosesc ViewModel + SwiftUI. Pentru UIKit, Apple recomandă MVC sau Coordinator.
Android: viewModelScope anulează automat corutinele la curățarea ViewModel. iOS: AnyCancellable din Combine se dezabonează automat la eliberarea obiectului care îl stochează. SwiftUI @StateObject gestionează ciclul de viață automat. Reguli principale: nu stocați referințe la View/Context în ViewModel, anulați operațiile de lungă durată la curățare, folosiți weak self în closure-uri.
StateFlow — alegerea modernă. LiveData este mai simplu și sigur pentru ciclul de viață, dar StateFlow este mai puternic: funcționează cu corutine, suportă flatMap, combine, filter, nu necesită adnotarea @Nullable. Singurul scenariu unde LiveData este preferabil — lucrul cu cod Java, unde StateFlow (Kotlin Flow-API) nu este disponibil. Google recomandă StateFlow pentru proiecte noi în Kotlin.
Concluzii
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și