MVVM (Model-View-ViewModel) — wzorzec architektoniczny, w którym ViewModel zastępuje Presenter i wykorzystuje mechanizmy reaktywne do komunikacji z View: ObservableObject w SwiftUI, LiveData/StateFlow w Android. ViewModel nie ma referencji do View — dane są przesyłane przez subskrypcję, co eliminuje potrzebę interfejsów ViewContract i jeszcze bardziej upraszcza testowanie. Apple zaleca MVVM ze SwiftUI od 2019 roku, Google — MVVM z Jetpack jako oficjalną architekturę Android. Więcej w Android Architecture Guide.
Najważniejsze
MVVM (Model-View-ViewModel) — wzorzec architektoniczny opisany przez Johna Gossmana w 2005 roku dla Windows Presentation Foundation (WPF) od Microsoft. ViewModel — centralny komponent, który zawiera stan ekranu i logikę biznesową, ale nie ma referencji do View. Dane są przesyłane przez mechanizmy reaktywnego wiązania: View subskrybuje zmiany ViewModel i automatycznie przerysowuje się, gdy dane się zmieniają.
Kluczowa różnica MVVM od MVP — brak ViewContract. W MVP Presenter wywołuje metody view.showUser(data), czyli Presenter aktywnie „wypycha” dane do View. W MVVM View sama „ciągnie” dane z ViewModel przez subskrypcję: ViewModel nie wie, czy ma subskrybenta. To eliminuje problem odłączonego View — jeśli Activity jest zniszczone przy obrocie, ViewModel kontynuuje pracę, a nowe Activity po prostu subskrybuje aktualne dane. W IT Sectr używamy MVVM we wszystkich nowych projektach od 2020 roku — kod stał się bardziej przewidywalny, testy stabilniejsze.
| Komponent | Odpowiedzialność | Platforma |
|---|---|---|
| Model | Dane, logika biznesowa, repozytoria | Android/iOS |
| View | Wyświetlanie, subskrypcja ViewModel | Activity/Composable, UIView/SwiftUI View |
| ViewModel | Stan ekranu, logika, nawigacja | ViewModel (Jetpack), ObservableObject |
Reaktywne wiązanie — fundament MVVM. W Android LiveData (część Jetpack) jest obserwowalnym magazynem danych. Activity subskrybuje przez observe(): viewModel.user.observe(this) { user -> binding.name.text = user.name }. Przy zmianie user wszyscy subskrybenci otrzymują nową wartość automatycznie. W iOS SwiftUI używa @Published-właściwości w ViewModel — zmiany automatycznie przerysowują View. To eliminuje ręczne wywołania showUser/hideLoading, które są wymagane w MVP.
ViewModel z Jetpack — oficjalny komponent od Google do implementacji MVVM. ViewModel przetrzymuje obrót ekranu: przy zmianie konfiguracji Activity jest niszczone i odtwarzane, a ViewModel pozostaje w pamięci. Nowa instancja Activity otrzymuje ten sam ViewModel przez ViewModelProvider. ViewModel nie ma referencji do Activity, Context ani View — jest czysty i testowany testami jednostkowymi bez Robolectric.
// ViewModel z StateFlow — nowoczesna implementacja 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) subskrybuje stan
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 -> /* pokazać ładowanie */
is UserState.Success -> /* wyświetlić dane */
is UserState.Error -> /* pokazać błąd */
}
}.launchIn(lifecycleScope)
viewModel.loadUser(42)
}
}
LiveData vs StateFlow — LiveData (2017) — pierwszy reaktywny komponent Jetpack, zoptymalizowany dla cyklu życia Activity: automatyczna rezygnacja z subskrypcji przy onStop. StateFlow (2021) — implementacja Kotlin Flow, nieprzywiązana do cyklu życia, ale wymagająca ręcznej rezygnacji przez lifecycleScope. StateFlow obsługuje coroutines, concat, map i inne operatory Flow, których brak w LiveData. W IT Sectr używamy StateFlow dla wszystkich nowych ViewModel — jest krótszy, wydajniejszy i lepiej integruje się z korutynami.
DataBinding i ViewBinding — DataBinding wiąże ViewModel z XML przez @{viewModel.user.name} bezpośrednio w układzie, eliminując kod w Activity. ViewBinding generuje typowo bezpieczną klasę dostępu do View. Google zaleca ViewBinding dla prostych projektów i DataBinding dla projektów ze złożonym wiązaniem danych. W Jetpack Compose DataBinding nie jest potrzebny — funkcje @Composable automatycznie przerysowują się przy zmianie State.
MVVM w iOS jest implementowane przez ObservableObject z Combine. ViewModel — klasa dziedzicząca ObservableObject z polami @Published. SwiftUI View subskrybuje ViewModel przez @ObservedObject lub @StateObject. Przy zmianie właściwości @Published SwiftUI automatycznie przerysowuje View, która zależy od tej właściwości. Apple przedstawiło SwiftUI w 2019 roku na WWDC wraz z Combine — od tego momentu MVVM stał się oficjalnie zalecanym wzorcem dla iOS.
import SwiftUI
import Combine
// ViewModel — ObservableObject z @Published polami
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 — subskrybuje 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 tworzy ViewModel i zarządza jego cyklem życia (raz na czas życia View). @ObservedObject — ViewModel jest tworzony zewnętrznie i przekazywany do View. WWDC 2022 zaleca @StateObject do tworzenia i @ObservedObject do przekazywania ViewModel między Views. W iOS 17 (2023) pojawił się @Observable — makro automatyzujące subskrypcję i eliminujące adnotacje @Published. @Observable — ewolucja Combine, przybliżająca iOS-development do reaktywności Kotlin Flow.
UIKit + MVVM — dla projektów na UIKit (bez SwiftUI) MVVM jest implementowany przez Combine i @Published z subskrypcją w UIViewController przez sink(). ViewModel ten sam, View — UIViewController z subskrypcjami na @Published. Combine jest dostępny od iOS 13 (2019) i wbudowany w system — nie wymaga dodatkowych zależności. Według Apple Developer Survey (2025), 45% projektów iOS używa Combine nawet przy UIKit, 35% używa SwiftUI + Combine, 20% — RxSwift (legacy).
MVVM wygrywa z MVP w trzech kluczowych aspektach: brak interfejsów ViewContract, automatyczne zarządzanie subskrypcjami i przetrzymywanie obrotu ekranu. W MVP każdy ekran wymaga interfejsu ViewContract + klasy Presenter + subskrypcji/rezygnacji w onStart/onStop. W MVVM tworzona jest tylko ViewModel — subskrypcja w Activity wykonuje się przez observe() bez ręcznego detach().
| Kryterium | MVP | MVVM |
|---|---|---|
| Interfejsy ViewContract | 1 na ekran | Niepotrzebne |
| Zarządzanie subskrypcjami | Ręczne attach/detach | Automatyczne (lifecycle-aware) |
| Obrót ekranu | Retain-fragment | ViewModel przetrzymuje obrót |
| Testowanie | Mock ViewContract | Czysta klasa bez zależności |
| Reaktywność | Callbacki w Presenter | LiveData/StateFlow/Combine |
Wady MVVM — złożoność debugowania reaktywnych łańcuchów i ryzyko wycieku pamięci przy nieprawidłowej subskrypcji. LiveData rozwiązuje problem bezpieczeństwa cyklu życia, StateFlow wymaga lifecycleScope, Combine — sink z AnyCancellable. W MVP wszystkie wywołania są jawne (view.showUser), w MVVM dane przychodzą przez strumień reaktywny — śledzenie wymaga debug-breakpointów w domknięciu subscribe. W dużych ViewModel z wieloma StateFlow można przegapić aktualizację UI, jeśli View nie subskrybuje konkretnego Flow.
Kiedy MVP jest nadal lepsze — w projektach z minimalną wersją Android poniżej API 21 (Android 5), gdzie Jetpack ViewModel jest niedostępny bez AndroidX, oraz w projektach na czystym UIKit bez Combine (iOS 12 i niżej). Dla projektów legacy, gdzie cała baza kodu jest już na MVP, pełne przejście na MVVM nie zawsze jest uzasadnione — taniej jest utrzymywać MVP ze stopniowym przenoszeniem logiki do serwisów, niż przepisywać 100 ekranów w 3 miesiące.
ViewModel jest testowany testami jednostkowymi bez zależności platformowych — to główny argument na korzyść MVVM. Na Android ViewModel nie zawiera Activity, Context ani View — wszystkie zależności (Repository, UseCase) są przekazywane przez konstruktor i zastępowane obiektami mock. W iOS ObservableObject jest testowany przez XCTest bez uruchamiania aplikacji, co zapewnia stabilność i szybkość wykonywania testów.
// Unit-test Android ViewModel z 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 jest testowany analogicznie: wstrzykujemy mock UserService, wywołujemy loadUser, sprawdzamy stan przez XCTestExpectation. Combine Publisher jest testowany przez XCTestCase z wait(for: expectations, timeout: 1.0). Struktura UserState — enum z wartościami powiązanymi — pozwala sprawdzić dokładny stan ekranu po operacji.
Pokrycie kodu w projektach IT Sectr na MVVM wynosi 75–90% dla ViewModel i Repository. ViewModel jest pokrywany testami jednostkowymi, Repository — testami integracyjnymi z testową bazą danych. View w SwiftUI i Jetpack Compose jest testowane testami UI (XCUITest, Compose Test) dla krytycznych scenariuszy. Reszta UI jest sprawdzana testami zrzutów ekranu (Snapshot Testing) — to szybsze niż testy UI i daje 95% pewności co do poprawności wyświetlania.
Często zadawane pytania
W MVVM ViewModel nie ma referencji do View — dane są przesyłane przez mechanizmy reaktywne (LiveData, StateFlow, @Published). W MVP Presenter bezpośrednio wywołuje metody View przez interfejs ViewContract. MVVM eliminuje ViewContract i ręczne attach/detach, ale wymaga zrozumienia strumieni reaktywnych. ViewModel przetrzymuje obrót ekranu na Android, Presenter wymaga retain-fragment.
Minimalny zestaw: lifecycle-viewmodel-ktx (ViewModel), lifecycle-livedata-ktx lub kotlinx-coroutines-core (StateFlow). Do wstrzykiwania — Hilt lub Koin. Do operacji asynchronicznych — Kotlin Coroutines. Do złożonego wiązania danych — DataBinding. W Jetpack Compose (zalecanym przez Google od 2022) wystarczy compose-runtime i lifecycle-viewmodel-compose.
SwiftUI (2019) został zaprojektowany pod architekturę reaktywną: @State i @Published automatycznie przerysowują View przy zmianie danych. MVVM — naturalne dopasowanie do SwiftUI: View — @ViewBuilder, ViewModel — ObservableObject. Apple nie narzuca MVVM jako jedynego wzorca, ale wszystkie materiały szkoleniowe od 2019 roku używają ViewModel + SwiftUI. Dla UIKit Apple zaleca MVC lub Coordinator.
Android: viewModelScope automatycznie anuluje korutyny przy czyszczeniu ViewModel. iOS: AnyCancellable z Combine automatycznie rezygnuje z subskrypcji przy zwolnieniu przechowującego go obiektu. SwiftUI @StateObject zarządza cyklem życia automatycznie. Główne zasady: nie przechowywać referencji do View/Context w ViewModel, anulować długotrwałe operacje przy czyszczeniu, używać weak self w domknięciach.
StateFlow — nowoczesny wybór. LiveData jest prostsze i bezpieczne dla cyklu życia, ale StateFlow jest potężniejsze: działa z korutynami, obsługuje flatMap, combine, filter, nie wymaga adnotacji @Nullable. Jedyny scenariusz, gdzie LiveData jest preferowane — praca z kodem Java, gdzie StateFlow (Kotlin Flow-API) jest niedostępny. Google zaleca StateFlow dla nowych projektów w Kotlin.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również