MVVM: cos'è, il pattern Model-View-ViewModel nello sviluppo mobile

Autore: IT Sectr Pubblicato: 2026-02-16 Tempo di lettura: 9 min

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 (dati), View (interfaccia), ViewModel (stato e logica senza riferimento a View)
  • Binding reattivo — LiveData, StateFlow, ObservableObject aggiornano automaticamente l'UI quando i dati cambiano
  • ViewModel — sopravvive alla rotazione dello schermo e non dipende da Android SDK/UIKit, testabile con unit test
  • Android Jetpack — ViewModel, LiveData, DataBinding — stack ufficiale di Google per MVVM
  • SwiftUI + Combine — implementazione nativa di MVVM in iOS con @Published e @ObservedObject

Cos'è MVVM: l'essenza del pattern Model-View-ViewModel

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.

ComponenteResponsabilitàPiattaforma
ModelDati, logica di business, repositoryAndroid/iOS
ViewVisualizzazione, sottoscrizione a ViewModelActivity/Composable, UIView/SwiftUI View
ViewModelStato schermo, logica, navigazioneViewModel (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.

MVVM in Android: ViewModel, LiveData e StateFlow

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.

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

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.

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

Confronto tra MVVM e MVP: vantaggi e svantaggi

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.

CriterioMVPMVVM
Interfacce ViewContract1 per schermataNon necessarie
Gestione sottoscrizioniattach/detach manualeAutomatica (lifecycle-aware)
Rotazione schermoRetain-fragmentViewModel sopravvive alla rotazione
TestMock ViewContractClasse pulita senza dipendenze
ReattivitàCallback in PresenterLiveData/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.

Test del ViewModel su Android e iOS

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.

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

Qual è la differenza principale tra MVVM e MVP?

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.

Quali librerie sono necessarie per MVVM su Android?

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.

Perché Apple raccomanda MVVM per iOS?

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.

Come evitare perdite di memoria in ViewModel?

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.

Cosa scegliere: LiveData o StateFlow per Android?

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

  • MVVM (Model-View-ViewModel) — pattern reattivo dove ViewModel non ha riferimenti a View
  • ViewModel — sopravvive alla rotazione dello schermo, testabile con unit test, indipendente dall'UI
  • Android — ViewModel + StateFlow + Kotlin Coroutines — stack moderno di Google
  • iOS — ObservableObject + @Published + SwiftUI — implementazione nativa di MVVM
  • MVVM vs MVP — MVVM elimina ViewContract e l'attach/detach manuale
  • Test — ViewModel è coperto da unit test senza dipendenze di piattaforma
  • Raccomandazione — MVVM per nuovi progetti; MVP per supporto legacy

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.

Discuti il progetto

Leggi anche