MVVM (Model-View-ViewModel) — egy architekturális minta, amelyben a ViewModel helyettesíti a Presentert és reaktív mechanizmusokat használ a View-val való kommunikációhoz: ObservableObject a SwiftUI-ban, LiveData/StateFlow Androidban. A ViewModel nem rendelkezik referenciával a View-ra — az adatok előfizetésen keresztül kerülnek továbbításra, ami kiküszöböli a ViewContract interfészek szükségességét és még egyszerűbbé teszi a tesztelést. Az Apple 2019 óta ajánlja az MVVM-et SwiftUI-val, a Google — az MVVM-et Jetpackkel mint az Android hivatalos architektúráját. Bővebben a Android Architecture Guide-ban.
Főbb pontok
MVVM (Model-View-ViewModel) — egy architekturális minta, amelyet John Gossman írt le 2005-ben a Microsoft Windows Presentation Foundation (WPF) rendszeréhez. A ViewModel — a központi komponens, amely tartalmazza a képernyő állapotát és az üzleti logikát, de nem rendelkezik referenciával a View-ra. Az adatok reaktív kötési mechanizmusokon keresztül kerülnek továbbításra: a View feliratkozik a ViewModel változásaira, és automatikusan újrarajzolódik, amikor az adatok megváltoznak.
Az MVVM legfontosabb különbsége az MVP-től — a ViewContract hiánya. Az MVP-ben a Presenter meghívja a view.showUser(data) metódusokat, vagyis a Presenter aktívan “tolja” az adatokat a View-ba. Az MVVM-ben a View maga “húzza” az adatokat a ViewModel-ből előfizetésen keresztül: a ViewModel nem tudja, van-e előfizetője. Ez kiküszöböli a leválasztott View problémáját — ha az Activity megsemmisül elforgatáskor, a ViewModel folytatja a munkát, és az új Activity egyszerűen feliratkozik az aktuális adatokra. Az IT Sectr-ben 2020 óta minden új projektben MVVM-et használunk — a kód kiszámíthatóbb lett, a tesztek stabilabbak.
| Komponens | Felelősség | Platform |
|---|---|---|
| Model | Adatok, üzleti logika, repozitóriumok | Android/iOS |
| View | Megjelenítés, feliratkozás ViewModel-re | Activity/Composable, UIView/SwiftUI View |
| ViewModel | Képernyő állapota, logika, navigáció | ViewModel (Jetpack), ObservableObject |
Reaktív kötés — az MVVM alapja. Androidban a LiveData (a Jetpack része) egy megfigyelhető adattároló. Az Activity az observe()-en keresztül iratkozik fel: viewModel.user.observe(this) { user -> binding.name.text = user.name }. Amikor a user megváltozik, minden előfizető automatikusan megkapja az új értéket. iOS-ben a SwiftUI @Published tulajdonságokat használ a ViewModel-ben — a változások automatikusan újrarajzolják a View-t. Ez kiküszöböli a kézi showUser/hideLoading hívásokat, amelyek az MVP-ben szükségesek.
ViewModel a Jetpackből — a Google hivatalos komponense az MVVM megvalósításához. A ViewModel túléli a képernyő elforgatását: konfigurációváltáskor az Activity megsemmisül és újra létrejön, míg a ViewModel a memóriában marad. Az Activity új példánya ugyanazt a ViewModel-t kapja a ViewModelProvider-en keresztül. A ViewModel nem rendelkezik referenciával az Activity-re, Context-re vagy View-ra — tiszta, és Robolectric nélkül unittesztekkel tesztelhető.
// ViewModel StateFlow-val — az MVVM modern megvalósítása
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) feliratkozik a state-re
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 -> /* betöltés megjelenítése */
is UserState.Success -> /* adatok megjelenítése */
is UserState.Error -> /* hiba megjelenítése */
}
}.launchIn(lifecycleScope)
viewModel.loadUser(42)
}
}
LiveData vs StateFlow — a LiveData (2017) — az első reaktív Jetpack komponens, optimalizálva az Activity életciklusára: automatikus leiratkozás onStop-kor. A StateFlow (2021) — Kotlin Flow megvalósítás, nem kötődik az életciklushoz, de kézi leiratkozást igényel a lifecycleScope-on keresztül. A StateFlow támogatja a coroutine-okat, concat, map és más Flow operátorokat, amelyek a LiveData-ban nincsenek. Az IT Sectr-ben a StateFlow-t használjuk minden új ViewModel-hez — rövidebb, erősebb és jobban integrálódik a korutinokkal.
DataBinding és ViewBinding — a DataBinding a ViewModel-t XML-hez köti a @{viewModel.user.name} segítségével közvetlenül a layout-ban, kiküszöbölve a kódot az Activity-ből. A ViewBinding egy típusbiztos osztályt generál a View eléréséhez. A Google a ViewBinding-et ajánlja egyszerű projektekhez, a DataBinding-et pedig összetett adatkötésű projektekhez. A Jetpack Compose-ban nincs szükség DataBinding-re — a @Composable függvények automatikusan újrarajzolódnak a State változásakor.
MVVM iOS-ben a Combine-ból származó ObservableObject segítségével valósul meg. A ViewModel — egy osztály, amely az ObservableObject-ből örököl, @Published mezőkkel. A SwiftUI View a @ObservedObject vagy @StateObject segítségével iratkozik fel a ViewModel-re. Amikor egy @Published tulajdonság megváltozik, a SwiftUI automatikusan újrarajzolja a View-t, amely ettől a tulajdonságtól függ. Az Apple 2019-ben mutatta be a SwiftUI-t a WWDC-n a Combine-nal együtt — ettől a pillanattól kezdve az MVVM hivatalosan ajánlott mintává vált iOS-ben.
import SwiftUI
import Combine
// ViewModel — ObservableObject @Published mezőkkel
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 — feliratkozik a ViewModel-re
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 — a @StateObject létrehozza a ViewModel-t és kezeli annak életciklusát (egyszer a View élettartama alatt). A @ObservedObject — a ViewModel kívülről jön létre, és átadásra kerül a View-nak. A WWDC 2022 a @StateObject-t ajánlja létrehozáshoz, a @ObservedObject-t pedig a ViewModel View-k közötti átadásához. Az iOS 17-ben (2023) megjelent a @Observable — egy makró, amely automatizálja az előfizetést és kiküszöböli a @Published annotációkat. A @Observable — a Combine evolúciója, amely közelebb hozza az iOS-fejlesztést a Kotlin Flow reaktivitásához.
UIKit + MVVM — UIKit-es projektekhez (SwiftUI nélkül) az MVVM a Combine és @Published segítségével valósul meg, előfizetéssel az UIViewController-ben a sink()-en keresztül. A ViewModel ugyanaz, a View — UIViewController @Published előfizetésekkel. A Combine iOS 13-tól (2019) érhető el, és beépül a rendszerbe — nem igényel további függőségeket. Az Apple Developer Survey (2025) szerint az iOS-projektek 45%-a használ Combine-t még UIKit esetén is, 35% használ SwiftUI + Combine-t, 20% — RxSwift (legacy).
Az MVVM felülmúlja az MVP-t három kulcsfontosságú szempontból: a ViewContract interfészek hiánya, az előfizetések automatikus kezelése és a képernyőelforgatás túlélése. Az MVP-ben minden képernyőhöz szükség van egy ViewContract interfészre + Presenter osztályra + előfizetés/leiratkozás az onStart/onStop-ban. Az MVVM-ben csak a ViewModel jön létre — az Activity-ben az előfizetés az observe()-en keresztül történik kézi detach() nélkül.
| Szempont | MVP | MVVM |
|---|---|---|
| ViewContract interfészek | 1 képernyőnként | Nem szükségesek |
| Előfizetések kezelése | Kézi attach/detach | Automatikus (lifecycle-aware) |
| Képernyő elforgatása | Retain-fragment | ViewModel túléli |
| Tesztelés | Mock ViewContract | Tiszta osztály függőségek nélkül |
| Reaktivitás | Callback-ek a Presenter-ben | LiveData/StateFlow/Combine |
Az MVVM hátrányai — a reaktív láncok hibakeresésének bonyolultsága és a memóriaszivárgás kockázata helytelen előfizetés esetén. A LiveData megoldja az életciklus-biztonság problémáját, a StateFlow lifecycleScope-ot igényel, a Combine — sink-et AnyCancellable-lel. Az MVP-ben minden hívás explicit (view.showUser), az MVVM-ben az adatok reaktív folyamon keresztül érkeznek — a nyomkövetés debug-töréspontokat igényel a subscribe lezárásban. Nagy ViewModel-ekben sok StateFlow-val előfordulhat, hogy a UI-frissítés elmarad, ha a View nincs feliratkozva egy adott Flow-ra.
Mikor jobb még az MVP — olyan projektekben, ahol a minimális Android verzió API 21 (Android 5) alatt van, ahol a Jetpack ViewModel AndroidX nélkül nem érhető el, és tiszta UIKit-es projektekben Combine nélkül (iOS 12 és alatta). Olyan legacy projekteknél, ahol a teljes kódbázis már MVP-n alapul, a teljes átállás az MVVM-re nem mindig indokolt — olcsóbb fenntartani az MVP-t a logika fokozatos szolgáltatásokba helyezésével, mint 100 képernyőt átírni 3 hónap alatt.
A ViewModel unittesztekkel tesztelhető platformfüggőségek nélkül — ez a fő érv az MVVM mellett. Androidon a ViewModel nem tartalmaz Activity-t, Context-et vagy View-t — minden függőség (Repository, UseCase) a konstruktoron keresztül kerül átadásra, és mock objektumokkal helyettesíthető. iOS-ben az ObservableObject XCTesten keresztül tesztelhető az alkalmazás elindítása nélkül, ami stabilitást és gyorsaságot biztosít a tesztek végrehajtásában.
// Android ViewModel unit teszt MockK-val
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 hasonlóan tesztelhető: injektálunk egy mock UserService-t, meghívjuk a loadUser-t, ellenőrizzük az állapotot XCTestExpectation segítségével. A Combine Publisher XCTestCase-en keresztül tesztelhető wait(for: expectations, timeout: 1.0) paraméterrel. A UserState struktúra — enum associated values-szel — lehetővé teszi a képernyő pontos állapotának ellenőrzését a művelet után.
Kódlefedettség az IT Sectr MVVM-es projektjeiben 75-90% a ViewModel és Repository esetében. A ViewModel-t unittesztek fedik le, a Repository-t — integrációs tesztek tesztadatbázissal. A View SwiftUI-ban és Jetpack Compose-ban UI-tesztekkel (XCUITest, Compose Test) tesztelhető kritikus forgatókönyvek esetén. A UI többi része képernyőkép-tesztekkel (Snapshot Testing) ellenőrizhető — ez gyorsabb, mint a UI-tesztek, és 95%-os biztonságot nyújt a megjelenítés helyességében.
Gyakran Ismételt Kérdések
Az MVVM-ben a ViewModel nem rendelkezik referenciával a View-ra — az adatok reaktív mechanizmusokon (LiveData, StateFlow, @Published) keresztül kerülnek továbbításra. Az MVP-ben a Presenter közvetlenül hívja meg a View metódusait a ViewContract interfészen keresztül. Az MVVM kiküszöböli a ViewContract-ot és a kézi attach/detach-t, de megköveteli a reaktív folyamatok megértését. A ViewModel Androidon túléli a képernyő elforgatását, a Presenter retain-fragment-et igényel.
Minimális készlet: lifecycle-viewmodel-ktx (ViewModel), lifecycle-livedata-ktx vagy kotlinx-coroutines-core (StateFlow). Injektáláshoz — Hilt vagy Koin. Aszinkron műveletekhez — Kotlin Coroutines. Összetett adatkötéshez — DataBinding. Jetpack Compose-ban (amelyet a Google 2022 óta ajánl) elegendő a compose-runtime és a lifecycle-viewmodel-compose.
A SwiftUI (2019) reaktív architektúrára lett tervezve: a @State és @Published automatikusan újrarajzolja a View-t az adatok változásakor. Az MVVM természetes illeszkedés a SwiftUI-hoz: View — @ViewBuilder, ViewModel — ObservableObject. Az Apple nem erőlteti az MVVM-et egyetlen mintaként, de minden oktatási anyag 2019 óta ViewModel + SwiftUI-t használ. A UIKit-hez az Apple az MVC-t vagy a Coordinator-t ajánlja.
Android: a viewModelScope automatikusan lemondja a korutinokat a ViewModel tisztításakor. iOS: a Combine-ból származó AnyCancellable automatikusan leiratkozik a tároló objektum felszabadításakor. A SwiftUI @StateObject automatikusan kezeli az életciklust. Fő szabályok: ne tároljon referenciákat View/Context-re a ViewModel-ben, mondja le a hosszú futású műveleteket tisztításkor, használjon weak self-et a lezárásokban.
A StateFlow — a modern választás. A LiveData egyszerűbb és életciklus-biztos, de a StateFlow erősebb: korutinokkal működik, támogatja a flatMap, combine, filter funkciókat, nem igényel @Nullable annotációt. Az egyetlen forgatókönyv, ahol a LiveData előnyösebb — Java kóddal való munka, ahol a StateFlow (Kotlin Flow-API) nem érhető el. A Google a StateFlow-t ajánlja új Kotlin projektekhez.
Összefoglalás
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is