MVVM: co to jest, wzorzec Model-View-ViewModel w programowaniu mobilnym

Autor: IT Sectr Opublikowano: 2026-02-16 Czas czytania: 9 min

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 (dane), View (interfejs), ViewModel (stan i logika bez referencji do View)
  • Reactive binding — LiveData, StateFlow, ObservableObject automatycznie aktualizują UI przy zmianie danych
  • ViewModel — przetrzymuje obrót ekranu i nie zależy od Android SDK/UIKit, testowany testami jednostkowymi
  • Android Jetpack — ViewModel, LiveData, DataBinding — oficjalny stos od Google dla MVVM
  • SwiftUI + Combine — natywna implementacja MVVM w iOS z @Published i @ObservedObject

Czym jest MVVM: istota wzorca Model-View-ViewModel

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.

KomponentOdpowiedzialnośćPlatforma
ModelDane, logika biznesowa, repozytoriaAndroid/iOS
ViewWyświetlanie, subskrypcja ViewModelActivity/Composable, UIView/SwiftUI View
ViewModelStan ekranu, logika, nawigacjaViewModel (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.

MVVM w Android: ViewModel, LiveData i StateFlow

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.

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

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.

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

Porównanie MVVM z MVP: zalety i wady

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

KryteriumMVPMVVM
Interfejsy ViewContract1 na ekranNiepotrzebne
Zarządzanie subskrypcjamiRęczne attach/detachAutomatyczne (lifecycle-aware)
Obrót ekranuRetain-fragmentViewModel przetrzymuje obrót
TestowanieMock ViewContractCzysta klasa bez zależności
ReaktywnośćCallbacki w PresenterLiveData/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.

Testowanie ViewModel na Android i iOS

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.

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

Jaka jest główna różnica między MVVM a MVP?

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.

Jakie biblioteki są potrzebne do MVVM na Android?

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.

Dlaczego Apple zaleca MVVM dla iOS?

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.

Jak uniknąć wycieków pamięci w ViewModel?

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.

Co wybrać: LiveData czy StateFlow dla Android?

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

  • MVVM (Model-View-ViewModel) — wzorzec reaktywny, gdzie ViewModel nie ma referencji do View
  • ViewModel — przetrzymuje obrót ekranu, testowany testami jednostkowymi, niezależny od UI
  • Android — ViewModel + StateFlow + Kotlin Coroutines — nowoczesny stos od Google
  • iOS — ObservableObject + @Published + SwiftUI — natywna implementacja MVVM
  • MVVM vs MVP — MVVM eliminuje ViewContract i ręczne attach/detach
  • Testowanie — ViewModel pokrywany testami jednostkowymi bez zależności platformowych
  • Zalecenie — MVVM dla nowych projektów; MVP dla utrzymania legacy

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.

Omów projekt

Przeczytaj również