Présentation de ViewModel Fait partie d'Android Jetpack.

La classe ViewModel est une logique métier ou un conteneur d'état au niveau de l'écran. Elle expose l'état au niveau de l'UI et encapsule la logique métier associée. Son principal avantage est qu'elle assure la mise en cache et la persistance de l'état en cas de modification de la configuration. Cela signifie que votre interface utilisateur n'a pas besoin de récupérer à nouveau les données lorsque vous passez d'une activité à une autre ou suite à une modification de la configuration, par exemple en cas de rotation de l'écran.

Pour plus d'informations sur les conteneurs d'état, lisez les conseils sur les conteneurs d'état. De même, pour en savoir plus sur la couche UI de manière générale, consultez les conseils sur la couche d'interface utilisateur.

Avantages de ViewModel

L'alternative à un ViewModel est une classe simple qui contient les données que vous affichez dans l'interface utilisateur. Cette méthode peut s'avérer problématique lors du passage d'une activité ou d'une destination de navigation à une autre. Elle détruit ces données si vous ne les stockez pas au moyen du mécanisme d'enregistrement de l'état de l'instance. ViewModel fournit une API pratique pour assurer la persistance des données qui résout ce problème.

La classe ViewModel présente principalement deux avantages :

  • Elle vous permet de conserver l'état de l'UI.
  • Elle donne accès à la logique métier.

Persistance

ViewModel permet la persistance à la fois via l'état qu'il détient et via les opérations qu'il déclenche. Cette mise en cache vous évite d'avoir à récupérer à nouveau les données lors des modifications de configuration courantes comme la rotation de l'écran.

Champ d'application

Lorsque vous instanciez un ViewModel, vous lui transmettez un objet qui implémente l'interface ViewModelStoreOwner. Il peut s'agir d'une destination ou d'un graphique de navigation, d'une activité, d'un fragment, ou de tout autre type qui implémente l'interface. Votre ViewModel s'applique ensuite au cycle de vie du ViewModelStoreOwner. Il reste en mémoire jusqu'à ce que son ViewModelStoreOwner disparaisse définitivement.

Une plage de classes représente soit des sous-classes directes, soit des sous-classes indirectes de l'interface ViewModelStoreOwner. Les sous-classes directes sont ComponentActivity, Fragment et NavBackStackEntry. Pour obtenir la liste complète des sous-classes indirectes, consultez la documentation de référence sur ViewModelStoreOwner.

Lorsque le fragment ou l'activité auxquels ViewModel s'applique sont détruits, le travail asynchrone se poursuit dans le ViewModel concerné. C'est la clé de la persistance.

Pour en savoir plus, consultez la section ci-dessous sur le cycle de vie de ViewModel.

SavedStateHandle

SavedStateHandle vous permet de conserver les données non seulement lors des modifications de la configuration, mais également lors de la recréation des processus. Autrement dit, il vous permet de conserver l'état de l'interface utilisateur intact même lorsque l'utilisateur ferme l'application et l'ouvre plus tard.

Accès à la logique métier

Même si la majeure partie de la logique métier se trouve dans la couche de données, la couche d'interface utilisateur peut également en contenir une partie. Ce peut être le cas lorsque vous combinez des données provenant de plusieurs dépôts pour créer l'état de l'UI à l'écran, ou lorsqu'un type de données particulier ne nécessite pas de couche de données.

ViewModel est l'endroit idéal pour gérer la logique métier dans la couche d'interface utilisateur. ViewModel gère également les événements et leur délégation à d'autres couches de la hiérarchie lorsque la logique métier doit être appliquée pour modifier les données d'application.

Jetpack Compose

Lorsque vous utilisez Jetpack Compose, ViewModel constitue le moyen principal d'exposer l'état de l'interface utilisateur à vos composables. Dans une application hybride, les activités et les fragments hébergent simplement vos fonctions modulables. Il s'agit d'un changement par rapport aux approches passées, dans lesquelles il n'était pas aussi simple et intuitif de créer des éléments d'interface utilisateur réutilisables avec des activités et des fragments, ce qui les rendait beaucoup plus actifs comme contrôleurs d'UI.

L'essentiel à garder à l'esprit lorsque vous utilisez ViewModel avec Compose est que vous ne pouvez pas appliquer un ViewModel à un composable. En effet, un composable n'est pas un ViewModelStoreOwner. Deux instances du même composable dans la composition, ou deux composables différents accédant au même type ViewModel dans le même ViewModelStoreOwner, reçoivent la même instance du ViewModel, ce qui souvent n'est pas le comportement attendu.

Pour bénéficier des avantages de ViewModel dans Compose, hébergez chaque écran dans un fragment ou une activité, ou utilisez Compose Navigation et des ViewModels dans des fonctions modulables le plus près possible de la destination de navigation. En effet, vous pouvez appliquer un ViewModel aux destinations et aux graphiques de navigation, aux activités et aux fragments.

Pour en savoir plus, consultez le guide sur le hissage d'état pour Jetpack Compose.

Implémenter un ViewModel

Voici un exemple d'implémentation d'un ViewModel pour un écran permettant à l'utilisateur de lancer des dés.

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
            )
        );
    }
}

Vous pouvez ensuite accéder au ViewModel à partir d'une activité comme suit :

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
}

Utiliser des coroutines avec ViewModel

ViewModel est compatible avec les coroutines Kotlin. Il peut conserver un travail asynchrone de la même manière qu'il conserve l'état de l'UI.

Pour en savoir plus, consultez Utiliser des coroutines Kotlin avec des composants d'architecture Android.

Cycle de vie d'un ViewModel

Le cycle de vie d'un ViewModel est directement lié à son champ d'application. Un ViewModel reste en mémoire jusqu'à ce que le ViewModelStoreOwner auquel il s'applique disparaisse. Cela peut se produire dans les cas suivants :

  • Dans le cas d'une activité, lorsqu'elle se termine.
  • Dans le cas d'un fragment, lorsqu'il se détache.
  • Dans le cas d'une entrée de navigation, lorsqu'elle est supprimée de la pile "Retour".

Les ViewModels constituent donc une solution idéale pour stocker des données qui résistent aux modifications de la configuration.

La figure 1 illustre les différents états de cycle de vie d'une activité lorsqu'elle est soumise à une rotation, et lorsqu'elle est terminée. L'image montre également la durée de vie du ViewModel à côté du cycle de vie de l'activité associé. Ce diagramme illustre les différents états d'une activité. Les mêmes états de base s'appliquent au cycle de vie d'un fragment.

Illustre le cycle de vie d&#39;un ViewModel à mesure que l&#39;activité change d&#39;état.

Vous demandez généralement un objet ViewModel la première fois que le système appelle la méthode onCreate() d'un objet d'activité. Le système peut appeler onCreate() plusieurs fois au cours de l'existence d'une activité, par exemple lors de la rotation de l'écran d'un appareil. Le ViewModel existe entre le moment où vous demandez un ViewModel pour la première fois et le moment où l'activité est terminée et détruite.

Effacer les dépendances de ViewModel

ViewModel appelle la méthode onCleared lorsque ViewModelStoreOwner la détruit au cours de son cycle de vie. Cela vous permet de nettoyer toutes les tâches ou dépendances qui suivent le cycle de vie de ViewModel.

L'exemple suivant présente une alternative à viewModelScope. viewModelScope est une CoroutineScope intégrée qui suit automatiquement le cycle de vie de ViewModel. ViewModel l'utilise pour déclencher des opérations liées à l'entreprise. Si vous souhaitez utiliser un champ d'application personnalisé au lieu de viewModelScope pour des faciliter les tests, le ViewModel peut recevoir une CoroutineScope en tant que dépendance dans son constructeur. Lorsque le ViewModelStoreOwner efface le ViewModel à la fin de son cycle de vie, le ViewModel annule également la CoroutineScope.

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

    // Other ViewModel logic ...

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

À partir de la version 2.5 du cycle de vie, vous pouvez transmettre un ou plusieurs objets Closeable au constructeur du ViewModel, qui se ferme automatiquement lorsque l'instance ViewModel est effacée.

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 ...
}

Bonnes pratiques

Voici quelques bonnes pratiques clés à suivre lorsque vous implémentez ViewModel :

  • En raison de leur champ d'application, utilisez les ViewModels comme détails d'implémentation d'un conteneur d'état au niveau de l'écran. Ne les utilisez pas comme conteneurs d'état de composants d'UI réutilisables, tels que des groupes de puces ou des formulaires. Sinon, vous obtiendrez la même instance ViewModel dans différentes utilisations du même composant d'UI sous le même ViewModelStoreOwner.
  • Les ViewModels ne doivent pas connaître les détails de l'implémentation de l'interface utilisateur. Les noms des méthodes exposées par l'API ViewModel et ceux des champs d'état de l'interface utilisateur doivent être aussi génériques que possible. De cette façon, votre ViewModel peut s'adapter à tout type d'UI : un téléphone mobile, un appareil pliable, une tablette ou même un Chromebook.
  • Comme ils peuvent potentiellement durer plus longtemps que ViewModelStoreOwner, les ViewModels ne doivent contenir aucune référence d'API liée au cycle de vie, comme Context ou Resources, afin d'éviter les fuites de mémoire.
  • Ne transmettez pas de ViewModels à d'autres classes, fonctions ou composants d'UI. Comme la plate-forme les gère, vous devez les garder aussi près que possible de votre activité, de votre fragment ou de votre fonction modulable au niveau de l'écran. Cela permet d'éviter que les composants de niveau inférieur n'accèdent à plus de données et de logique qu'ils n'en ont besoin.

Informations supplémentaires

À mesure que vos données gagnent en complexité, vous pouvez choisir de disposer d'une classe distincte pour les charger. L'objectif de ViewModel est d'encapsuler les données pour un contrôleur d'interface utilisateur afin de permettre aux données de survivre aux modifications de configuration. Pour en savoir plus sur le chargement, la conservation et la gestion des données lors des modifications de configuration, consultez États d'interface utilisateur enregistrés.

Le Guide de l'architecture des applications Android suggère de créer une classe de dépôt pour gérer ces fonctions.

Ressources supplémentaires

Pour en savoir plus sur la classe ViewModel, consultez les ressources suivantes.

Documentation

Exemples