MVVM: ce este, pattern-ul Model-View-ViewModel în dezvoltarea mobilă

Autor: IT Sectr Publicat: 2026-02-16 Timp de citire: 9 min

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 (date), View (interfață), ViewModel (stare și logică fără referință la View)
  • Reactive binding — LiveData, StateFlow, ObservableObject actualizează automat UI la modificarea datelor
  • ViewModel — supraviețuiește rotației ecranului și nu depinde de Android SDK/UIKit, se testează cu teste unitare
  • Android Jetpack — ViewModel, LiveData, DataBinding — stiva oficială de la Google pentru MVVM
  • SwiftUI + Combine — implementarea nativă MVVM în iOS cu @Published și @ObservedObject

Ce este MVVM: esența pattern-ului Model-View-ViewModel

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ăResponsabilitatePlatformă
ModelDate, logică de business, repository-uriAndroid/iOS
ViewAfișare, abonare la ViewModelActivity/Composable, UIView/SwiftUI View
ViewModelStarea ecranului, logică, navigareViewModel (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.

MVVM în Android: ViewModel, LiveData și StateFlow

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.

kotlin
// 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: ObservableObject și SwiftUI

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.

swift
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).

Compararea MVVM cu MVP: avantaje și dezavantaje

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.

CriteriuMVPMVVM
Interfețe ViewContract1 per ecranNu sunt necesare
Gestionarea abonărilorManual attach/detachAutomată (lifecycle-aware)
Rotația ecranuluiRetain-fragmentViewModel supraviețuiește
TestareMock ViewContractClasă pură fără dependențe
ReactivitateCallback-uri în PresenterLiveData/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.

Testarea ViewModel pe Android și iOS

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.

kotlin
// 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

Care este diferența principală între MVVM și MVP?

Î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.

Ce biblioteci sunt necesare pentru MVVM pe Android?

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.

De ce recomandă Apple MVVM pentru iOS?

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.

Cum să evităm scurgerile de memorie în ViewModel?

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.

Ce să alegem: LiveData sau StateFlow pentru Android?

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

  • MVVM (Model-View-ViewModel) — pattern reactiv, în care ViewModel nu are referință la View
  • ViewModel — supraviețuiește rotației ecranului, se testează cu teste unitare, independent de UI
  • Android — ViewModel + StateFlow + Kotlin Coroutines — stiva modernă de la Google
  • iOS — ObservableObject + @Published + SwiftUI — implementarea nativă MVVM
  • MVVM vs MVP — MVVM elimină ViewContract și attach/detach manual
  • Testare — ViewModel acoperit cu teste unitare fără dependențe de platformă
  • Recomandare — MVVM pentru proiecte noi; MVP pentru suport legacy

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.

Discutați proiectul

Citiți și