• 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

Optimisez l'organisation de vos composants pour des interfaces plus harmonieuses

Nous allons maintenant gérer individuellement la taille et la disposition d’un composant. Nous en profiterons aussi pour apprendre quelques bonnes pratiques pour organiser vos composants de manière autonome et efficace.

Modifiez la taille et l’alignement d’un composant

Modifiez la taille

Par défaut, les layouts standards enveloppent leurs enfants. Ils s'adaptent à la taille de leurs contenus enfants, ajustant leur hauteur et leur largeur en fonction des dimensions de ces derniers. Cela permet au conteneur de s'adapter parfaitement à son contenu, assurant ainsi que rien ne dépasse ni ne laisse d'espace inutilisé.

Illustrons cela avec un composant qui affiche simplement un titre “Mes Podcasts” et une icône dans un layout de typeRow. Cela donne le code ci-dessous :

@Preview
@Composable
fun DemoLayoutWrapByDefault() {
    Row {
        Text(
            text = "Mes Podcasts",
            style = MaterialTheme.typography.titleMedium,
        )
        Icon(
            Icons.Default.Podcasts,
            contentDescription = null,
        )
    }
}

Dans cet exemple, laRowparente occupe exactement la largeur et la hauteur nécessaires pour afficher le texte “Mes podcasts” et l'icône associée. 

On voit le rendu final quand la 'row' parente occupe exactement la largeur et la hauteur nécessaire pour afficher le texte “Mes podcasts” et l'icône associée.

Comment faire pour que laRowparente occupe tout l’espace disponible en largeur, soit l’équivalent de la valeurmatch_parentdans les systèmes de mise en page en XML ?

Pour atteindre cet objectif, nous allons devoir utiliser le ModifierfillMaxWidth()comme ceci : 

@Preview
@Composable
fun DemoLayout() {
    Row(modifier = Modifier.fillMaxWidth()) {
        Text(
            text = "Mes Podcasts",
            style = MaterialTheme.typography.titleMedium,
        )
        Icon(
            Icons.Default.Podcasts,
            contentDescription = null,
        )
    }
}

Voici le résultat associé.

On voit le résultat final de la `Row` qui occupe toute la largeur disponible.
Demonstration de la "Row" qui occupe toute la largeur disponible

Il existe d'autresModifiersqui influent sur la taille d'un composant. Voici une liste non exhaustive :

  • Modifier.fillMaxWidth(): pour occuper toute la largeur disponible. Il peut prendre en paramètre une fraction, pour déterminer la proportion de la largeur à occuper.

  • Modifier.fillMaxHeight() : pour occuper toute la hauteur disponible. Il peut prendre en paramètre une fraction, pour déterminer la proportion de la hauteur à occuper.

  • Modifier.fillMaxSize(): pour occuper toute la largeur et la hauteur disponibles.

  • Modifier.size(): pour définir une taille préférentielle.

  • Modifier.weight(): pour répartir la largeur ou la hauteur d'un composant proportionnellement au poids spécifié avec cette fonction.

  • Modifier.aspectRatio(): pour contraindre la largeur et la hauteur d'un composant selon un ratio spécifique, garantissant ainsi des proportions fixes indépendamment de l'espace disponible.

Notez que leModifier.weight()est utilisable uniquement au sein d’un layout de typeRowouColumn. Si vous essayez de l’utiliser au sein d’un autre layout, votre code ne compilera pas. Vous vous en rendrez vite compte !

Voici un exemple utilisant certains de cesModifierspour obtenir un rendu plus aéré que l’exemple précédent.

@Preview
@Composable
fun DemoLayout() {
   Row(
       modifier = Modifier.fillMaxWidth(),
       verticalAlignment = Alignment.CenterVertically
   ) {
       Text(
           text = "Mes Podcasts",
           style = MaterialTheme.typography.titleMedium,
           modifier = Modifier.weight(1f),
       )
       Icon(
           Icons.Default.Podcasts,
           contentDescription = null,
           modifier = Modifier.size(48.dp),
       )
   }
}

Et voici le résultat.

Résultat final du composant `DemoLayout` utilisant plusieurs modifier affectant sa taille.
Prévisualisation du composant "DemoLayout" utilisant plusieurs Modifier affectant sa taille

Modifiez le défilement

Grâce aux ModifiersModifier.horizontalScroll()etModifier.verticalScroll(), un composant devient défilable lorsque l'espace alloué sur l'écran est plus petit que la taille du contenu qu’il contient. 

Comment cela s’applique à notre application, BestPodcast ?

Eh bien justement nous en avons besoin pour le composantCategoryFilterque nous avons illustré dans “Organisez vos composants avec les layouts de base”. Rappelez-vous, ce composant utilise un layout de typeRowpour afficher plusieurs boutons permettant de filtrer la liste des podcasts par catégorie. Nous avons illustré son utilisation avec uniquement trois catégories et visuellement tout semblait correct. Cependant, si vous ajoutez plus d’éléments, comme dans la prévisualisation suivante, vous verrez que le rendu visuel n’est plus correct. Voici le code pour obtenir la prévisualisation.

@Preview
@Composable
fun CategoryFilterPreview() {
    BestPodcastTheme {
        val podcastCategories =
            listOf(
                Category(1, "Technologie"),
                Category(2, "Ecologie"),
                Category(3, "Santé"),
                Category(id = 4, "Sport"),
                Category(id = 5, "Education"),
            )
        val selected: MutableState<Int?> = remember { mutableStateOf(null) }
        CategoryFilter(
            categories = podcastCategories,
            onCategoryClicked = {
                if (selected.value == it) {
                    selected.value = null
                } else {
                    selected.value = it
                }
            },
            selectedId = selected.value,
        )
    }
}

Et voici le résultat.

On voit les trois composants Technologie, Écologie et Santé. Il y en a un autre sur le côté qu'on ne voit pas car il est mal configuré.
Prévisualisation du composant "CategoryFilter" avec plus de trois filtres

 Pour corriger cela, il suffit de rendre défilable horizontalement le layoutRowparent en appliquant leModifier.horizontalScroll(state: ScrollState). Le code du composantCategoryFilterdevient alors :

@Composable
fun CategoryFilter(
    categories: List<Category>,
    selectedId: Int?,
    onCategoryClicked: (Int) -> Unit,
) {
    val scrollState = rememberScrollState()
    Row(
        modifier =
            Modifier
                .fillMaxWidth()
                .selectableGroup()
                .horizontalScroll(scrollState),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
    ) {
        categories.forEach { category ->
	  /*...*/
        }
    }
}

Notez que le paramètre state de typeScrollStateque vous voyez ici permet de connaître la position de défilement et fournit des fonctions commeScrollState.animateScrollTo()etScrollState.scroll(). Pour le créer,rememberScrollState()doit être utilisé, mémorisant ainsi l’état de défilement du composant au-delà des recompositions. Et voici le rendu final.

Maintenant que vous en savez un peu plus sur lesModifiersà adopter pour mettre en page les composants de vos écrans, il est temps que je vous partage des bonnes pratiques pour vous faciliter la tâche lors de la conception et la maintenance de vos composants.

Modifiez l’alignement

Nous avons observé que les layoutsRow,ColumnetBoxdisposent d’un paramètre pour ajuster l’alignement de leurs enfants. Mais il est parfois nécessaire de personnaliser l'alignement de certains éléments unitairement. C’est ce que nous permet de faire leModifier.align()qui est accessible au sein de chacun de ces layouts.

Comment cela s’applique à notre application, BestPodcast ?

Chaque podcast a un titre et une catégorie associée. Pour pouvoir aligner uniquement le texte contenant la catégorie à droite, nous allons utiliser sur ce texte le modifierModifier.align()accessible au sein du layoutColumn, en lui appliquant la valeurAlignment.End. Cela donne le code ci-dessous :

@Composable
fun OtherPodcastItem(
    podcast: Podcast,
) {
    Column( 
       modifier = Modifier.width(200.dp),
       verticalArrangement = Arrangement.SpaceAround,
) {
        AsyncImage(
            model = podcast.logoUrl,
            contentDescription = null,
            placeholder = painterResource(id = R.drawable.placeholder),
            modifier = Modifier.fillMaxWidth().aspectRatio(1f)
        )
        Text(
            text = podcast.category.text,
            style = MaterialTheme.typography.labelLarge,
            fontStyle = FontStyle.Italic,
              modifier = Modifier.align(Alignment.End),
            color = MaterialTheme.colorScheme.tertiary,
        )
        Text(
            text = podcast.title,
            style = MaterialTheme.typography.titleMedium,
            color = MaterialTheme.colorScheme.primary,
        )
    }
}

Et voici le résultat final.

Rendu final d’un composant utilisant une colonne pour afficher des informations concernant un podcast. Le titre Industrie est aligné à droite et le titre Changements est aligné à gauche.
Prévisualisation d’un composant utilisant une colonne pour afficher des informations concernant un podcast

Ajoutez des modifiers optionnels pour réutiliser vos composants

Si nous ne pouvions pas concevoir des composants réutilisables dans une application, cela entraînerait plusieurs problèmes ! En tant que développeurs et développeuses, nous serions contraints de dupliquer les composants chaque fois qu’on en a besoin dans des contextes différents. Cela augmenterait considérablement notre charge de travail et le risque d'erreurs.

Alors pour éviter cela, la pratique la plus courante est d’exposer un paramètremodifieroptionnel parmi les paramètres des composants. 

Cela les rendra flexibles et réutilisables dans divers contextes et facilitera ainsi leur évolution future.

Comment cela s’applique à notre application, BestPodcast ?

Identifiez votre objectif cible

Rappelez-vous du composantOtherPodcastItemutilisé précédemment. Celui-ci donne des informations sur un podcast dans un contenu affiché dans un layout de typeColumn

Notre objectif est d’obtenir ce rendu visuel dans notre application Android.

Le rendu final montre deux colonnes pour présenter les suggestions du podcast. La première est Industrie Changements et la seconde est Technologie Le numérique vert.
Illustration de l’utilisation souhaitée du composant "OtherPodcastItem" dans le cadre de l’affichage de deux suggestions

Ce design a les particularités suivantes :

  • La couleur de fond doit êtreprimaryContainerpour la suggestion de gauche, ettertiaryContainerpour celle de droite. 

  • Les deux suggestions ont une ombre donnant l’impression d’être surélevées et elles sont cliquables.

  • Les deux suggestions occupent chacune la moitié de l’écran, en largeur. 

  • La hauteur des deux suggestions dépend de la plus grande des deux. Si la suggestion du podcast “Le numérique vert” a besoin de plus de place pour s’afficher sur deux lignes, alors la suggestion du podcast “Changements” doit faire la même hauteur.

Identifiez le problème de conception de votre composant initial

Pour rappel, voici maintenant le rendu et le code actuel de notre composantOtherPodcastItem.

Sans modifier particulier, le composant OtherPodcastItem n'est pas bien agencé. La taille des 2 icônes n'est pas identique.
Illustration du rendu actuel de l’utilisation du composant "OtherPodcastItem" sans l'utilisation d’un paramètre "Modifier"

En l’état, le code associé ne nous permet pas d’obtenir le rendu que nous souhaitons car :

  • La largeur fixe de200.dpne permet pas d’adapter le composant à la moitié de la largeur de l’écran.

  • Il n’y a pas d’ombre, le composant n’est pas cliquable et la couleur de fond ne peut pas être personnalisée.

@Composable
fun OtherPodcastItem(
    podcast: Podcast,
) {
    Column(
        verticalArrangement = Arrangement.SpaceAround,
        modifier = Modifier.width(200.dp)
) {
        AsyncImage(
            model = podcast.logoUrl,
            contentDescription = null,
            placeholder = painterResource(id = R.drawable.placeholder),
            modifier = Modifier.fillMaxWidth().aspectRatio(1f),
        )
        Text(
            text = stringResource(id = podcast.category,
            style = MaterialTheme.typography.labelLarge,
            fontStyle = FontStyle.Italic,
            modifier = Modifier.align(Alignment.End),
            color = MaterialTheme.colorScheme.tertiary,
        )
        Text(
            text = podcast.title,
            style = MaterialTheme.typography.titleMedium,
            color = MaterialTheme.colorScheme.primary,
        )
    }
}

Évitez d’inclure dans la signature de la fonction les paramètres de style du composant

Alors pourquoi ne pas passer tout simplement ces paramètres de style en paramètre de la fonctionOtherPodcastItem

Nous pourrions effectivement revoir la signature de notre fonction pour inclure ces critères comme ceci :

@Composable
fun OtherPodcastItem(
   podcast: Podcast,
   width: Dp,
   backgroundColor: Color,
   elevation: Dp,
   onClick: () -> Unit
) {
   /*...*/
}

Transformez votre composant pour le réutiliser facilement

Pour résoudre ce problème et faciliter les futures évolutions de notre composant, nous allons :

  1. Ajouter un paramètre modifieroptionnel dans la signature du composantOtherPodcastItem.

  2. Appliquer ce paramètre au premier conteneur du composant.

  3. Ne pas imposer de tailles fixes au layout parent du composant. Cela permet ainsi à notre composant de s’adapter au contexte où il est utilisé, et de se répartir selon l’espace dont il dispose. 

Voici la mise à jour du code de la signature de notre composant.

@Composable
fun OtherPodcastItem(
   podcast: Podcast,
   modifier: Modifier = Modifier,
) {
   Column(
      modifier = modifier,
      verticalArrangement = Arrangement.SpaceAround,
) {
     /*...*/
   }
}

Maintenant que sa signature est à jour, nous pouvons personnaliser le rendu du composantOtherPodcastItemcomme ce qui suit :

@Preview
@Composable
fun TwoSuggestionsRowPreview() {
    BestPodcastTheme {
        Row(
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            modifier = Modifier.padding(vertical = 8.dp).height(IntrinsicSize.Max),
        ) {
            OtherPodcastItem(
                podcast = PodcastFactory.makePodcasts()[0],
                modifier =
                    Modifier
                        .fillMaxHeight()
                        .weight(1f)
                        .shadow(4.dp)
                        .background(MaterialTheme.colorScheme.primaryContainer)
                        .padding(4.dp)
	           .clickable { /* TODO */ },
            )
            OtherPodcastItem(
                podcast = PodcastFactory.makePodcasts()[1],
                modifier =
                    Modifier
                        .fillMaxHeight()
                        .weight(1f)
                        .shadow(4.dp)
                        .background(MaterialTheme.colorScheme.tertiaryContainer)
                        .padding(4.dp)
                        .clickable { /* TODO */ },
            )
        }
    }
}

Quelques remarques intéressantes concernant cet extrait de code :

  • Le modifier.weight(1f)appliqué à chaqueOtherPodcastItempermet d’affecter un poids de 1 à chaque composant, les faisant occuper chacun la moitié de l’écran en largeur.

  • Le modifiershadowajoute un effet d’élévation.

  • Le modifierclickablerend cliquable le composant en entier.

  • Le modifierfillMaxHeight()appliqué sur chaque OtherPodcastItem en combinaison avec le modifier.height(IntrinsicSize.Max`)sur laRowindique à Jetpack Compose que la hauteur des deuxOtherPodcastItemsera définie par celui qui a la plus grande hauteur. Le podcast “Le numérique vert” passe sur deux lignes, la suggestion “Changements” sera aussi haute. Sans ce modifier, nous aurions le rendu suivant :

La zone pour le podcast Industrie Changements n'est pas alignée avec celle pour le podcast technologie Le numérique vert.
Illustration l’affichage de deux suggestions sans utiliser "IntrinsicSize" et "fillMaxHeight()"

Voici une vidéo qui récapitule les principales étapes pour optimiser l'organisation de vos composants pour des interfaces plus harmonieuses.

À vous de jouer !

Contexte

Vous venez de découvrir des notions fondamentales sur la conception de composants réutilisables. Il est temps maintenant pour vous :

  • de refactoriser le composantPodcastItemet l'ensemble des composants que vous avez développés jusqu'à présent en appliquant ces nouvelles connaissances ;

  • concevoir un nouveau composantSuggestioncorrespondant à la maquette que Mehdi, le graphiste de votre équipe, vous a fourni. Il a utilisé pour cela l’outil Excalidraw. Le composant sera utilisé plus tard dans l’applicationBestPodcast .

La maquette présente les principales mesures à) respecter : 16DP de marge à gauche avant le titre Le numérique vert. 60% de la largeur totale pour l'icône. 8DP de marge entre le titre Technologie et le contour du bas.
Maquette du composant "Suggestion"

Consignes

1. Refactorisez l’ensemble des composants conçus jusqu'à maintenant dans BestPodcast en appliquant la bonne pratique qui consiste à exposer un paramètre optionnel de typeModifierdans leur signature et à l’appliquer au premier conteneur.

2. Avant de vous lancer tête baissée dans l’implémentation du composantSuggestionillustré ci-dessus, prenez le temps d’identifier

  • Les états dont dépend le composant afin de définir la signature de sa fonction.

  • Les layouts à utiliser et leurs imbrications.

  • Les modifiers à appliquer.

Pour ce faire, vous pouvez, sur la base de ce que Mehdi vous a fourni, dessiner un simple croquis ou utiliser un outil numérique comme Excalidraw.

3. Développez le composantSuggestion. Vous aurez besoin de la ressourcesound_wave.xmlpour l’image représentant une onde sonore positionnée derrière le logo.

Livrables

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

En résumé

  • Les layoutsRow,ColumnetBox enveloppent leurs enfants par défaut.

  • Les modifiersfillMaxWidth(),fillMaxHeight(),fillMaxSize() ,size(),weight(), ou encoreaspectRatio()permettent d’ajuster la taille d’un composant ou d’un layout.

  • Les layoutsRow,ColumnetBox incluent une méthode Modifier.align()qui permet de personnaliser individuellement l'alignement de leurs éléments enfants.

  • Exposer un paramètremodifieroptionnel dans la signature de votre composant permet de le rendre personnalisable en termes de style et de disposition.

  • Pour permettre au composant de se répartir selon l'espace dont il dispose, il ne faut pas imposer de tailles fixes à son layout parent. 

Maintenant que nous avons soigneusement perfectionné la représentation d’un podcast, il est temps de les afficher tous ensemble dans une liste.

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