Panoramica di ViewModel   Parte di Android Jetpack.

Prova con Kotlin Multiplatform
Kotlin Multiplatform consente di condividere la logica di business con altre piattaforme. Scopri come configurare e utilizzare ViewModel in KMP

La classe ViewModel è un holder di stato a livello di logica di business o schermata. Espone lo stato all'interfaccia utente e incapsula la logica di business correlata. Il suo vantaggio principale è che memorizza nella cache lo stato e lo mantiene durante le modifiche alla configurazione. Ciò significa che la tua UI non deve recuperare nuovamente i dati quando passi da un'attività all'altra o in seguito a modifiche alla configurazione, ad esempio quando ruoti lo schermo.

Per saperne di più sui titolari dello stato, consulta le indicazioni sui titolari dello stato. Allo stesso modo, per ulteriori informazioni sul livello UI in generale, consulta le indicazioni relative al livello UI.

Vantaggi di ViewModel

L'alternativa a un ViewModel è una classe semplice che contiene i dati visualizzati nella UI. Questo può diventare un problema quando si passa da un'attività all'altra o alle destinazioni di navigazione. In questo modo, i dati vengono eliminati se non li memorizzi utilizzando il meccanismo di salvataggio dello stato dell'istanza. ViewModel fornisce un'API comoda per la persistenza dei dati che risolve questo problema.

I vantaggi principali della classe ViewModel sono essenzialmente due:

  • Consente di rendere persistente lo stato dell'interfaccia utente.
  • Fornisce l'accesso alla logica di business.

Persistenza

ViewModel consente la persistenza sia dello stato mantenuto da ViewModel sia delle operazioni attivate da ViewModel. Questa memorizzazione nella cache significa che non devi recuperare di nuovo i dati tramite modifiche alla configurazione comuni, ad esempio la rotazione dello schermo.

Ambito

Quando crei un'istanza di un ViewModel, gli passi un oggetto che implementa l'interfaccia ViewModelStoreOwner. Può trattarsi di una destinazione di navigazione, di un grafico di navigazione, di un'attività, di un frammento o di qualsiasi altro tipo che implementi l'interfaccia. Il ViewModel viene quindi definito nell'ambito del ciclo di vita del ViewModelStoreOwner. Rimane in memoria finché il suo ViewModelStoreOwner non scompare definitivamente.

Una serie di classi sono sottoclassi dirette o indirette dell'interfaccia ViewModelStoreOwner. Le sottoclassi dirette sono ComponentActivity, Fragment e NavBackStackEntry. Per un elenco completo delle sottoclassi indirette, consulta il riferimento ViewModelStoreOwner.

Quando il fragment o l'attività a cui è associato il ViewModel viene eliminato, il lavoro asincrono continua nel ViewModel associato. Questa è la chiave per la persistenza.

Per maggiori informazioni, consulta la sezione Ciclo di vita di ViewModel di seguito.

SavedStateHandle

SavedStateHandle consente di rendere persistenti i dati non solo durante le modifiche alla configurazione, ma anche durante la ricreazione del processo. ovvero ti consente di mantenere intatto lo stato dell'interfaccia utente anche quando l'utente chiude l'app e la apre in un secondo momento.

Accesso alla logica di business

Anche se la stragrande maggioranza della logica di business è presente nel livello dati, anche il livello UI può contenere logica di business. Questo può accadere quando si combinano dati di più repository per creare lo stato dell'interfaccia utente della schermata o quando un particolare tipo di dati non richiede un livello dati.

ViewModel è il posto giusto per gestire la logica di business nel livello UI. Il ViewModel è anche responsabile della gestione degli eventi e della loro delega ad altri livelli della gerarchia quando è necessario applicare la logica di business per modificare i dati dell'applicazione.

Jetpack Compose

Quando utilizzi Jetpack Compose, ViewModel è il mezzo principale per esporre lo stato dell'interfaccia utente dello schermo ai tuoi composable. In un'app ibrida, le attività e i fragment ospitano semplicemente le funzioni componibili. Si tratta di un cambiamento rispetto agli approcci passati, in cui non era così semplice e intuitivo creare parti riutilizzabili dell'interfaccia utente con attività e frammenti, il che le rendeva molto più attive come controller dell'interfaccia utente.

L'aspetto più importante da tenere presente quando utilizzi ViewModel con Compose è che non puoi definire l'ambito di un ViewModel per un composable. Questo perché un composable non è un ViewModelStoreOwner. Due istanze dello stesso elemento componibile nella composizione o due elementi componibili diversi che accedono allo stesso tipo di ViewModel nello stesso ViewModelStoreOwner riceverebbero la stessa istanza di ViewModel, il che spesso non è il comportamento previsto.

Per usufruire dei vantaggi di ViewModel in Compose, ospita ogni schermata in un Fragment o in un'Activity oppure utilizza Compose Navigation e usa i ViewModel nelle funzioni composable il più vicino possibile alla destinazione di navigazione. perché puoi definire l'ambito di un ViewModel per destinazioni di navigazione, grafici di navigazione, attività e fragment.

Per ulteriori informazioni, consulta la guida sul sollevamento dello stato per Jetpack Compose.

Implementare un ViewModel

Di seguito è riportato un esempio di implementazione di un ViewModel per una schermata che consente all'utente di tirare i dadi.

Kotlin

data class DiceUiState(
    val firstDieValue: Int? = null,
    val secondDieValue: Int? = null,
    val numberOfRolls: Int = 0,
)

class DiceRollViewModel : ViewModel() {

    // Expose screen UI state
    private val _uiState = MutableStateFlow(DiceUiState())
    val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()

    // Handle business logic
    fun rollDice() {
        _uiState.update { currentState ->
            currentState.copy(
                firstDieValue = Random.nextInt(from = 1, until = 7),
                secondDieValue = Random.nextInt(from = 1, until = 7),
                numberOfRolls = currentState.numberOfRolls + 1,
            )
        }
    }
}

Java

public class DiceUiState {
    private final Integer firstDieValue;
    private final Integer secondDieValue;
    private final int numberOfRolls;

    // ...
}

public class DiceRollViewModel extends ViewModel {

    private final MutableLiveData<DiceUiState> uiState =
        new MutableLiveData(new DiceUiState(null, null, 0));
    public LiveData<DiceUiState> getUiState() {
        return uiState;
    }

    public void rollDice() {
        Random random = new Random();
        uiState.setValue(
            new DiceUiState(
                random.nextInt(7) + 1,
                random.nextInt(7) + 1,
                uiState.getValue().getNumberOfRolls() + 1
            )
        );
    }
}

Puoi quindi accedere al ViewModel da un'attività nel seguente modo:

Kotlin

import androidx.activity.viewModels

class DiceRollActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same DiceRollViewModel instance created by the first activity.

        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val viewModel: DiceRollViewModel by viewModels()
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

Java

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.
        DiceRollViewModel model = new ViewModelProvider(this).get(DiceRollViewModel.class);
        model.getUiState().observe(this, uiState -> {
            // update UI
        });
    }
}

Jetpack Compose

import androidx.lifecycle.viewmodel.compose.viewModel

// Use the 'viewModel()' function from the lifecycle-viewmodel-compose artifact
@Composable
fun DiceRollScreen(
    viewModel: DiceRollViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    // Update UI elements
}

Utilizzare le coroutine con ViewModel

ViewModel include il supporto per le coroutine Kotlin. È in grado di rendere persistente il lavoro asincrono nello stesso modo in cui rende persistente lo stato della UI.

Per maggiori informazioni, vedi Utilizzare le coroutine Kotlin con i componenti dell'architettura Android.

Il ciclo di vita di un ViewModel

Il ciclo di vita di un ViewModel è legato direttamente al suo ambito. Un ViewModel rimane in memoria finché non scompare il ViewModelStoreOwner a cui è associato. Ciò può verificarsi nei seguenti contesti:

  • Nel caso di un'attività, al termine.
  • Nel caso di un frammento, quando si stacca.
  • Nel caso di una voce di navigazione, quando viene rimossa dallo stack precedente.

Ciò rende i ViewModel un'ottima soluzione per archiviare i dati che sopravvivono alle modifiche alla configurazione.

La Figura 1 illustra i vari stati del ciclo di vita di un'attività durante la rotazione e al termine. L'illustrazione mostra anche la durata del ViewModel accanto al ciclo di vita dell'attività associata. Questo particolare diagramma illustra gli stati di un'attività. Gli stessi stati di base si applicano al ciclo di vita di un fragment.

Illustra il ciclo di vita di un ViewModel quando un&#39;attività cambia stato.

Di solito, richiedi un ViewModel la prima volta che il sistema chiama il metodo onCreate() di un oggetto attività. Il sistema potrebbe chiamare onCreate() più volte durante l'esistenza di un'attività, ad esempio quando lo schermo di un dispositivo viene ruotato. L'ViewModel esiste dal momento in cui richiedi per la prima volta un ViewModel fino al termine e all'eliminazione dell'attività.

Cancellazione delle dipendenze di ViewModel

ViewModel chiama il metodo onCleared quando ViewModelStoreOwner lo distrugge nel corso del suo ciclo di vita. In questo modo puoi eliminare qualsiasi lavoro o dipendenza che segue il ciclo di vita di ViewModel.

L'esempio seguente mostra un'alternativa a viewModelScope. viewModelScope è un CoroutineScope integrato che segue automaticamente il ciclo di vita di ViewModel. ViewModel lo utilizza per attivare operazioni correlate all'attività. Se vuoi utilizzare un ambito personalizzato anziché viewModelScope per semplificare i test, il ViewModel può ricevere un CoroutineScope come dipendenza nel suo costruttore. Quando ViewModelStoreOwner cancella il ViewModel alla fine del suo ciclo di vita, il ViewModel annulla anche CoroutineScope.

class MyViewModel(
    private val coroutineScope: CoroutineScope =
        CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
) : ViewModel() {

    // Other ViewModel logic ...

    override fun onCleared() {
        coroutineScope.cancel()
    }
}

A partire dal ciclo di vita versione 2.5 e successive, puoi passare uno o più oggetti Closeable al costruttore di ViewModel che si chiude automaticamente quando l'istanza di ViewModel viene cancellata.

class CloseableCoroutineScope(
    context: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    override fun close() {
        coroutineContext.cancel()
   }
}

class MyViewModel(
    private val coroutineScope: CoroutineScope = CloseableCoroutineScope()
) : ViewModel(coroutineScope) {
    // Other ViewModel logic ...
}

Best practice

Di seguito sono riportate alcune best practice chiave da seguire durante l'implementazione di ViewModel:

  • A causa del loro ambito, utilizza i ViewModels come dettagli di implementazione di un holder dello stato a livello di schermata. Non utilizzarli come contenitori di stato di componenti UI riutilizzabili come gruppi di chip o moduli. In caso contrario, otterresti la stessa istanza di ViewModel in diversi utilizzi dello stesso componente UI nello stesso ViewModelStoreOwner, a meno che tu non utilizzi una chiave ViewModel esplicita per chip.
  • I ViewModel non devono conoscere i dettagli di implementazione dell'UI. Mantieni i nomi dei metodi esposti dall'API ViewModel e quelli dei campi dello stato dell'UI il più generici possibile. In questo modo, il ViewModel può ospitare qualsiasi tipo di UI: un cellulare, un dispositivo pieghevole, un tablet o persino un Chromebook.
  • Poiché possono potenzialmente durare più a lungo di ViewModelStoreOwner, i ViewModel non devono contenere riferimenti ad API correlate al ciclo di vita, come Context o Resources, per evitare perdite di memoria.
  • Non passare ViewModels ad altre classi, funzioni o altri componenti UI. Poiché vengono gestiti dalla piattaforma, devi tenerli il più vicino possibile. Vicino alla funzione componibile a livello di attività, frammento o schermata. In questo modo, i componenti di livello inferiore non possono accedere a più dati e logica di quelli necessari.

Ulteriori informazioni

Man mano che i dati diventano più complessi, potresti scegliere di avere una classe separata solo per caricare i dati. Lo scopo di ViewModel è incapsulare i dati per un controller UI per consentire ai dati di sopravvivere alle modifiche alla configurazione. Per informazioni su come caricare, rendere persistenti e gestire i dati tra le modifiche alla configurazione, consulta Stati dell'interfaccia utente salvati.

La Guida all'architettura delle app per Android suggerisce di creare una classe repository per gestire queste funzioni.

Risorse aggiuntive

Per ulteriori informazioni sul corso ViewModel, consulta le seguenti risorse.

Documentazione

Campioni