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 commeStateFlow
ouLiveData
. 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é.
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 typeSuggestionsViewModel
au composant racineSuggestionsScreen
:
@Composable
def SuggestionsScreen(
modifier: Modifier = Modifier,
viewModel: SuggestionsViewModel = viewModel(),
) {
/*...*/
}
2. En parallèle, créer la classeSuggestionsViewModel
qui é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 lesStateFlow
etLiveData
. 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 ?
SuggestionsViewModel
utilise unStateFlow
pour récupérer l’état nomméuiState
depuis 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 duViewModel
qui 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”.
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 :
Consignes
Créez une classe
PodcastsViewModel
de typeViewModel
contenant une variablepodcastsInDatabase
correspondant à 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
.Au sein de la classe
PodcastsViewModel
, exposez unStateFlow
contenant 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.Ajoutez
PodcastsViewModel
en paramètre du composantPodcastsScreen
.Modifiez le composant
PodcastSearchField
afin de remonter dans ses paramètres les états et événements métiers que Charlie a identifiés.Au sein du composant
PodcastsScreen
, collectez leuiState
depuis 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-compose
pour pouvoir collecter en tenant compte du cycle de vie de votre écran).Créez une fonction
onSearchValueChanged
au sein duViewModel
prenant 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.Appelez la fonction
onSearchValueChanged
duViewModel
à l’endroit adéquat dans le composantPodcastsScreen
.Faites de même avec l’événement
onDownloadClicked
. Vous pouvez utiliser le code suivant dans votreViewModel
pour 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 type
ViewModel
au composant racine via la fonctionviewModel
pour accéder aux états et événements métiers.Des fonctions comme
collectAsStateWithLifecycle()
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.