• 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 les effets de bords et naviguez entre plusieurs écrans

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

CommentSideEffectpeut s'appliquer à notre application BestPodcast ?

Le gestionnaire d’effetsSideEffectpeut ê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 utiliserSideEffectpour écrire un message dans les logs de l’application chaque fois queCategoryFilterest recomposé.

@Composable
fun CategoryFilter(/* parameters */) {
    SideEffect {
        Log.d(“BestPodcast”,"CategoryFilter recomposé")
    }
    /*...*/
}

CommentDisposableEffectpeut s'appliquer à notre application BestPodcast ?

DisposableEffectpeut être utilisé dans l’écranSuggestionsScreenpour :

  • démarrer l’observation du cycle de vie lorsque l'écran s’affiche pour la première fois en utilisant l’objetLocalLifecycleOwner.currentet 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’écranSuggestionsViewModeldispose de deux fonctionsstartTimeretstopTimerqui permettent de gérer, au niveau adéquat, le calcul de la durée. Nous pouvons alors modifier le code de l’écranSuggestionsScreencomme 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 fonctionDisposableEffectdans ce code :

  • Elle a un paramètrekey1qui 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’estUnitqui 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 fonctiononDisposespé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’effetSideEffectetDisposableEffect.

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ékey1change. La coroutine est automatiquement annulée lorsque le composant est retiré de l'écran.

  • rememberCoroutineScope: Cette fonction fournit unCoroutineScopelié 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.

CommentLaunchedEffectpeut s'appliquer à notre application BestPodcast ?

Nous pouvons utiliserLaunchedEffectpour 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.

CommentrememberCoroutineScopepeut s'appliquer à notre application BestPodcast ?

Dans le chapitre "Concevez une liste", nous avons utilisérememberCoroutineScopepour 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 :

  1. La fonctionrememberNavControllerqui fournit une instance de typeNavController. Elle permet de gérer la navigation entre différents écrans.

  2. Le composantNavHostqui 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 composantNavHostest le point central pour implémenter la navigation. Il prend en premier paramètre un objet de typeNavControllerobtenu 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ù larouteest l’identifiant de l’écran, et à l’intérieur de cette fonction le composant correspondant à cet écran.

Comment utiliserNavHostau 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 :

  • NavHostconfigure la navigation avec deux destinations :suggestionsetsuggestions/{itemId}.

  • LeNavControllergère les transitions entre ces écrans en fonction des routes définies.

  • Lorsque l'utilisateur clique sur un podcast, c’est la fonctionnavController.navigate("suggestions/${podcast.id}")qui sera appelée, en spécifiant l'identifiant du podcast dans la routesuggestions/{itemId}.

  • DansSuggestionDetailsScreen,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ètreactionsdu composantTopAppBarqui 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 unNavHostet la logique permettant de naviguer entre l’écran "PodcastsScreen" et "SuggestionsScreen".

7. Modifiez le composantPodcastsScreenafin 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 commeSideEffectouDisposableEffect.

  • Certaines fonctions fournies par Jetpack Compose permettent de gérer des opérations asynchrones commeLaunchedEffectourememberCoroutineScope.

  • Pour naviguer au sein d’un écran utilisant Jetpack Compose, nous pouvons utiliser la fonctionrememberNavControlleret 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 !

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