Créer des mises en page adaptatives

L'interface utilisateur (UI) de votre application doit s'adapter à différentes tailles d'écran, différentes orientations et différents facteurs de forme. Une mise en page adaptative change en fonction de l'espace disponible sur l'écran. Ces modifications vont d'un simple ajustement de la mise en page afin de remplir l'espace à un changement complet de la mise en page afin d'utiliser encore plus d'espace.

En tant que kit d'interface utilisateur déclaratif, Jetpack Compose convient à la conception et à la mise en œuvre de mises en page qui s'ajustent pour afficher le contenu différemment en fonction de tailles variées. Ce document explique comment utiliser Compose pour rendre l'UI responsive.

Rendre explicites les changements de mise en page importants pour les composables au niveau de l'écran

Lorsque vous utilisez Compose pour mettre en page une application entière, les composables au niveau de l'application et de l'écran occupent tout l'espace qui est alloué à cette application pour afficher l'interface. À ce niveau de la conception, il peut être judicieux de modifier la mise en page globale d'un écran afin d'exploiter les grands écrans.

Évitez d'utiliser des valeurs physiques et matérielles pour prendre des décisions concernant la mise en page. Il peut être tentant de prendre des décisions sur la base d'une valeur tangible fixe (l'appareil est-il une tablette ? L'écran physique présente-t-il un certain format ?), mais les réponses à ces questions ne permettent pas forcément de déterminer l'espace avec lequel votre UI peut fonctionner.

Schéma illustrant plusieurs facteurs de forme : un téléphone, un pliable, une tablette et un ordinateur portable

Sur les tablettes, une application peut s'exécuter en mode multifenêtre, ce qui signifie qu'elle partage l'écran avec une autre application. Sous ChromeOS, une application peut se trouver dans une fenêtre redimensionnable. Il peut même y avoir plusieurs écrans physiques, par exemple avec les pliables. Dans tous ces cas de figure, la taille physique de l'écran n'a pas d'importance pour déterminer comment afficher le contenu.

Vous devez plutôt prendre des décisions basées sur la partie de l'écran qui est réellement allouée à votre application, telles que les métriques de fenêtre actuelles fournies par la bibliothèque WindowManager de Jetpack. Pour découvrir comment utiliser WindowManager dans une application Compose, consultez l'exemple JetNews.

Cette approche permet à votre application d'être plus flexible, car son comportement sera approprié dans tous les scénarios ci-dessus. Le fait de rendre vos mises en page adaptables à l'espace d'écran disponible réduit également le nombre de manipulations spéciales nécessaires pour prendre en charge des plates-formes telles que ChromeOS, ainsi que les facteurs de forme tels que les tablettes et les pliables.

Une fois que vous avez évalué l'espace approprié disponible pour votre application, il est utile de convertir la taille brute en une classe de taille significative, comme décrit dans la section Classes de taille de fenêtre. Les tailles sont alors regroupées dans des buckets standards, qui sont des points d'arrêt conçus pour trouver un juste milieu entre simplicité et flexibilité dans le but d'optimiser votre application pour la plupart des cas uniques. Ces classes de taille font référence à la fenêtre globale de votre application. Par conséquent, utilisez-les pour les décisions qui affectent la mise en page globale de votre écran. Vous pouvez transmettre ces classes de taille en tant qu'état ou appliquer une logique supplémentaire pour créer un état dérivé à transmettre aux composables imbriqués.

class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            val windowSizeClass = calculateWindowSizeClass(this)
            MyApp(windowSizeClass)
        }
    }
}
@Composable
fun MyApp(windowSizeClass: WindowSizeClass) {
    // Perform logic on the size class to decide whether to show
    // the top app bar.
    val showTopAppBar = windowSizeClass.heightSizeClass != WindowHeightSizeClass.Compact

    // MyScreen knows nothing about window sizes, and performs logic
    // based on a Boolean flag.
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

Cette approche à plusieurs niveaux confine la logique de taille d'écran à un seul emplacement, au lieu de la disperser dans votre application à de nombreux endroits qui doivent être synchronisés. Cet emplacement unique génère un état, qui peut être explicitement transmis à d'autres composables, comme pour tout autre état de l'application. La transmission explicite de l'état simplifie les composables individuels, car ils sont simplement traités comme des fonctions modulables standards qui utilisent la classe de taille ou la configuration spécifiée, ainsi que d'autres données.

Les composables imbriqués flexibles sont réutilisables

Les composables qui peuvent être placés à divers endroits sont plus facilement réutilisables. S'ils sont destinés à un certain emplacement et à une taille spécifique, il est plus difficile de les réutiliser ailleurs ou avec un autre volume d'espace disponible. Cela signifie également que les composables individuels et réutilisables doivent éviter implicitement de dépendre des informations de taille "globales".

Prenons un exemple : imaginez un composable imbriqué qui implémente une mise en page liste/détail, qui peut afficher un seul volet ou deux volets côte à côte.

Capture d'écran d'une application montrant deux volets côte à côte

Figure 1 : Capture d'écran d'une application montrant une mise en page classique de liste/détails. 1 est la zone de liste et 2 est la zone de détail.

Cette décision doit faire partie de la mise en page globale de l'application. C'est pourquoi elle est transmise à partir d'un composable au niveau de l'écran, comme nous l'avons vu plus haut :

@Composable
fun AdaptivePane(
    showOnePane: Boolean,
    /* ... */
) {
    if (showOnePane) {
        OnePane(/* ... */)
    } else {
        TwoPane(/* ... */)
    }
}

Et si nous préférons un composable qui modifie sa mise en page indépendamment en fonction de l'espace disponible ? Par exemple, une fiche qui présente plus de détails si l'espace le permet. Une logique basée sur les tailles disponibles doit être appliquée, mais quelles tailles exactement ?

Exemples de deux fiches différentes : une fiche étroite ne comportant qu'une icône et un titre, et une fiche plus large montrant l'icône, le titre et une brève description

Comme nous l'avons vu ci-dessus, il est préférable de ne pas se baser sur la taille de l'écran de l'appareil. Cette information n'est pas précise pour plusieurs écrans. Elle ne l'est pas non plus si l'application n'est pas en plein écran.

Comme le composable n'est pas un composable au niveau de l'écran, les métriques de la fenêtre actuelles ne peuvent pas non plus être utilisées afin de maximiser la réutilisation. Si le composant est placé avec une marge intérieure (par exemple, pour des encarts) ou s'il existe des composants tels que des rails de navigation ou des barres d'application, l'espace disponible pour le composable peut différer considérablement de l'espace global disponible pour l'application.

Par conséquent, nous devons tenir compte de la largeur donnée au composable pour qu'il s'affiche. Deux options s'offrent à nous pour obtenir cette largeur :

Si vous souhaitez modifier l'emplacement ou le mode d'affichage du contenu, vous pouvez utiliser une collection de modificateurs ou une mise en page personnalisée pour que la mise en page soit responsive. Vous pouvez par exemple demander à un enfant de remplir tout l'espace disponible ou disposer plusieurs éléments enfants avec plusieurs colonnes s'il y a assez d'espace.

Si vous souhaitez modifier ce qui s'affiche, vous pouvez utiliser BoxWithConstraints comme alternative plus puissante. Ce composable fournit des contraintes de mesure que vous pouvez utiliser pour appeler différents composables en fonction de l'espace disponible. Toutefois, cela entraîne des frais, car BoxWithConstraints retarde la composition jusqu'à la phase de mise en page, lorsque ces contraintes sont connues. Cela implique donc davantage de travail pour la mise en page.

@Composable
fun Card(/* ... */) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(/* ... */)
                Title(/* ... */)
            }
        } else {
            Row {
                Column {
                    Title(/* ... */)
                    Description(/* ... */)
                }
                Image(/* ... */)
            }
        }
    }
}

S'assurer que toutes les données sont compatibles avec différentes tailles

Lorsque vous exploitez l'espace supplémentaire disponible à l'écran, vous pouvez avoir l'espace nécessaire pour présenter plus de contenu à l'utilisateur sur un grand écran que sur un petit écran. Lorsque vous implémentez un composable avec ce comportement, il peut être tentant d'en profiter pour charger les données en tant qu'effet secondaire de la taille actuelle.

Cependant, cela va à l'encontre des principes du flux de données unidirectionnel, qui permet de hisser les données et de les fournir simplement aux composables pour qu'ils s'affichent de manière appropriée. Un volume suffisant de données doit être mis à disposition du composable afin qu'il dispose toujours de ce dont il a besoin pour s'afficher dans n'importe quelle taille, même si une partie des données n'est pas toujours utilisée.

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(description)
                }
                Image(imageUrl)
            }
        }
    }
}

En nous appuyant sur l'exemple de fiche (Card), notez que nous transmettons toujours la description à Card. Même si la description n'est utilisée que lorsque la largeur permet de l'afficher, Card l'exige toujours, quelle que soit la largeur disponible.

Le fait de toujours transmettre des données simplifie les mises en page adaptatives en les rendant moins dynamiques et évite de déclencher des effets secondaires lors du basculement entre les tailles (ce qui peut se produire en raison d'un redimensionnement de fenêtre, d'un changement d'orientation ou du pliage et du dépliage d'un appareil).

Ce principe permet également de préserver l'état entre des modifications de mise en page. En hissant des informations qui peuvent ne pas être utilisées dans toutes les tailles, nous pouvons conserver l'état de l'utilisateur à mesure que la taille de la mise en page change. Par exemple, nous pouvons hisser un indicateur booléen showMore afin de préserver l'état de l'utilisateur lorsque les redimensionnements entraînent le masquage ou l'affichage de la description par la mise en page :

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    var showMore by remember { mutableStateOf(false) }

    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(
                        description = description,
                        showMore = showMore,
                        onShowMoreToggled = { newValue ->
                            showMore = newValue
                        }
                    )
                }
                Image(imageUrl)
            }
        }
    }
}

En savoir plus

Pour en savoir plus sur les mises en page personnalisées dans Compose, consultez les ressources supplémentaires suivantes.

Exemples d'applications

  • Les mises en page standards sur grand écran constituent un référentiel de modèles de conception éprouvés qui offrent une expérience utilisateur optimale sur les appareils à grand écran.
  • JetNews montre comment concevoir une application qui adapte son UI pour utiliser l'espace disponible.
  • Répondre est un exemple adaptatif conçu pour les mobiles, les tablettes et les pliables.
  • Now in Android est une application qui utilise les mises en page adaptatives pour prendre en charge différentes tailles d'écran.

Vidéos