• 12 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 01/10/2024

Gérez un état ou un événement métier

Dans les applications qui n'utilisent pas encore Jetpack Compose, vous utilisez probablement déjà un ViewModel pour centraliser la logique métier et gérer l'état des données avec des types observables commeStateFlowouLiveData. Cela simplifie la gestion des événements et assure la persistance des données, tout en séparant la logique métier de l’interface utilisateur pour une application plus fluide et robuste. Avec Jetpack Compose, ce principe reste inchangé.

Remontez les états et événements métier au composant racine

Si votre composant comporte des états représentant des données métier ou si ses événements impliquent de la logique métier, la règle est simple : remontez ces états ou événements dans les paramètres de sa fonction composable et laissez le parent les transmettre à son tour à son parent via ses paramètres, et ainsi de suite, jusqu’au composant racine.

Comment cela s’applique à notre application, BestPodcast ?

En remontant les états et événements identifiés pour notre écran de suggestion, voici la signature du composantSuggestion.

@Composable
fun Suggestion(
    podcast: Podcast,
    onAddToLibraryClicked: () -> Unit,
    onFavouriteToggled: () -> Unit,
    modifier: Modifier = Modifier,
) { /*...*/ }

Voici la signature du composantCategoryFilter.

@Composable 
fun CategoryFilter(
    categories: List<Category>,
    selectedId: Int?,
    onCategoryClicked: (Int) -> Unit,
    modifier: Modifier = Modifier
) { /*...*/

 Voici la signature du composantSuggestionList.

@Composable
fun SuggestionsList(
    suggestions: List<Podcast>,
    categories: List<Category>,
    selectedCategoryId: Int?,
    filterSuggestions: (Int) -> Unit,
    onFavouriteToggled: (podcastId: String) -> Unit,
    onAddToLibraryClicked: (podcastId: String) -> Unit,
    modifier: Modifier = Modifier,
) { /*...*/ }

Et voici l’arbre de composition associé.

Diagramme de la structure d'une interface utilisateur en Jetpack Compose montrant une architecture avec des composants @Composable : Scaffold (SuggestionScreen), Box, SuggestionsList avec des filtres et actions, CategoryFilter et Suggestion.
Arbre de composition des états et événements métier de l’écran “SuggestionsScreen” remontés dans les paramètres des fonctions composable enfants

Récupérez les états et événements métier dans le composant racine

Ce n’est pas encore terminé car maintenant le composant racine doit fournir tous les états et les événements métier de notre écran.

OK, mais où doit-il aller les chercher ?

Dans une architecture MVVM, le composant racine va tout naturellement récupérer ces données métier depuis le ViewModel qui lui est associé. Pour cela, il est recommandé d’ajouter au composant racine un paramètre optionnel de type ViewModel. Ce paramètre doit être initialisé par défaut grâce à la fonctionviewModel()issue de la dépendanceandroidx.lifecycle:lifecycle-viewmodel-compose.

À ce stade, vous vous demandez peut-être pourquoi nous faisons remonter tous les états et événements jusqu'au composant racine, plutôt que d'accéder directement au ViewModel depuis chaque composant qui manipule un état métier. Accéder directement au ViewModel depuis chaque composant compliquerait la gestion du cycle de vie, augmenterait le couplage entre les composants et pourrait nuire aux performances. C'est pourquoi il est préférable d'accéder au ViewModel uniquement depuis le composant racine.

Comment cela s’applique à notre application, BestPodcast ?

Pour utiliser un ViewModel au sein de notre application, il faut suivre les étapes suivantes.

1. Ajouter un paramètre de typeSuggestionsViewModelau composant racineSuggestionsScreen:

@Composable
def SuggestionsScreen(
    modifier: Modifier = Modifier,
    viewModel: SuggestionsViewModel = viewModel(),
) {
   /*...*/
}

 2. En parallèle, créer la classeSuggestionsViewModelqui étendViewModel:

class SuggestionsViewModel : ViewModel() {
}

3. Comme cette classe doit gérer l’état de notre écran, il faut modéliser cet état en kotlin au sein d’unedata class. Pour rappel, les états métiers de l’écran des suggestions sont :

  • les différentes catégories ;

  • la liste des podcasts suggérés ;

  • l’identifiant de la catégorie sélectionné dans le filtre.

La modélisation de cet état donne alors :

data class SuggestionsUiState(
    val podcasts: List<Podcast>,
    val categories: List<Category>,
    val selectedCategoryId: Int? = null,
)

4. Émettre cet état en utilisant unStateFlow.

class SuggestionsViewModel : ViewModel() {
    private val podcasts = PodcastFactory.makePodcasts()
    private val categories = podcasts.map { it.category }.distinct()
    private val _uiState = MutableStateFlow(
        SuggestionsUiState(
            podcasts = podcasts,
            categories = categories
        )
    )
    val uiState: StateFlow<SuggestionsUiState> = _uiState.asStateFlow()
}

Dans notre exemple, l’état reçoit les données depuis notrePodcastFactory, mais dans la vraie vie, ces données proviendraient certainement du réseau ou de la base de données et seraient fournies par une classe de type Repository comme nous avons pu le voir dans le chapitre “Écrivez la couche interface utilisateur de l’architecture MVVM” du cours “Gérez vos données localement pour une application Android 100 % hors-ligne”. Vous y retrouverez aussi la notion deStateFlow.

Générez un état supporté par Jetpack Compose

Jetpack Compose n’est pas capable de manipuler directement les états observables tels que lesStateFlowetLiveData. Pour les exposer à un composant écrit en Jetpack Compose, il est nécessaire de les convertir en état de typeState<T>.

Pour transformer un état observable standard en état de typeState<T>, Jetpack Compose fournit plusieurs fonctions, comme :

  • StateFlow<T>.collectAsState()pour transformer unStateFlow;

  • LiveData<T>.observeAsState()pour transformer unLiveData.

Jetpack Compose offre également des fonctions pour transformer des états observables tout en respectant le cycle de vie de l’application évitant ainsi d’éventuelles fuites mémoire, comme :

  • StateFlow<T>.collectAsStateWithLifecycle();

  • LiveData<T>.observeAsStateWithLifecycle().

Pour utiliser ces deux fonctions, vous devez ajouter la dépendanceandroidx.lifecycle:lifecycle-runtime-compose.

Comment cela s’applique à notre application, BestPodcast ?

SuggestionsViewModelutilise unStateFlowpour récupérer l’état nomméuiStatedepuis la fonction composableSuggestionsScreen.

1. Nous allons donc utiliser la fonctioncollectAsStateWithLifecycle.

@Composable
fun SuggestionsScreen(
    modifier: Modifier = Modifier,
    viewModel: SuggestionsViewModel = viewModel(),
) {
    val uiState = viewModel.uiState.collectAsStateWithLifecycle()
    /*...*/
}

2. Nous partageons aux enfants de notre écran les états dont ils ont besoin, comme ceci :

@Composable
fun SuggestionsScreen(
    modifier: Modifier = Modifier,
    viewModel: SuggestionsViewModel = viewModel(),
) {
    val uiState = viewModel.uiState.collectAsStateWithLifecycle()
    
    Scaffold(/*...*/) { innerPadding ->
        Box(/*...*/) {
            /*...*/
            SuggestionsList(
                suggestions = uiState.value.podcasts,
                categories = uiState.value.categories,
                selectedCategoryId = uiState.value.selectedCategoryId,
                filterSuggestions = { /* TODO */ },
                onAddToLibraryClicked = { /* TODO */},
                onFavouriteToggled = { /* TODO */ },
            )
            /*...*/
        }
    }
}

3. Pour gérer les événements métiers de notre écran, nous allons ajouter les fonctions correspondantes dansSuggestionsViewModel:

class SuggestionsViewModel : ViewModel() {
    private val podcasts = PodcastFactory.makePodcasts()
    private val categories = podcasts.map { it.category }.distinct()
    private val _uiState = MutableStateFlow(
        SuggestionsUiState(
            podcasts = podcasts,
            categories = categories
        )
    )
    val uiState: StateFlow<SuggestionsUiState> = _uiState.asStateFlow()
    fun filterByCategory(categoryId: Int) {
        /*TODO */
    }
    fun togglePodcastInLibrary(podcastId: String) {
        /*TODO */
    }
    fun toggleFavouritePodcast(podcastId: String) {
        /* TODO */
    }
}

4. Nous pouvons maintenant appeler ces fonctions depuis le composant racine aux endroits adéquats dans le code du composantSuggestionsScreen, en utilisant l’instance duViewModelqui lui est passée en paramètre :

@Composable
fun SuggestionsScreen(
    modifier: Modifier = Modifier,
    viewModel: SuggestionsViewModel = viewModel(),
) {
    val uiState = viewModel.uiState.collectAsStateWithLifecycle()
    
    Scaffold(/*...*/) { innerPadding ->
        Box(/*...*/) {
            /*...*/
            SuggestionsList(
                suggestions = uiState.value.podcasts,
                categories = uiState.value.categories,
                selectedCategoryId = uiState.value.selectedCategoryId,
                filterSuggestions = { viewModel.filterByCategory(it) },
                onAddToLibraryClicked= { viewModel.togglePodcastInLibrary(it) },
                onFavouriteToggled = { viewModel.toggleFavouritePodcast(it) },
            )
            /*...*/
        }
    }
}

Cet arbre de composition annoté récapitule le parcours des états et événements au sein de “SuggestionsScreen”.

Architecture de l'écran SuggestionScreen en Jetpack Compose, connecté à SuggestionsViewModel. L'écran comprend un Scaffold, Box, SuggestionsList, CategoryFilter, Suggestions, et FloatingActionButton.
Arbre de composition annoté des états et événement métier de l’écran “SuggestionsScreen”

Voici une vidéo qui récapitule les principales étapes pour gérer des états et des événements qui impliquent de la donnée métier.

À vous de jouer !

Contexte

Grâce à l’arbre de composition annoté des états et des événements impliquant de la donnée et de la logique métier, Charlie a pu modéliser clairement leurs parcours jusqu’au composant racinePodcastsScreen. Elle vous demande maintenant de modifier l’application pour refléter ce parcours. Voici la modélisation fournie par Charlie :

Diagramme montrant l'architecture d'une interface Compose avec PodcastsScreen, Box, LazyColumn, PodcastSearchField, PodcastItem, et FloatingActionButton.
Parcours des états et événement métier au sein de l’écran “PodcastsScreen”

Consignes

  1. Créez une classePodcastsViewModelde typeViewModelcontenant une variablepodcastsInDatabasecorrespondant à la liste des podcasts fournis parPodcastFactory.makePodcasts(). Modélisez les états métiers identifiés dans cet écran au sein d’une data class que vous pouvez nommer par exemplePodcastsUiState.

  2. Au sein de la classePodcastsViewModel, exposez unStateFlowcontenant un objet de typePodcastsUiState. Celui-ci sera initialisé à partir de notrePodcastsFactory, et avec une chaîne de caractère vide pour le champ de recherche.

  3. AjoutezPodcastsViewModelen paramètre du composantPodcastsScreen.

  4. Modifiez le composantPodcastSearchFieldafin de remonter dans ses paramètres les états et événements métiers que Charlie a identifiés.

  5. Au sein du composantPodcastsScreen, collectez leuiStatedepuis leViewModel, puis fournissez les données métier dont chaque composant enfant a besoin. (N’oubliez pas d’ajouter la dépendanceandroidx.lifecycle:lifecycle-runtime-composepour pouvoir collecter en tenant compte du cycle de vie de votre écran).

  6. Créez une fonctiononSearchValueChangedau sein duViewModelprenant en paramètre la saisie du champ de recherche, et implémentez la logique métier associée à cette fonctionnalité. Mettez à jour les états métiers en conséquence.

  7. Appelez la fonctiononSearchValueChangedduViewModelà l’endroit adéquat dans le composantPodcastsScreen.

  8. Faites de même avec l’événementonDownloadClicked. Vous pouvez utiliser le code suivant dans votreViewModelpour simuler le téléchargement du podcast, en attendant de se brancher au repository.

fun onDownloadClicked(podcastId: String) {
viewModelScope.launch(Dispatchers.IO) {
    podcastsInDatabase = podcastsInDatabase.map { podcast ->
        if (podcast.id == podcastId) {
            podcast.copy(downloadStatus = DownloadStatus.InProgress)
        } else {
            podcast
        }
    }
    _uiState.value = _uiState.value.copy(
        podcasts = podcastsInDatabase
    )
    delay(2000)

    podcastsInDatabase = podcastsInDatabase.map { podcast ->
        if (podcast.id == podcastId) {
            podcast.copy(downloadStatus = DownloadStatus.Downloaded)
        } else {
            podcast
        }
    }
    _uiState.value = _uiState.value.copy(
        podcasts = podcastsInDatabase
    )
}

Livrables

Votre projet sur Android Studio doit être conforme aux consignes ci-dessus.

En résumé

  • Pour remonter les états et événements associés aux données métier jusqu’au composant racine, il faut les passer dans les paramètres des composants enfants.

  • Le composant racine récupère les données métier depuis le ViewModel associé.

  • Il faut ajouter un paramètre optionnel de typeViewModelau composant racine via la fonctionviewModelpour accéder aux états et événements métiers.

  • Des fonctions commecollectAsStateWithLifecycle()etobserveAsStateWithLifecycle()permettent de convertir des états observables en états compatibles avec Jetpack Compose, tout en tenant compte du cycle de vie du composant pour assurer une mise à jour optimale de l'interface utilisateur.

  • Accéder au ViewModel uniquement depuis le composant racine simplifie la gestion du cycle de vie, réduit le couplage et améliore les performances.

Maintenant que nous gérons correctement nos états et nos événements impliquant de la logique métier, voyons comment gérer nos états et événements purement visuels.

Exemple de certificat de réussite
Exemple de certificat de réussite