MVVM (Model-View-ViewModel) è un pattern architetturale in cui ViewModel sostituisce Presenter e utilizza meccanismi reattivi per comunicare con View: ObservableObject in SwiftUI, LiveData/StateFlow in Android. ViewModel non ha riferimenti a View — i dati vengono passati tramite sottoscrizione, eliminando la necessità di interfacce ViewContract e rendendo i test ancora più semplici. Apple raccomanda MVVM con SwiftUI dal 2019, Google raccomanda MVVM con Jetpack come architettura ufficiale di Android. Ulteriori informazioni nella Android Architecture Guide.
Punti chiave
MVVM (Model-View-ViewModel) è un pattern architetturale descritto da John Gossman nel 2005 per Windows Presentation Foundation (WPF) di Microsoft. ViewModel è il componente centrale che contiene lo stato dello schermo e la logica di business ma non ha riferimenti a View. I dati vengono trasmessi tramite meccanismi di binding reattivo: View si sottoscrive ai cambiamenti di ViewModel e viene renderizzata automaticamente quando i dati cambiano.
Differenza chiave tra MVVM e MVP — assenza di ViewContract. In MVP, Presenter chiama metodi view.showUser(data), cioè Presenter "spinge" attivamente i dati verso View. In MVVM, la View stessa "tira" i dati da ViewModel tramite sottoscrizione: ViewModel non sa se ha un sottoscrittore. Questo elimina il problema della View disconnessa — se Activity viene distrutta durante la rotazione, ViewModel continua a funzionare e la nuova Activity si sottoscrive semplicemente ai dati correnti. In IT Sectr, utilizziamo MVVM in tutti i nuovi progetti dal 2020 — il codice è diventato più prevedibile, i test più stabili.
| Componente | Responsabilità | Piattaforma |
|---|---|---|
| Model | Dati, logica di business, repository | Android/iOS |
| View | Visualizzazione, sottoscrizione a ViewModel | Activity/Composable, UIView/SwiftUI View |
| ViewModel | Stato schermo, logica, navigazione | ViewModel (Jetpack), ObservableObject |
Binding reattivo — il fondamento di MVVM. In Android, LiveData (parte di Jetpack) è un contenitore di dati osservabile. Activity si sottoscrive tramite observe(): viewModel.user.observe(this) { user -> binding.name.text = user.name }. Quando user cambia, tutti i sottoscrittori ricevono automaticamente il nuovo valore. In iOS, SwiftUI utilizza proprietà @Published in ViewModel — i cambiamenti renderizzano automaticamente View. Questo elimina le chiamate manuali showUser/hideLoading richieste in MVP.
ViewModel di Jetpack — il componente ufficiale di Google per implementare MVVM. ViewModel sopravvive alla rotazione dello schermo: quando la configurazione cambia, Activity viene distrutta e ricreata, mentre ViewModel rimane in memoria. La nuova istanza di Activity ottiene lo stesso ViewModel tramite ViewModelProvider. ViewModel non ha riferimenti ad Activity, Context o View — è pulito e testabile con unit test senza Robolectric.
// ViewModel con StateFlow — implementazione moderna di 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) si sottoscrive a 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 -> /* mostrare il caricamento */
is UserState.Success -> /* visualizzare i dati */
is UserState.Error -> /* mostrare l'errore */
}
}.launchIn(lifecycleScope)
viewModel.loadUser(42)
}
}
LiveData vs StateFlow — LiveData (2017) — il primo componente reattivo di Jetpack, ottimizzato per il ciclo di vita di Activity: annullamento automatico della sottoscrizione a onStop. StateFlow (2021) — implementazione Kotlin Flow, non legata al ciclo di vita, ma che richiede annullamento manuale tramite lifecycleScope. StateFlow supporta coroutine, concat, map e altri operatori Flow, che LiveData non ha. In IT Sectr, utilizziamo StateFlow per tutti i nuovi ViewModel — è più corto, più potente e si integra meglio con le coroutine.
DataBinding e ViewBinding — DataBinding lega ViewModel all'XML tramite @{viewModel.user.name} direttamente nel layout, eliminando codice in Activity. ViewBinding genera una classe type-safe per accedere alle viste. Google raccomanda ViewBinding per progetti semplici e DataBinding per progetti con binding di dati complessi. In Jetpack Compose, DataBinding non è necessario — le funzioni @Composable vengono renderizzate automaticamente quando State cambia.
MVVM in iOS viene implementato tramite ObservableObject di Combine. ViewModel è una classe che eredita ObservableObject, con proprietà @Published. La SwiftUI View si sottoscrive a ViewModel tramite @ObservedObject o @StateObject. Quando una proprietà @Published cambia, SwiftUI renderizza automaticamente la View che dipende da questa proprietà. Apple ha presentato SwiftUI nel 2019 al WWDC insieme a Combine — da allora MVVM è diventato il pattern ufficialmente raccomandato per iOS.
import SwiftUI
import Combine
// ViewModel — ObservableObject con campi @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 — si sottoscrive a 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 crea ViewModel e gestisce il suo ciclo di vita (una volta per durata di View). @ObservedObject — ViewModel viene creato esternamente e passato a View. WWDC 2022 raccomanda @StateObject per la creazione e @ObservedObject per passare ViewModel tra Views. In iOS 17 (2023), è apparsa la macro @Observable — che automatizza la sottoscrizione ed elimina le annotazioni @Published. @Observable è l'evoluzione di Combine, avvicinando lo sviluppo iOS alla reattività di Kotlin Flow.
UIKit + MVVM — per progetti UIKit (senza SwiftUI), MVVM viene implementato tramite Combine e @Published con sottoscrizione in UIViewController via sink(). ViewModel è lo stesso, View è UIViewController con sottoscrizioni a @Published. Combine è disponibile da iOS 13 (2019) ed è integrato nel sistema — non richiede dipendenze aggiuntive. Secondo Apple Developer Survey (2025), il 45% dei progetti iOS utilizza Combine anche con UIKit, il 35% utilizza SwiftUI + Combine, il 20% utilizza RxSwift (legacy).
MVVM vince su MVP in tre aspetti chiave: assenza di interfacce ViewContract, gestione automatica delle sottoscrizioni e sopravvivenza alla rotazione dello schermo. In MVP, ogni schermata richiede un'interfaccia ViewContract + classe Presenter + sottoscrizione/annullamento in onStart/onStop. In MVVM, viene creato solo ViewModel — la sottoscrizione in Activity avviene tramite observe() senza detach() manuale.
| Criterio | MVP | MVVM |
|---|---|---|
| Interfacce ViewContract | 1 per schermata | Non necessarie |
| Gestione sottoscrizioni | attach/detach manuale | Automatica (lifecycle-aware) |
| Rotazione schermo | Retain-fragment | ViewModel sopravvive alla rotazione |
| Test | Mock ViewContract | Classe pulita senza dipendenze |
| Reattività | Callback in Presenter | LiveData/StateFlow/Combine |
Svantaggi di MVVM — complessità di debug delle catene reattive e rischio di perdite di memoria con sottoscrizione errata. LiveData risolve la sicurezza del ciclo di vita, StateFlow richiede lifecycleScope, Combine richiede sink con AnyCancellable. In MVP, tutte le chiamate sono esplicite (view.showUser), in MVVM i dati arrivano tramite un flusso reattivo — la tracciabilità richiede punti di interruzione nelle closure subscribe. In ViewModel grandi con più StateFlow, si può perdere un aggiornamento dell'UI se View non è sottoscritta a un Flow specifico.
Quando MVP è ancora migliore — in progetti con versione minima di Android inferiore a API 21 (Android 5), dove Jetpack ViewModel non è disponibile senza AndroidX, e in progetti che utilizzano UIKit puro senza Combine (iOS 12 e inferiori). Per progetti legacy dove l'intera base di codice è già in MVP, una transizione completa a MVVM non è sempre giustificata — è più economico mantenere MVP con estrazione graduale della logica in servizi che riscrivere 100 schermate in 3 mesi.
ViewModel viene testato con unit test senza dipendenze di piattaforma — questo è l'argomento principale a favore di MVVM. Su Android, ViewModel non contiene Activity, Context o View — tutte le dipendenze (Repository, UseCase) vengono passate tramite il costruttore e sostituite con oggetti mock. Su iOS, ObservableObject viene testato tramite XCTest senza avviare l'applicazione, offrendo stabilità e velocità di esecuzione dei test.
// Test unitario di ViewModel Android con 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)
}
}
ViewModel iOS viene testato in modo simile: iniettare mock UserService, chiamare loadUser, verificare lo stato tramite XCTestExpectation. Combine Publisher viene testato tramite XCTestCase con wait(for: expectations, timeout: 1.0). La struttura UserState — enum con valori associati — permette di verificare lo stato esatto dello schermo dopo un'operazione.
Copertura del codice nei progetti IT Sectr che utilizzano MVVM è del 75–90% per ViewModel e Repository. ViewModel è coperto da unit test, Repository da test di integrazione con database di test. View in SwiftUI e Jetpack Compose viene testata con test UI (XCUITest, Compose Test) per scenari critici. Il resto dell'UI viene verificato con test screenshot (Snapshot Testing) — è più veloce dei test UI e fornisce il 95% di fiducia nella correttezza della visualizzazione.
Domande frequenti
In MVVM, ViewModel non ha riferimenti a View — i dati vengono trasmessi tramite meccanismi reattivi (LiveData, StateFlow, @Published). In MVP, Presenter chiama direttamente i metodi di View tramite l'interfaccia ViewContract. MVVM elimina ViewContract e l'attach/detach manuale, ma richiede la comprensione dei flussi reattivi. ViewModel sopravvive alla rotazione dello schermo su Android, Presenter richiede un retain-fragment.
Set minimo: lifecycle-viewmodel-ktx (ViewModel), lifecycle-livedata-ktx o kotlinx-coroutines-core (StateFlow). Per l'iniezione — Hilt o Koin. Per operazioni asincrone — Kotlin Coroutines. Per binding di dati complesso — DataBinding. In Jetpack Compose (raccomandato da Google dal 2022), compose-runtime e lifecycle-viewmodel-compose sono sufficienti.
SwiftUI (2019) è progettato per architettura reattiva: @State e @Published renderizzano automaticamente View quando i dati cambiano. MVVM è un adattamento naturale per SwiftUI: View — @ViewBuilder, ViewModel — ObservableObject. Apple non impone MVVM come unico pattern, ma tutti i materiali di formazione dal 2019 utilizzano ViewModel + SwiftUI. Per UIKit, Apple raccomanda MVC o Coordinator.
Android: viewModelScope annulla automaticamente le coroutine quando ViewModel viene pulito. iOS: AnyCancellable di Combine annulla automaticamente la sottoscrizione quando l'oggetto che lo contiene viene deallocato. SwiftUI @StateObject gestisce il ciclo di vita automaticamente. Regole principali: non memorizzare riferimenti a View/Context in ViewModel, annullare operazioni di lunga durata durante la pulizia, usare weak self nelle closure.
StateFlow è la scelta moderna. LiveData è più semplice e sicuro per il ciclo di vita, ma StateFlow è più potente: funziona con coroutine, supporta flatMap, combine, filter, non richiede annotazione @Nullable. L'unico scenario in cui LiveData è preferibile — lavorare con codice Java dove StateFlow (Kotlin Flow API) non è disponibile. Google raccomanda StateFlow per nuovi progetti Kotlin.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche