Gérer les effets de bord et naviguer entre les écrans nécessitent une approche spécifique lorsque vous utilisez Jetpack Compose. Voyons ensemble comment intégrer efficacement ces opérations secondaires et comment configurer une navigation fluide pour une expérience utilisateur optimale.
Comprenez la notion d’effet de bord
Un effet de bord se réfère à toute action qui dépasse le cadre et le contrôle d’une fonction. Cela inclut des opérations telles que :
Lire ou écrire une variable globale ;
Accéder ou modifier une base de données ;
Manipuler un fichier ;
Effectuer des appels réseau.
Bien que ces opérations soient courantes dans les applications, les intégrer directement dans une fonction composable peut mener à des comportements imprévus. Cela est dû au cycle de vie particulier des composables, qui diffère de celui des fonctions traditionnelles.
Comment cela s’applique à notre application, BestPodcast ?
Pour que l’application BestPodcast fasse défiler automatiquement la liste des suggestions à chaque affichage de l’écran “SuggestionsScreen”, nous pourrions être tentés d’écrire ceci :
fun SuggestionsScreen(
modifier: Modifier = Modifier,
viewModel: SuggestionsViewModel = viewModel(),
) {
val uiState = viewModel.uiState.collectAsStateWithLifecycle()
Scaffold( /*... */) { innerPadding ->
Box( /*... */) {
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
val jumpToFirstPodcastVisible = rememberSaveable {
derivedStateOf { listState.firstVisibleItemIndex > 0 }
}
scope.launch {
listState.animateScrollToItem(0)
}
SuggestionsList(/*...*/)
if (jumpToFirstPodcastVisible.value) {
FloatingActionButton(/*...*/) {
/*...*/
}
}
}
}
}
Ici, l'appel àlistState.animateScrollToItem(0)
est lancé dès que la fonctionSuggestionsScreen
est recomposée. En raison du cycle de vie des fonctions composables, ce code peut être exécuté plusieurs fois. Cela entraîne alors des comportements imprévisibles comme des animations de défilement non désirées, ou des appels répétitifs lorsque l'état de la liste change.
Découvrez les gestionnaires d’effets de bord fournis par Jetpack Compose
Ces effets de bord ont besoin d’être gérés correctement. C’est pour cela que Jetpack Compose fournit plusieurs fonctions permettant d’exécuter des actions à des moments clés du cycle de vie d’une fonction composable, comme lors de son apparition initiale ou chaque fois que son état interne change. Il en existe bien d’autres mais nous allons seulement nous focaliser sur celles qui sont les plus fréquentes. Prenons le temps d’explorer ces fonctions les plus couramment utilisées.
Gérez un effet de bord synchrone
Ces effets sont ceux qui se produisent de manière immédiate. Le code attend la complétion de cet effet de bord avant de continuer. Pour les gérer, Jetpack Compose fournit plusieurs fonctions :
SideEffect
: Elle est utilisée pour déclencher des actions qui doivent se produire après chaque recomposition du composant. Cette fonction s'assure que l'effet de bord s'exécute après que tous les calculs de l'interface utilisateur soient terminés et que l'affichage soit à jour.DisposableEffect(key1: T)
: Elle permet d'exécuter des effets de bord qui nécessitent un nettoyage lorsque le composant quitte l’écran. Elle prend en paramètre une clé qui, lorsqu'elle change, provoque l'arrêt de l'effet actuel et le déclenchement d'un nouvel effet.
CommentSideEffect
peut s'appliquer à notre application BestPodcast ?
Le gestionnaire d’effetsSideEffect
peut être utilisé à des fins de débogage. Si vous soupçonnez un composant de générer trop de recompositions et que vous remarquez que cela engendre des problèmes de performance, eh bien c’est cette fonction qu’il faut implémenter dans votre code. Vous pourrez ainsi visualiser le nombre de fois qu'un composant est recomposé. Supposons que nous ayons des doutes sur les performances du composantFilterCategory
, nous pouvons alors utiliserSideEffect
pour écrire un message dans les logs de l’application chaque fois queCategoryFilter
est recomposé.
@Composable
fun CategoryFilter(/* parameters */) {
SideEffect {
Log.d(“BestPodcast”,"CategoryFilter recomposé")
}
/*...*/
}
CommentDisposableEffect
peut s'appliquer à notre application BestPodcast ?
DisposableEffect
peut être utilisé dans l’écranSuggestionsScreen
pour :
démarrer l’observation du cycle de vie lorsque l'écran s’affiche pour la première fois en utilisant l’objet
LocalLifecycleOwner.current
et sa fonctionaddObserver()
;arrêter d’observer le cycle de vie lorsque l’écran n’est plus à l’écran (pas même en background sur le téléphone de l’utilisateur).
Supposons que l’écranSuggestionsViewModel
dispose de deux fonctionsstartTimer
etstopTimer
qui permettent de gérer, au niveau adéquat, le calcul de la durée. Nous pouvons alors modifier le code de l’écranSuggestionsScreen
comme ceci :
@Composable
fun SuggestionsScreen(viewModel: SuggestionsViewModel = viewModel()) {
val lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
DisposableEffect( key1 = Unit ) {
// Observer le cycle de vie, et démarrer le compteur le compteur lorsque l'écran apparait
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
viewModel.startTimer()
} else if (event == Lifecycle.Event.ON_STOP) {
viewModel.stopTimer()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
Scaffold {
/*...*/
}
}
Analysons l’utilisation de la fonctionDisposableEffect
dans ce code :
Elle a un paramètre
key1
qui détermine la fréquence à laquelle l’effet est exécuté. Chaque fois que la valeur de ce paramètre change, l’effet est réexécuté. Dans notre exemple, c’estUnit
qui est utilisée comme valeur. Comme c’est une valeur constante et immuable, l’effet ne s’exécute qu’une seule fois, lors de la première composition du composant (c'est-à-dire la première fois qu'il est affiché).La fonction
onDispose
spécifie le code à exécuter lorsque le composant est retiré de l'affichage. C’est ici qu’il faut placer le code pour nettoyer les ressources ou arrêter des processus, ou comme dans notre exemple, arrêter d’observer le cycle de vie de l’application.
Cette vidéo résume l’utilisation des gestionnaires d’effetSideEffect
etDisposableEffect
.
Gérez un effet de bord asynchrone
Ces actions sont exécutées de manière asynchrone, permettant ainsi au programme de continuer son exécution pendant que l'effet de bord est toujours en cours.
Pour gérer ces effets de bord asynchrones, Jetpack Compose propose les fonctions suivantes :
LaunchedEffect(key1: T)
: Cette fonction exécute un effet au sein d’une coroutine. Il s’effectue lors de la première composition d'un composant et à chaque fois que la clékey1
change. La coroutine est automatiquement annulée lorsque le composant est retiré de l'écran.rememberCoroutineScope
: Cette fonction fournit unCoroutineScope
lié au cycle de vie du composant, permettant de lancer des coroutines à partir de fonctions composables. Il est pratique pour exécuter des tâches asynchrones déclenchées par des interactions utilisateur, comme des actions sur des boutons.
CommentLaunchedEffect
peut s'appliquer à notre application BestPodcast ?
Nous pouvons utiliserLaunchedEffect
pour faire défiler automatiquement la liste des suggestions à chaque fois que l’utilisateur la filtre selon une catégorie. Cela permet de garantir que l’utilisateur voit les premières suggestions correspondant à son action de filtre. Cela donne concrètement :
fun SuggestionsScreen(
modifier: Modifier = Modifier,
viewModel: SuggestionsViewModel = viewModel(),
) {
val uiState = viewModel.uiState.collectAsStateWithLifecycle()
Scaffold(
/*... */
) { innerPadding ->
Box( /*... */) {
/*... */
val listState = rememberLazyListState()
LaunchedEffect(uiState.podcasts.value) {
// Défiler automatiquement au début lors de l'affichage de l'écran
listState.animateScrollToItem(0)
}
SuggestionsList(/*...*/)
if (jumpToFirstPodcastVisible.value) {
FloatingActionButton(/*...*/) {
/*...*/
}
}
}
}
}
LaunchedEffect(uiState.podcasts.value)
permet ici de déclencher une animation de défilement à chaque fois que la liste des podcasts change.
Voici une vidéo qui récapitule les principales étapes pour gérer un effet de bord asynchrone avecLaunchedEffect
.
CommentrememberCoroutineScope
peut s'appliquer à notre application BestPodcast ?
Dans le chapitre "Concevez une liste", nous avons utilisérememberCoroutineScope
pour exécuter la fonctionsuspend animateScrollToItem()
au clic du bouton flottant. L’objectif était de scroller au début de la liste. Son utilisation donnait :
val coroutineScope = rememberCoroutineScope()
if (isScrollToFirstVisible.value) {
FloatingActionButton(
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 8.dp),
shape = CircleShape,
onClick = {
coroutineScope.launch {
listState.animateScrollToItem(0)
}
},
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
contentColor = MaterialTheme.colorScheme.onTertiaryContainer
) {
Icon(
Icons.AutoMirrored.Default.ArrowBack,
contentDescription = "Revenir au début de la liste",
)
}
}
Naviguez entre plusieurs écrans
Intégrez les notions de navigation propre à Jetpack Compose
La navigation n’est pas une fonctionnalité propre à Jetpack Compose. Pour gérer la navigation dans une application utilisant Jetpack Compose, il est alors recommandé d'utiliser une autre composante de l'écosystème Jetpack, nommée Jetpack Navigation. C’est la même logique de navigation que vous utilisiez peut-être déjà avec la version originale du UI Toolkit en XML. Dans ce cours, nous nous concentrerons uniquement sur la manière d'intégrer cette bibliothèque avec Jetpack Compose, sans entrer dans les détails complets de sa configuration et de son utilisation.
Avant de commencer, ajoutez la dépendanceandroidx.navigation:navigation-compose:<VERSION_A_COMPLETER>
dans votre projet. Ensuite, les deux choses à connaître pour gérer une navigation au sein d’un écran Jetpack Compose sont :
La fonction
rememberNavController
qui fournit une instance de typeNavController
. Elle permet de gérer la navigation entre différents écrans.Le composant
NavHost
qui définit les différentes destinations de votre application et la manière dont elles sont affichées.
Implémentez la navigation entre plusieurs écrans
Le composantNavHost
est le point central pour implémenter la navigation. Il prend en premier paramètre un objet de typeNavController
obtenu grâce à la fonctionrememberNavController
. Il prend en second paramètre le point de départ de la navigation (startDestination
). Puis, ce composant configure, en son sein, les différents écrans de l’application dans des fonctions nomméescomposable(route: String){}
où laroute
est l’identifiant de l’écran, et à l’intérieur de cette fonction le composant correspondant à cet écran.
Comment utiliserNavHost
au sein de notre application BestPodcast ?
Nous souhaitons d’un côté rendre une suggestion de podcast cliquable et de l’autre nous diriger vers un autre écran lors de l’événement de ce clic. Pour gérer la navigation entre ces deux écrans, nous pouvons intégrer unNavHost
à notre code :
@Composable
fun MainScreen() {
val navController = rememberNavController()
NavHost(navController, startDestination = "suggestions") {
composable("suggestions") {
SuggestionsScreen(
navigateToSuggestionDetails = {
navController.navigate("suggestions/${it}”)
} ,
)
}
composable("suggestions/{podcastId}") { backStackEntry ->
backStackEntry.arguments?.getString("podcastId")?.let { podcastId ->
SuggestionDetailsScreen(podcastId)
}
}
}
}
Analysons cet exemple :
NavHost
configure la navigation avec deux destinations :suggestions
etsuggestions/{itemId}
.Le
NavController
gère les transitions entre ces écrans en fonction des routes définies.Lorsque l'utilisateur clique sur un podcast, c’est la fonction
navController.navigate("suggestions/${podcast.id}")
qui sera appelée, en spécifiant l'identifiant du podcast dans la routesuggestions/{itemId}
.Dans
SuggestionDetailsScreen
,backStackEntry.arguments?.getString("podcastId")
extrait cet ID pour afficher les détails du podcast sélectionné. À partir de cet ID, il sera ensuite possible de récupérer à nouveau le podcast depuis la base de données, voire depuis un webservice, si par exemple cet écran de détail affiche d’autres informations que nous n’avons pas encore récupérées.
Voici une vidéo qui récapitule les principales étapes pour gérer la navigation entre écrans.
À vous de jouer
Contexte
Félicitations, c’est la dernière ligne droite pour l’application "BestPodcast"! Avant de la mettre en production, votre product owner vous a demandé d’effectuer quelques derniers ajustements sur l’écran “PodcastsScreen”. Vos objectifs sont clairs : finaliser l’application, améliorer l’expérience utilisateur et mesurer l’usage de l’application.
Consignes
1. Utilisez le gestionnaire d’effet adéquat pour mesurer la durée pendant laquelle l’utilisateur reste sur l’écranPodcastsScreen
. C’est Charlie qui s’occupera du code appelant votre outil d’analytics. En attendant de récupérer son travail, vous pouvez utiliser les fonctions suivantes dePodcastsViewModel
:
class PodcastsViewModel : ViewModel() {
/*...*/
fun startTimer(){
// TODO appeler ici l'outil d'analytics
Log.d("BestPodcast","START viewing PodcastsScreen")
}
fun stopTimer(){
// TODO appeler ici l'outil d'analytics
Log.d("BestPodcast","STOP viewing PodcastsScreen")
}
}
2. Utilisez le gestionnaire d’effet adéquat pour faire défiler automatiquement la liste des podcasts en première position dès que "PodcastsScreen" est affiché.
3. Intégrez un bouton icône représentant une ampoule dans la barre de navigation. Ce dernier doit permettre de naviguer vers l’écran “SuggestionsScreen” au sein du paramètreactions
du composantTopAppBar
qui contient actuellement le titre “Best Podcasts”.
4. Intégrez le code source des différents composants de l’écran "SuggestionsScreen" et son ViewModel associé.
5. N’hésitez pas à organiser correctement votre package contenant vos composants. Vous pouvez créer deux sous-packages intitulés "podcasts" et "suggestions".
6. Créez un composantMainScreen
, comportant unNavHost
et la logique permettant de naviguer entre l’écran "PodcastsScreen" et "SuggestionsScreen".
7. Modifiez le composantPodcastsScreen
afin d’effectuer l’événement de navigation au bon niveau de l’arbre de composition.
Livrables
Votre projet sur Android Studio doit être conforme aux consignes ci-dessus.
En résumé
Un effet de bord est une action dépassant le cadre et le contrôle d’une fonction.
Intégrer les effets de bord dans des fonctions composables sans utiliser les fonctions fournies par Jetpack Compose peut entraîner des comportements imprévisibles en raison de leur cycle de vie particulier.
Certaines fonctions fournies par Jetpack Compose permettent de gérer des opérations synchrones comme
SideEffect
ouDisposableEffect
.Certaines fonctions fournies par Jetpack Compose permettent de gérer des opérations asynchrones comme
LaunchedEffect
ourememberCoroutineScope
.Pour naviguer au sein d’un écran utilisant Jetpack Compose, nous pouvons utiliser la fonction
rememberNavController
et le composantNavHost
.
Ce cours vous donne des bases solides en Jetpack Compose. C’est un bel accomplissement ! Bravo ! Je vous encourage à continuer votre exploration en autonomie. N’hésitez pas à consulter le code source de Jetpack Compose dès que vous en avez l’occasion et de jeter un œil aux applications de démos développées par Google disponibles sur GitHub. En explorant ces exemples concrets, vous allez approfondir la compréhension de son fonctionnement. C’est aussi une très bonne source d’inspiration pour améliorer votre pratique. Continuez à expérimenter et à apprendre et vous verrez vos compétences progresser. Bonne continuation et à bientôt dans vos prochaines explorations en développement Android !