Panoramica di ViewModel Componente di Android Jetpack.

La classe ViewModel è un titolare di stato a livello di logica di business o di schermo. 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 persiste tramite le modifiche alla configurazione. Ciò significa che la tua UI non deve recuperare nuovamente i dati durante la navigazione tra attività o in seguito a modifiche alla configurazione, ad esempio durante la rotazione dello schermo.

Per ulteriori informazioni sui titolari di stati, consulta le indicazioni relative ai proprietari. Allo stesso modo, per ulteriori informazioni sul livello UI in generale, consulta le linee guida relative al livello UI.

Vantaggi di ViewModel

L'alternativa a un ViewModel è una classe normale che contiene i dati visualizzati nella UI. Questo può diventare un problema durante la navigazione tra le attività o le destinazioni di navigazione. Questa operazione distrugge i dati se non li archivi utilizzando il meccanismo di salvataggio dello stato dell'istanza. ViewModel fornisce un'API comoda per la persistenza dei dati che risolve il problema.

I vantaggi principali della classe ViewModel sono essenzialmente due:

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

Persistenza

ViewModel consente la persistenza sia tramite lo stato mantenuto da un ViewModel, sia tramite le operazioni attivate da un ViewModel. Grazie a questa memorizzazione nella cache, non devi più recuperare i dati tramite modifiche comuni alla configurazione, ad esempio una rotazione dello schermo.

Ambito

Quando crei un'istanza di un ViewModel, 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 implementa l'interfaccia. L'ambito ViewModel è quindi limitato al ciclo di vita di ViewModelStoreOwner. Rimane in memoria fino a quando la sua ViewModelStoreOwner non scompare definitivamente.

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

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

Per saperne di più, consulta la sezione seguente sul ciclo di vita di ViewModel.

HandleStateSalvato

SaveStateHandle consente di mantenere i dati persistenti non solo tramite le modifiche alla configurazione, ma durante la ricreazione dei processi. Ciò 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 maggior parte della logica di business è presente nel livello dati, il livello UI può anche contenere la logica di business. ad esempio quando combini i dati di più repository per creare lo stato dell'interfaccia utente della schermata o quando un determinato tipo di dati non richiede un livello dati.

ViewModel è il posto giusto per gestire la logica di business nel livello UI. ViewModel ha anche il compito di gestire gli eventi e di delerli ad altri livelli della gerarchia quando è necessario applicare la logica di business per modificare i dati delle applicazioni.

Jetpack Compose

Quando utilizzi Jetpack Compose, ViewModel è il mezzo principale per esporre lo stato dell'UI dello schermo ai tuoi componibili. In un'app ibrida, le attività e i frammenti ospitano semplicemente le funzioni componibili. Questo è un cambiamento rispetto ad approcci passati, dove non era così semplice e intuitivo creare parti dell'interfaccia utente riutilizzabili con attività e frammenti, rendendoli molto più attivi come controller UI.

La cosa più importante da tenere presente quando si utilizza ViewModel con Compose è che non è possibile definire l'ambito di un ViewModel in un componibile. Questo perché un componibile non è un ViewModelStoreOwner. Due istanze dello stesso componibile nella composizione oppure due diversi elementi componibili che accedono allo stesso tipo di ViewModel nello stesso ViewModelStoreOwner riceverebbero la stessa istanza dell'ViewModel, che spesso non è il comportamento previsto.

Per usufruire dei vantaggi di ViewModel in Compose, ospita ogni schermata in un frammento o un'attività oppure utilizza la navigazione di Compose e utilizza i ViewModel nelle funzioni componibili il più vicino possibile alla destinazione di navigazione. Questo perché puoi definire l'ambito di un ViewModel a destinazioni di navigazione, grafici di navigazione, attività e frammenti.

Per ulteriori informazioni, consulta la guida all'organizzazione 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 lanciare 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 a 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 mantenere il lavoro asincrono nello stesso modo in cui persiste lo stato dell'UI.

Per ulteriori informazioni, consulta Utilizzare le coroutine Kotlin con i componenti dell'architettura Android.

Il ciclo di vita di un ViewModel

Il ciclo di vita di una ViewModel è legato direttamente al suo ambito. Un elemento ViewModel rimane in memoria finché non scompare l'ViewModelStoreOwner a cui è limitato. Ciò può verificarsi nei seguenti contesti:

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

Questo rende ViewModels un'ottima soluzione per l'archiviazione dei dati che sono sopravvissuti alle modifiche alla configurazione.

La Figura 1 illustra i vari stati del ciclo di vita di un'attività mentre viene sottoposta a una rotazione e poi terminata. L'illustrazione mostra anche la durata di ViewModel accanto al ciclo di vita dell'attività associata. Questo schema specifico illustra gli stati di un'attività. Gli stessi stati di base si applicano al ciclo di vita di un frammento.

Illustrazione del ciclo di vita di un ViewModel come un&#39;attività cambia stato.

In genere viene richiesto un valore ViewModel la prima volta che il sistema chiama il metodo onCreate() di un oggetto attività. Il sistema può chiamare onCreate() più volte nel corso di un'attività, ad esempio quando lo schermo di un dispositivo viene ruotato. La ViewModel esiste dal momento in cui richiedi per la prima volta una ViewModel fino al completamento ed eliminazione dell'attività.

Cancellazione delle dipendenze ViewModel

ViewModel chiama il metodo onCleared quando ViewModelStoreOwner lo elimina nel corso del suo ciclo di vita. Questo ti consente di pulire qualsiasi lavoro o dipendenza che segue il ciclo di vita di ViewModel.

L'esempio seguente mostra un'alternativa a viewModelScope. viewModelScope è un componente CoroutineScope integrato che segue automaticamente il ciclo di vita del modello ViewModel. ViewModel lo utilizza per attivare operazioni relative all'attività. Se vuoi utilizzare un ambito personalizzato invece di viewModelScope per test più semplici, l'oggetto ViewModel può ricevere CoroutineScope come dipendenza nel suo costruttore. Quando ViewModelStoreOwner cancella l'oggetto ViewModel al termine del suo ciclo di vita, l'oggetto ViewModel annulla anche CoroutineScope.

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

    // Other ViewModel logic ...

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

Dalla versione 2.5 e successive del ciclo di vita, puoi passare uno o più oggetti Closeable al costruttore di ViewModel, che si chiude automaticamente quando l'istanza 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 che devi seguire per implementare ViewModel:

  • A causa della loro ambito, utilizza i ViewModel come dettagli di implementazione di un titolare di stato a livello di schermata. Non usarli come contenitori di stato di componenti UI riutilizzabili, come moduli o gruppi di chip. In caso contrario, otterresti la stessa istanza ViewModel in diversi utilizzi dello stesso componente UI nello stesso ViewModelStoreOwner.
  • I modelli ViewModel non devono conoscere i dettagli di implementazione dell'interfaccia utente. Mantieni i nomi dei metodi esposti dall'API ViewModel e quelli dei campi di stato dell'interfaccia utente il più generici possibile. In questo modo, ViewModel può supportare qualsiasi tipo di UI: cellulare, pieghevole, tablet o persino Chromebook.
  • Poiché possono avere una durata potenzialmente più lunga di ViewModelStoreOwner, i modelli ViewModel non devono contenere riferimenti di API relative al ciclo di vita, come Context o Resources per evitare perdite di memoria.
  • Non passare i ViewModel ad altre classi, funzioni o altri componenti dell'interfaccia utente. Poiché è la piattaforma a gestirli, devi avvicinarli il più possibile. Vicino alla funzione componibile a livello di attività, frammento o schermo. Ciò impedisce ai componenti di livello inferiore di accedere a più dati e logica di quella necessaria.

Ulteriori informazioni

Man mano che i dati diventano più complessi, potresti scegliere di creare una classe distinta solo per caricarli. Lo scopo di ViewModel è incapsulare i dati per un controller dell'interfaccia utente per fare in modo che siano sopravvissuti alle modifiche alla configurazione. Per informazioni su come caricare, mantenere e gestire i dati nelle modifiche alla configurazione, consulta Stati dell'interfaccia utente salvati.

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

Risorse aggiuntive

Per ulteriori informazioni sulla classe ViewModel, consulta le seguenti risorse.

Documentazione

Campioni