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, laRow
parente occupe exactement la largeur et la hauteur nécessaires pour afficher le texte “Mes podcasts” et l'icône associée.
Comment faire pour que laRow
parente occupe tout l’espace disponible en largeur, soit l’équivalent de la valeurmatch_parent
dans 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é.
Il existe d'autresModifiers
qui 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 typeRow
ouColumn
. 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 cesModifiers
pour 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.
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 composantCategoryFilter
que nous avons illustré dans “Organisez vos composants avec les layouts de base”. Rappelez-vous, ce composant utilise un layout de typeRow
pour 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.
Pour corriger cela, il suffit de rendre défilable horizontalement le layoutRow
parent en appliquant leModifier.horizontalScroll(state: ScrollState)
. Le code du composantCategoryFilter
devient 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 typeScrollState
que 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
,Column
etBox
disposent 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.
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ètremodifier
optionnel 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 composantOtherPodcastItem
utilisé 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.
Ce design a les particularités suivantes :
La couleur de fond doit être
primaryContainer
pour la suggestion de gauche, ettertiaryContainer
pour 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
.
En l’état, le code associé ne nous permet pas d’obtenir le rendu que nous souhaitons car :
La largeur fixe de
200.dp
ne 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 :
Ajouter un paramètre
modifier
optionnel dans la signature du composantOtherPodcastItem
.Appliquer ce paramètre au premier conteneur du composant.
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 composantOtherPodcastItem
comme 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é à chaqueOtherPodcastItem
permet d’affecter un poids de 1 à chaque composant, les faisant occuper chacun la moitié de l’écran en largeur.Le modifier
shadow
ajoute un effet d’élévation.Le modifier
clickable
rend cliquable le composant en entier.
Le modifier
fillMaxHeight()
appliqué sur chaque OtherPodcastItem en combinaison avec le modifier.height(IntrinsicSize.Max`)
sur laRow
indique à Jetpack Compose que la hauteur des deuxOtherPodcastItem
sera 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 :
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 composant
PodcastItem
et l'ensemble des composants que vous avez développés jusqu'à présent en appliquant ces nouvelles connaissances ;concevoir un nouveau composant
Suggestion
correspondant à 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
.
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 typeModifier
dans leur signature et à l’appliquer au premier conteneur.
2. Avant de vous lancer tête baissée dans l’implémentation du composantSuggestion
illustré 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.xml
pour 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 layouts
Row
,Column
etBox
enveloppent leurs enfants par défaut.Les modifiers
fillMaxWidth()
,fillMaxHeight()
,fillMaxSize()
,size()
,weight()
, ou encoreaspectRatio()
permettent d’ajuster la taille d’un composant ou d’un layout.Les layouts
Row
,Column
etBox
incluent une méthodeModifier.align()
qui permet de personnaliser individuellement l'alignement de leurs éléments enfants.Exposer un paramètre
modifier
optionnel 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.