MVVM (Model-View-ViewModel) — ett arkitekturmönster där ViewModel ersätter Presenter och använder reaktiva mekanismer för kommunikation med View: ObservableObject i SwiftUI, LiveData/StateFlow i Android. ViewModel har ingen referens till View — data överförs via prenumeration, vilket eliminerar behovet av ViewContract-gränssnitt och gör testning ännu enklare. Apple rekommenderar MVVM med SwiftUI sedan 2019, Google — MVVM med Jetpack som den officiella Android-arkitekturen. Mer i Android Architecture Guide.
Huvudpunkter
MVVM (Model-View-ViewModel) — ett arkitekturmönster som beskrevs av John Gossman 2005 för Windows Presentation Foundation (WPF) från Microsoft. ViewModel — den centrala komponenten som innehåller skärmens tillstånd och affärslogik, men har ingen referens till View. Data överförs via reaktiva bindningsmekanismer: View prenumererar på ViewModels ändringar och ritas automatiskt om när data ändras.
Den viktigaste skillnaden mellan MVVM och MVP — frånvaron av ViewContract. I MVP anropar Presenter metoder som view.showUser(data), det vill säga Presenter “skjuter” aktivt data till View. I MVVM “drar” View själv data från ViewModel via prenumeration: ViewModel vet inte om den har en prenumerant. Detta eliminerar problemet med frånkopplad View — om Activity förstörs vid rotation fortsätter ViewModel att arbeta, och den nya Activity prenumererar helt enkelt på aktuell data. På IT Sectr har vi använt MVVM i alla nya projekt sedan 2020 — koden blev mer förutsägbar, testerna mer stabila.
| Komponent | Ansvar | Plattform |
|---|---|---|
| Model | Data, affärslogik, repositories | Android/iOS |
| View | Visning, prenumeration på ViewModel | Activity/Composable, UIView/SwiftUI View |
| ViewModel | Skärmtillstånd, logik, navigering | ViewModel (Jetpack), ObservableObject |
Reaktiv bindning — grunden för MVVM. I Android är LiveData (del av Jetpack) en observerbar datalagring. Activity prenumererar via observe(): viewModel.user.observe(this) { user -> binding.name.text = user.name }. När user ändras får alla prenumeranter automatiskt det nya värdet. I iOS använder SwiftUI @Published-egenskaper i ViewModel — ändringar ritar automatiskt om View. Detta eliminerar manuella anrop av showUser/hideLoading som krävs i MVP.
ViewModel från Jetpack — den officiella komponenten från Google för att implementera MVVM. ViewModel överlever skärmrotation: vid konfigurationsändring förstörs Activity och återskapas, medan ViewModel finns kvar i minnet. Den nya Activity-instansen får samma ViewModel via ViewModelProvider. ViewModel har inga referenser till Activity, Context eller View — den är ren och testas med enhetstester utan Robolectric.
// ViewModel med StateFlow — modern implementering av 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) prenumererar på 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 -> /* visa laddning */
is UserState.Success -> /* visa data */
is UserState.Error -> /* visa fel */
}
}.launchIn(lifecycleScope)
viewModel.loadUser(42)
}
}
LiveData vs StateFlow — LiveData (2017) — den första reaktiva Jetpack-komponenten, optimerad för Activitys livscykel: automatisk avprenumeration vid onStop. StateFlow (2021) — Kotlin Flow-implementering, inte bunden till livscykeln, men kräver manuell avprenumeration via lifecycleScope. StateFlow stöder coroutines, concat, map och andra Flow-operatorer som LiveData saknar. På IT Sectr använder vi StateFlow för alla nya ViewModels — den är kortare, kraftfullare och integreras bättre med korutiner.
DataBinding och ViewBinding — DataBinding binder ViewModel till XML via @{viewModel.user.name} direkt i layouten, vilket eliminerar kod i Activity. ViewBinding genererar en typsäker klass för åtkomst till View. Google rekommenderar ViewBinding för enkla projekt och DataBinding för projekt med komplex databindning. I Jetpack Compose behövs inte DataBinding — @Composable-funktioner ritas automatiskt om när State ändras.
MVVM i iOS implementeras via ObservableObject från Combine. ViewModel — en klass som ärver ObservableObject, med @Published-fält. SwiftUI View prenumererar på ViewModel via @ObservedObject eller @StateObject. När en @Published-egenskap ändras ritar SwiftUI automatiskt om View som är beroende av denna egenskap. Apple introducerade SwiftUI 2019 på WWDC tillsammans med Combine — från det ögonblicket blev MVVM det officiellt rekommenderade mönstret för iOS.
import SwiftUI
import Combine
// ViewModel — ObservableObject med @Published-fält
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 — prenumererar på 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 skapar ViewModel och hanterar dess livscykel (en gång under Viewns livstid). @ObservedObject — ViewModel skapas externt och skickas till View. WWDC 2022 rekommenderar @StateObject för skapande och @ObservedObject för att skicka ViewModel mellan Views. I iOS 17 (2023) dök @Observable upp — ett makro som automatiserar prenumeration och eliminerar @Published-annoteringar. @Observable — evolutionen av Combine, som för iOS-utveckling närmare Kotlin Flows reaktivitet.
UIKit + MVVM — för projekt på UIKit (utan SwiftUI) implementeras MVVM via Combine och @Published med prenumeration i UIViewController via sink(). ViewModel är densamma, View — UIViewController med prenumerationer på @Published. Combine är tillgängligt sedan iOS 13 (2019) och är inbyggt i systemet — kräver inga extra beroenden. Enligt Apple Developer Survey (2025) använder 45% av iOS-projekten Combine även med UIKit, 35% använder SwiftUI + Combine, 20% — RxSwift (legacy).
MVVM vinner över MVP i tre viktiga aspekter: frånvaro av ViewContract-gränssnitt, automatisk prenumerationshantering och överlevnad av skärmrotation. I MVP kräver varje skärm ett ViewContract-gränssnitt + Presenter-klass + prenumeration/avprenumeration i onStart/onStop. I MVVM skapas bara ViewModel — prenumeration i Activity sker via observe() utan manuell detach().
| Kriterium | MVP | MVVM |
|---|---|---|
| ViewContract-gränssnitt | 1 per skärm | Behövs inte |
| Prenumerationshantering | Manuell attach/detach | Automatisk (lifecycle-aware) |
| Skärmrotation | Retain-fragment | ViewModel överlever |
| Testning | Mock ViewContract | Ren klass utan beroenden |
| Reaktivitet | Callbacks i Presenter | LiveData/StateFlow/Combine |
Nackdelar med MVVM — komplexiteten i att felsöka reaktiva kedjor och risk för minnesläckor vid felaktig prenumeration. LiveData löser problemet med livscykelsäkerhet, StateFlow kräver lifecycleScope, Combine — sink med AnyCancellable. I MVP är alla anrop explicita (view.showUser), i MVVM kommer data via en reaktiv ström — spårning kräver debug-brytpunkter i subscribe-slutningen. I stora ViewModels med många StateFlows kan en UI-uppdatering missas om View inte prenumererar på en specifik Flow.
När MVP fortfarande är bättre — i projekt med lägsta Android-version under API 21 (Android 5), där Jetpack ViewModel inte är tillgänglig utan AndroidX, och i projekt på ren UIKit utan Combine (iOS 12 och lägre). För legacy-projekt där hela kodbasen redan är på MVP är en fullständig övergång till MVVM inte alltid motiverad — det är billigare att underhålla MVP med gradvis utflyttning av logik till tjänster än att skriva om 100 skärmar på 3 månader.
ViewModel testas med enhetstester utan plattformsberoenden — detta är det främsta argumentet för MVVM. På Android innehåller ViewModel inte Activity, Context eller View — alla beroenden (Repository, UseCase) skickas via konstruktorn och ersätts med mock-objekt. På iOS testas ObservableObject via XCTest utan att starta applikationen, vilket ger stabilitet och snabbhet i testkörningen.
// Enhetstest av Android ViewModel med 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 testas på liknande sätt: vi injicerar en mock UserService, anropar loadUser, kontrollerar tillståndet via XCTestExpectation. Combine Publisher testas via XCTestCase med wait(for: expectations, timeout: 1.0). Strukturen UserState — enum med associated values — gör det möjligt att kontrollera skärmens exakta tillstånd efter en operation.
Kodtäckning i IT Sectr-projekt på MVVM är 75-90% för ViewModel och Repository. ViewModel täcks av enhetstester, Repository — av integrationstester med en testdatabas. View i SwiftUI och Jetpack Compose testas med UI-tester (XCUITest, Compose Test) för kritiska scenarier. Resterande UI kontrolleras med skärmbildstester (Snapshot Testing) — detta är snabbare än UI-tester och ger 95% förtroende för korrekt visning.
Vanliga frågor
I MVVM har ViewModel ingen referens till View — data överförs via reaktiva mekanismer (LiveData, StateFlow, @Published). I MVP anropar Presenter direkt View-metoder via ViewContract-gränssnittet. MVVM eliminerar ViewContract och manuell attach/detach, men kräver förståelse för reaktiva flöden. ViewModel överlever skärmrotation på Android, Presenter kräver retain-fragment.
Minsta uppsättning: lifecycle-viewmodel-ktx (ViewModel), lifecycle-livedata-ktx eller kotlinx-coroutines-core (StateFlow). För injicering — Hilt eller Koin. För asynkrona operationer — Kotlin Coroutines. För komplex databindning — DataBinding. I Jetpack Compose (rekommenderat av Google sedan 2022) räcker compose-runtime och lifecycle-viewmodel-compose.
SwiftUI (2019) är designat för reaktiv arkitektur: @State och @Published ritar automatiskt om View när data ändras. MVVM är en naturlig passform för SwiftUI: View — @ViewBuilder, ViewModel — ObservableObject. Apple tvingar inte MVVM som det enda mönstret, men allt utbildningsmaterial sedan 2019 använder ViewModel + SwiftUI. För UIKit rekommenderar Apple MVC eller Coordinator.
Android: viewModelScope avbryter automatiskt korutiner vid rensning av ViewModel. iOS: AnyCancellable från Combine avprenumererar automatiskt när objektet som lagrar det frigörs. SwiftUI @StateObject hanterar livscykeln automatiskt. Huvudregler: lagra inte referenser till View/Context i ViewModel, avbryt långvariga operationer vid rensning, använd weak self i slutningar.
StateFlow — det moderna valet. LiveData är enklare och livscykelsäkert, men StateFlow är kraftfullare: arbetar med korutiner, stöder flatMap, combine, filter, kräver inte @Nullable-annotering. Det enda scenariot där LiveData är att föredra — arbete med Java-kod där StateFlow (Kotlin Flow-API) inte är tillgängligt. Google rekommenderar StateFlow för nya projekt i Kotlin.
Sammanfattning
Vi utvecklar en mobil applikation nyckelfärdigt
IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.
Läs också