Principes de base de Jetpack Compose

1. Avant de commencer

Jetpack Compose est un kit d'outils moderne conçu pour simplifier le développement des interfaces utilisateur. Il allie un modèle de programmation réactif à la concision et à la facilité d'utilisation du langage de programmation Kotlin. Il est entièrement déclaratif, c'est-à-dire que vous décrivez votre interface utilisateur en appelant une série de fonctions qui transforment les données en hiérarchie d'interface utilisateur. Lorsque les données sous-jacentes sont modifiées, le framework réexécute automatiquement ces fonctions, mettant ainsi à jour la hiérarchie de l'interface utilisateur pour vous.

Une application Compose est constituée de fonctions composables, qui sont simplement des fonctions standards annotées avec @Composable et pouvant appeler d'autres fonctions composables. Vous n'avez besoin de rien d'autre qu'une fonction pour créer un élément d'UI. L'annotation indique à Compose d'ajouter une prise en charge spéciale pour la fonction afin de mettre à jour et de gérer l'interface utilisateur au fil du temps. Compose vous permet de structurer votre code en petits fragments. Les fonctions composables sont souvent appelées "composables".

Créer de petits composables réutilisables permet de concevoir facilement une bibliothèque d'éléments d'UI pour votre application. Chaque composable est responsable d'une partie de l'écran et peut être modifié indépendamment.

Pour obtenir de l'aide tout au long de cet atelier de programmation, reportez-vous au code suivant :

Remarque : Ce code est basé sur Material 2, tandis que l'atelier de programmation a été mis à jour et se base sur Material 3. Certaines étapes seront donc différentes.

Conditions préalables

  • Connaître la syntaxe du langage Kotlin, y compris les lambdas

Objectifs de l'atelier

Cet atelier de programmation traite des points suivants :

  • Présentation de Compose
  • Création d'interfaces utilisateur avec Compose
  • Gestion de l'état dans les fonctions composables
  • Création d'une liste performante
  • Ajout d'animations
  • Application d'un style et d'un thème à une application

Vous allez créer une application avec un écran d'accueil ainsi qu'une liste d'éléments déroulants animés :

8d24a786bfe1a8f2.gif

Ce dont vous avez besoin

2. Démarrer un nouveau projet Compose

Pour démarrer un nouveau projet Compose, ouvrez Android Studio.

Si vous êtes dans la fenêtre Bienvenue dans Android Studio, cliquez sur Démarrer un nouveau projet Android Studio. Si vous avez déjà ouvert un projet Android Studio, sélectionnez File > New > New Project (Fichier > Nouveau > Nouveau projet) dans la barre de menu.

Pour un nouveau projet, sélectionnez Empty Activity (Activité vide) dans la liste des modèles disponibles.

d12472c6323de500.png

Cliquez sur Next (Suivant) et configurez votre projet selon la méthode habituelle. Appelez-le Basics Codelab. Veillez à sélectionner une minimumSdkVersion ayant au moins le niveau d'API 21, ce qui correspond au niveau d'API minimum accepté par Compose.

Lorsque vous sélectionnez le modèle Empty Activity, le code suivant est automatiquement généré dans le projet :

  • Le projet est déjà configuré afin d'utiliser Compose.
  • Le fichier AndroidManifest.xml est créé.
  • Les fichiers build.gradle.kts et app/build.gradle.kts contiennent les options et les dépendances nécessaires à Compose.

Une fois le projet synchronisé, ouvrez MainActivity.kt et vérifiez le code.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greeting("Android")
    }
}

Dans la section suivante, vous verrez comment fonctionne chaque méthode et comment les améliorer pour créer des mises en page réutilisables flexibles.

Solution de l'atelier de programmation

Le code nécessaire à la solution de cet atelier de programmation est disponible sur GitHub :

$ git clone https://github.com/android/codelab-android-compose

Vous pouvez également télécharger le dépôt sous forme de fichier ZIP :

Vous trouverez le code de la solution dans le projet BasicsCodelab. Nous vous recommandons de suivre l'atelier de programmation étape par étape, à votre rythme, et de consulter la solution si vous le jugez nécessaire. Au cours de cet atelier de programmation, vous découvrirez des extraits de code que vous devrez ajouter au projet.

3. Premiers pas avec Compose

Parcourez les différentes classes et méthodes associées à Compose qu'Android Studio a générées pour vous.

Fonctions composables

Une fonction composable est une fonction standard annotée avec @Composable. Cela permet à votre fonction d'appeler les autres fonctions @Composable qu'elle contient. Comme vous pouvez le constater, la fonction Greeting est annotée avec @Composable. Cette fonction génère une partie de la hiérarchie de l'interface utilisateur qui affiche l'entrée donnée, String. Text est une fonction composable fournie par la bibliothèque.

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

Compose dans une application Android

Avec Compose, Activity reste le point d'entrée d'une application Android. Dans notre projet, MainActivity est lancé lorsque l'utilisateur ouvre l'application (comme indiqué dans le fichier AndroidManifest.xml). Vous utilisez setContent pour définir votre mise en page, mais au lieu d'utiliser un fichier XML comme vous le feriez dans le système View traditionnel, vous appelez les fonctions composables qu'il contient.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                  modifier = Modifier.fillMaxSize(),
                  color = MaterialTheme.colorScheme.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

BasicsCodelabTheme permet de définir un style pour les fonctions composables. Pour en savoir plus à ce sujet, consultez la section Appliquer un thème à votre application. Pour voir comment le texte s'affiche à l'écran, vous pouvez exécuter l'application dans un émulateur ou sur un appareil, ou utiliser l'aperçu Android Studio.

Pour utiliser l'aperçu Android Studio, il vous suffit de marquer toute fonction composable sans paramètre ou toute fonction avec des paramètres par défaut à l'aide de l'annotation @Preview et de créer votre projet. Vous pouvez déjà voir une fonction Preview Composable dans le fichier MainActivity.kt. Vous pouvez afficher plusieurs aperçus dans un même fichier et leur attribuer chacun un nom.

@Preview(showBackground = true, name = "Text preview")
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greeting(name = "Android")
    }
}

fb011e374b98ccff.png

Il est possible que l'aperçu ne s'affiche pas si l'option Code eeacd000622ba9b.png est sélectionnée. Cliquez sur Split (Diviser) 7093def1e32785b2.png pour afficher l'aperçu.

4. Modifier l'UI

Commençons par définir une couleur d'arrière-plan différente pour Greeting. Pour ce faire, encapsulez le composable Text avec une Surface. Surface accepte une couleur. Utilisez MaterialTheme.colorScheme.primary.

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier
        )
    }
}

Les composants imbriqués dans Surface s'afficheront par-dessus cette couleur d'arrière-plan.

Vous pouvez voir les nouvelles modifications dans l'aperçu :

c88121ec49bde8c7.png

Vous avez peut-être raté un détail important : le texte est désormais blanc. Comment cela se fait-il ?

Nous n'avons pourtant rien fait. Les composants Material Design, tels que androidx.compose.material3.Surface, sont conçus pour vous faciliter la tâche en se chargeant des fonctionnalités courantes que vous souhaitez probablement ajouter dans votre application, comme sélectionner une couleur de texte appropriée. Nous qualifions Material Design de catégorique, car il décide de valeurs par défaut et de modèles communs à la plupart des applications qui sont pratiques. Les composants Material Design de Compose reposent sur d'autres composants de base (dans androidx.compose.foundation), qui sont également accessibles depuis les composants de votre application, si vous avez besoin de plus de flexibilité.

Dans ce cas, Surface comprend que, lorsque l'arrière-plan est défini sur la couleur primary, tout texte qui se trouve par-dessus doit utiliser la couleur onPrimary, qui est également définie dans le thème. Pour en savoir plus à ce sujet, consultez la section Appliquer un thème à votre application.

Modificateurs

La plupart des éléments d'UI de Compose, tels que Surface et Text, acceptent un paramètre modifier facultatif. Les modificateurs indiquent à un élément d'UI comment s'afficher ou se comporter dans sa mise en page parent. Vous avez peut-être déjà remarqué que le composable Greeting comporte déjà un modificateur par défaut, qui est ensuite transmis à Text.

Par exemple, le modificateur padding applique un espace autour de l'élément qu'il décore. Vous pouvez créer un modificateur de marge intérieure à l'aide de Modifier.padding(). Vous pouvez également ajouter plusieurs modificateurs en les enchaînant. Dans notre cas, nous pouvons ajouter le modificateur de marge intérieure au modificateur par défaut : modifier.padding(24.dp).

Ajoutez à présent une marge intérieure à votre Text à l'écran :

import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp
// ...

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier.padding(24.dp)
        )
    }
}

ef14f7c54ae7edf.png

Il existe des dizaines de modificateurs pour aligner, animer, mettre en page ou transformer des éléments, ou encore pour les rendre cliquables ou les faire défiler. Pour la liste complète, reportez-vous à la page Liste des modificateurs de Compose. Vous utiliserez certains d'entre eux dans les prochaines étapes.

5. Réutiliser des composables

Plus vous ajoutez de composants à l'interface utilisateur, plus vous créez de niveaux d'imbrication. Cela peut affecter la lisibilité lorsqu'une fonction devient très volumineuse. Créer de petits composants réutilisables permet de concevoir facilement une bibliothèque d'éléments d'UI pour votre application. Chaque composable est responsable d'une fraction de l'écran et peut être modifié indépendamment.

Nous vous recommandons d'inclure un paramètre "Modificateur" vide par défaut dans votre fonction. Transférez ce modificateur vers le premier composable que vous appelez dans votre fonction. De cette façon, le site appelant peut adapter les instructions de mise en page et les comportements en dehors de votre fonction.

Créez un composable appelé MyApp qui inclut le message d'accueil.

@Composable
fun MyApp(modifier: Modifier = Modifier) {
    Surface(
        modifier = modifier,
        color = MaterialTheme.colorScheme.background
    ) {
        Greeting("Android")
    }
}

Cela vous permet de nettoyer le rappel onCreate et l'aperçu, car vous pouvez désormais réutiliser le composable MyApp, ce qui vous évite de dupliquer du code.

Dans l'aperçu, appelez MyApp et supprimez le nom de l'aperçu.

Votre fichier MainActivity.kt devrait se présenter comme suit :

package com.example.basicscodelab

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.basicscodelab.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {
    Surface(
        modifier = modifier,
        color = MaterialTheme.colorScheme.background
    ) {
        Greeting("Android")
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier.padding(24.dp)
        )
    }
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        MyApp()
    }
}

6. Créer des lignes et des colonnes

Dans Compose, les trois éléments de mise en page standards de base sont Column, Row et Box.

518dbfad23ee1b05.png

Ce sont des fonctions composables qui requièrent des contenus composables pour que vous puissiez placer des éléments à l'intérieur. Par exemple, chaque enfant à l'intérieur d'une Column sera placé verticalement.

// Don't copy over
Column {
    Text("First row")
    Text("Second row")
}

Essayez à présent de modifier Greeting afin que le message d'accueil affiche une colonne avec deux éléments de texte, comme dans cet exemple :

bf27ee688c3231df.png

Notez que vous devrez peut-être déplacer la marge intérieure.

Comparez votre résultat avec cette solution :

import androidx.compose.foundation.layout.Column
// ...

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Column(modifier = modifier.padding(24.dp)) {
            Text(text = "Hello ")
            Text(text = name)
        }
    }
}

Compose et Kotlin

Les fonctions composables peuvent être utilisées comme n'importe quelle autre fonction dans Kotlin. Il est ainsi très facile de créer des interfaces utilisateur, puisque vous pouvez ajouter des instructions pour indiquer comment l'UI doit s'afficher.

Par exemple, vous pouvez utiliser une boucle for pour ajouter des éléments à Column :

@Composable
fun MyApp(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

a7ba2a8cb7a7d79d.png

Vous n'avez pas encore défini de dimensions ni ajouté de contraintes à la taille de vos composables. Par défaut, chaque ligne, de même que l'aperçu, occupe donc un espace minimal. Modifions à présent notre aperçu pour émuler la largeur courante d'un petit téléphone (320 dp). Ajoutez un paramètre widthDp à l'annotation @Preview :

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        MyApp()
    }
}

a5d5f6cdbdd918a2.png

Les modificateurs sont fréquemment utilisés dans Compose. Nous allons donc nous entraîner avec un exercice plus avancé : essayez de répliquer la mise en page suivante en utilisant les modificateurs fillMaxWidth et padding.

a9599061cf49a214.png

À présent, comparez votre code à celui de la solution :

import androidx.compose.foundation.layout.fillMaxWidth

@Composable
fun MyApp(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier = modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Column(modifier = Modifier.fillMaxWidth().padding(24.dp)) {
            Text(text = "Hello ")
            Text(text = name)
        }
    }
}

Gardez à l'esprit les points suivants :

  • Les modificateurs acceptent d'être "surchargés" pour que vous puissiez, par exemple, spécifier différentes manières de créer une marge intérieure.
  • Pour ajouter plusieurs modificateurs à un élément, il vous suffit de les enchaîner.

Il existe plusieurs façons d'y parvenir. Par conséquent, si votre code ne correspond pas à cet extrait, cela ne signifie pas qu'il est incorrect. Cependant, copiez et collez ce code pour poursuivre l'atelier de programmation.

Ajouter un bouton

Dans cette étape vous allez ajouter un élément cliquable qui développe Greeting. Nous devons donc commencer par ajouter ce bouton. L'objectif est de créer la mise en page suivante :

ff2d8c3c1349a891.png

Button est un composable fourni par le package Material 3 qui nécessite un composable comme dernier argument. Comme les lambdas de fin peuvent être placés en dehors des parenthèses, vous pouvez ajouter n'importe quel contenu au bouton en tant qu'enfant. Par exemple, un Text :

// Don't copy yet
Button(
    onClick = { } // You'll learn about this callback later
) {
    Text("Show less")
}

Pour cela, vous devez apprendre à placer un composable à la fin d'une ligne. Comme il n'y a pas de modificateur alignEnd, vous devez commencer par attribuer une weight au composable. Le modificateur weight permet à l'élément de remplir tout l'espace disponible. Il est donc flexible et éloigne les autres éléments qui ne sont pas pondérés (et qui sont donc dits inflexibles). Cela rend également le modificateur fillMaxWidth redondant.

À présent, essayez d'ajouter le bouton et de le placer comme illustré dans l'image précédente.

Voici la solution :

import androidx.compose.foundation.layout.Row
import androidx.compose.material3.ElevatedButton
// ...

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { /* TODO */ }
            ) {
                Text("Show more")
            }
        }
    }
}

7. Gérer les états dans Compose

Dans cette section, vous allez ajouter une interaction à votre écran. Jusqu'à présent, vous avez créé des mises en page statiques. Maintenant, vous allez les faire réagir aux modifications apportées par les utilisateurs pour obtenir ce résultat :

6675d41779cac69.gif

Avant d'apprendre à rendre un bouton cliquable et à redimensionner un élément, vous devez stocker quelque part une valeur qui indique si chaque élément est développé ou non, c'est-à-dire son état. Étant donné que nous avons besoin d'une valeur par message d'accueil, l'emplacement logique pour la stocker est le composable Greeting. Voyez comment la valeur booléenne expanded est utilisée dans le code :

// Don't copy over
@Composable
fun Greeting(name: String) {
    var expanded = false // Don't do this!

    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

Notez que nous avons également ajouté une action onClick et un texte de bouton dynamique. Nous y reviendrons plus tard.

Cependant, cela ne fonctionnera pas comme prévu. Définir une valeur différente pour la variable expanded ne permettra pas à Compose de la détecter comme un changement d'état. Il ne va donc rien se passer.

Si la modification de cette variable ne déclenche pas de recompositions, c'est parce qu'elle n'est pas suivie par Compose. De plus, chaque fois que Greeting est appelé, la variable est réinitialisée à la valeur "false".

Pour ajouter un état interne à un composable, vous pouvez utiliser la fonction mutableStateOf, qui permet à Compose de recomposer des fonctions qui lisent ce State.

import androidx.compose.runtime.mutableStateOf
// ...

// Don't copy over
@Composable
fun Greeting() {
    val expanded = mutableStateOf(false) // Don't do this!
}

Cependant, vous ne pouvez pas vous contenter d'attribuer mutableStateOf à une variable à l'intérieur d'un composable. Comme expliqué précédemment, la recomposition peut se produire à tout moment, ce qui appelle à nouveau le composable, réinitialisant ainsi l'état à un nouvel état modifiable avec la valeur false.

Pour conserver l'état lors des recompositions, mémorisez l'état modifiable à l'aide de remember.

import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
// ...

@Composable
fun Greeting(...) {
    val expanded = remember { mutableStateOf(false) }
    // ...
}

remember permet de se prémunir contre la recomposition et donc de ne pas réinitialiser l'état.

Notez que si vous appelez le même composable depuis différentes parties de l'écran, vous créerez différents éléments d'UI, chacun avec sa propre version de l'état. Vous pouvez considérer l'état interne comme étant une variable privée dans une classe.

La fonction composable est automatiquement "abonnée" à l'état. Si l'état change, les composables qui lisent ces champs sont recomposés pour afficher les mises à jour.

Modifier un état et réagir aux changements d'état

Vous avez peut-être remarqué que, pour modifier l'état, Button avait un paramètre appelé onClick, qui n'accepte pas une valeur, mais une fonction.

Vous pouvez définir l'action à exécuter lors d'un clic en lui attribuant une expression lambda. Par exemple, basculons la valeur de l'état développé et affichons un texte différent en fonction de la valeur.

ElevatedButton(
    onClick = { expanded.value = !expanded.value },
) {
   Text(if (expanded.value) "Show less" else "Show more")
}

Exécutez l'application en mode interactif pour voir le comportement.

374998ad358bf8d6.png

Lorsque l'utilisateur clique sur le bouton, expanded bascule et déclenche la recomposition du texte à l'intérieur du bouton. Chaque Greeting possède son propre état développé, car il appartient à différents éléments d'UI.

93d839b53b7d9bea.gif

Voici le code jusqu'à présent :

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    val expanded = remember { mutableStateOf(false) }
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

Développer l'élément

À présent, développons un élément lorsque l'utilisateur clique dessus. Ajoutons une variable supplémentaire qui dépend de notre état :

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {

    val expanded = remember { mutableStateOf(false) }

    val extraPadding = if (expanded.value) 48.dp else 0.dp
// ...

Vous n'avez pas besoin de mémoriser extraPadding pour la recomposition, car il effectue un calcul simple.

Nous pouvons maintenant appliquer un nouveau modificateur de marge intérieure à la colonne :

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    val expanded = remember { mutableStateOf(false) }
    val extraPadding = if (expanded.value) 48.dp else 0.dp
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(
                modifier = Modifier
                    .weight(1f)
                    .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

Si vous exécutez l'application sur un émulateur ou en mode interactif, vous devriez constater que chaque élément peut être développé indépendamment :

6675d41779cac69.gif

8. Hisser un état

Dans les fonctions composables, un état lu ou modifié par plusieurs fonctions doit résider dans un ancêtre commun. Ce processus est appelé hissage d'état. Hisser signifie lever ou élever.

Rendre l'état hissable permet d'éviter les doublons et l'introduction de bugs, aide à réutiliser les composables, et facilite considérablement le test des composables. À l'inverse, les états qui n'ont pas besoin d'être contrôlés par le parent d'un composable ne doivent pas être hissés. La référence est l'entité qui crée et contrôle cet état.

À titre d'exemple, créons un écran d'accueil pour notre application.

5d5f44508fcfa779.png

Ajoutez le code suivant à MainActivity.kt :

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.material3.Button
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
// ...

@Composable
fun OnboardingScreen(modifier: Modifier = Modifier) {
    // TODO: This state should be hoisted
    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = { shouldShowOnboarding = false }
        ) {
            Text("Continue")
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen()
    }
}

Ce code contient de nombreuses fonctionnalités nouvelles :

  • Vous avez ajouté un nouveau composable appelé OnboardingScreen et un nouvel aperçu. Si vous compilez le projet, vous remarquerez que vous pouvez avoir plusieurs aperçus en même temps. Nous avons également ajouté une hauteur fixe pour vérifier que le contenu est correctement aligné.
  • Column peut être configuré pour afficher son contenu au centre de l'écran.
  • shouldShowOnboarding utilise un mot clé by au lieu de =. Ce délégué de propriété vous évite de saisir .value à chaque fois.
  • Lorsque vous cliquez sur le bouton, shouldShowOnboarding est défini sur false. Toutefois, vous ne lisez pas encore son état.

Nous pouvons à présent ajouter ce nouvel écran d'accueil à notre application. Nous voulons l'afficher au lancement, puis le masquer lorsque l'utilisateur appuie sur "Continuer".

Dans Compose, vous ne masquez pas les éléments d'UI. Au lieu de les masquer, il vous suffit de ne pas les ajouter à la composition pour qu'ils ne soient pas ajoutés à l'arborescence de l'UI générée par Compose. Pour ce faire, utilisez une logique Kotlin conditionnelle simple. Par exemple, pour afficher l'écran d'accueil ou la liste des messages d'accueil, utilisez le code suivant :

// Don't copy yet
@Composable
fun MyApp(modifier: Modifier = Modifier) {
    Surface(modifier) {
        if (shouldShowOnboarding) { // Where does this come from?
            OnboardingScreen()
        } else {
            Greetings()
        }
    }
}

Toutefois, nous n'avons pas accès à shouldShowOnboarding. Il est clair que nous devons partager l'état que nous avons créé dans OnboardingScreen avec le composable MyApp.

Au lieu de partager la valeur de l'état avec son parent, nous hissons l'état, c'est-à-dire que nous le déplaçons tout simplement vers l'ancêtre commun qui doit y accéder.

Tout d'abord, déplacez le contenu de MyApp dans un nouveau composable appelé Greetings : Adaptez également l'aperçu pour appeler la méthode Greetings à la place :

@Composable
fun MyApp(modifier: Modifier = Modifier) {
     Greetings()
}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier = modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingsPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

Ajoutez un aperçu de notre nouveau composable MyApp de premier niveau afin que nous puissions tester son comportement :

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

Ajoutez à présent la logique pour afficher les différents écrans dans MyApp, puis hissez l'état.

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(/* TODO */)
        } else {
            Greetings()
        }
    }
}

Nous devons également partager shouldShowOnboarding avec l'écran d'accueil. Cependant, nous n'allons pas le transmettre directement. Au lieu de laisser OnboardingScreen modifier notre état, il est préférable de lui demander de nous signaler quand l'utilisateur clique sur le bouton Continue (Continuer).

Comment transmettre des événements ? En transmettant des rappels. Les rappels sont des fonctions qui sont transmises en tant qu'arguments à d'autres fonctions et qui sont exécutées lorsque l'événement se produit.

Essayez d'ajouter un paramètre de fonction à l'écran d'accueil défini en tant que onContinueClicked: () -> Unit pour que vous puissiez modifier l'état MyApp.

Solution :

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier
                .padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }

}

En transmettant à OnboardingScreen une fonction plutôt qu'un état, il devient plus facile de réutiliser ce composable. De plus, cela protège l'état de toute modification par d'autres composables. En général, cela simplifie le processus. Un bon exemple de cela est la façon dont l'aperçu de l'écran d'accueil doit être modifié pour appeler OnboardingScreen :

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {}) // Do nothing on click.
    }
}

Attribuer onContinueClicked à une expression lambda vide équivaut à ne rien faire, ce qui est parfait pour un aperçu.

Cela ressemble de plus en plus à une vraie application. Bravo !

25915eb273a7ef49.gif

Dans le composable MyApp, nous avons utilisé le délégué de propriété by pour la première fois pour éviter d'utiliser une valeur à chaque fois. Utilisons by au lieu de =, également dans le composable Greeting pour la propriété expanded. Assurez-vous de remplacer val par var pour expanded.

Code complet jusqu'ici :

package com.example.basicscodelab

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }
}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier = modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by remember { mutableStateOf(false) }

    val extraPadding = if (expanded) 48.dp else 0.dp

    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(
                modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

9. Créer une liste inactive performante

Créons une liste de noms plus réaliste. Jusqu'à présent, vous avez affiché deux messages d'accueil dans une Column. Mais est-ce que la colonne peut en gérer plusieurs milliers ?

Modifiez la valeur de liste par défaut dans les paramètres Greetings pour utiliser un autre constructeur de liste permettant de définir la taille de la liste et de la remplir avec la valeur contenue dans son lambda (ici, $it représente l'index de liste) :

names: List<String> = List(1000) { "$it" }

Cette opération crée 1 000 messages d'accueil, même ceux qui ne tiennent pas dans l'écran. Ce n'est évidemment pas optimal. Vous pouvez essayer de l'exécuter sur un émulateur. Attention : ce code peut figer votre émulateur.

Pour afficher une colonne déroulante, nous utilisons LazyColumn. LazyColumn affiche uniquement les éléments visibles à l'écran, ce qui permet d'améliorer les performances lors de l'affichage d'une longue liste.

Dans son utilisation de base, l'API LazyColumn fournit un élément items dans son champ d'application, où chaque logique de rendu d'élément est écrite :

import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
// ...

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = List(1000) { "$it" }
) {
    LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

284f925eb984fb56.gif

10. Enregistrer l'état

Notre application présente deux problèmes :

Enregistrer l'état de l'écran d'accueil

Si vous exécutez l'application sur un appareil, cliquez sur les boutons, puis faites pivoter l'écran, l'écran d'accueil s'affiche à nouveau. La fonction remember fonctionne tant que le composable est conservé dans la composition. Lorsque vous faites pivoter l'appareil, toute l'activité est redémarrée et l'état est perdu. Cela se produit également en cas de modification de la configuration ou de fin du processus.

Au lieu d'utiliser remember, vous pouvez utiliser rememberSaveable. Cela enregistrera chaque état survivant à une modification de la configuration (rotation, par exemple) ou à la fin du processus.

Remplacez maintenant l'utilisation de remember dans shouldShowOnboarding par rememberSaveable :

    import androidx.compose.runtime.saveable.rememberSaveable
    // ...

    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

Exécutez l'application, faites pivoter l'appareil, passez en mode sombre ou arrêtez le processus. L'écran d'accueil ne s'affiche que si vous avez d'abord quitté l'application.

Enregistrer l'état développé des éléments de la liste

Si vous développez un élément de liste, puis que vous faites défiler la liste jusqu'à ce qu'il ne soit plus visible, ou que vous faites pivoter l'appareil, puis revenez à l'élément développé, vous constaterez que l'élément retrouve son état initial.

Pour régler ce problème, utilisez rememberSaveable pour l'état développé :

   var expanded by rememberSaveable { mutableStateOf(false) }

En 120 lignes de code environ jusqu'à présent, vous avez réussi à afficher une longue liste déroulante d'éléments performante, chacun avec son propre état. De plus, comme vous pouvez le constater, votre application dispose d'un mode sombre parfaitement fonctionnel, sans lignes de code supplémentaires. Nous verrons comment appliquer un thème un peu plus tard.

11. Animer votre liste

Compose propose plusieurs façons d'animer une interface utilisateur, des API de haut niveau pour les animations simples aux méthodes de bas niveau pour un contrôle total et des transitions complexes. Pour en savoir plus, consultez la documentation.

Dans cette section, vous allez utiliser l'une des API de bas niveau. Ne vous inquiétez pas, elles peuvent également être très simples. Animons la modification de la taille que nous avons déjà implémentée :

9efa14ce118d3835.gif

Pour cela, vous allez utiliser le composable animateDpAsState. Il renvoie un objet State dont la value est mise à jour en continu par l'animation jusqu'à la fin de celle-ci. Il utilise une "valeur cible" de type Dp.

Créez une extraPadding animée qui dépend de l'état développé.

import androidx.compose.animation.core.animateDpAsState

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by rememberSaveable { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp
    )
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }

        }
    }
}

Exécutez l'application et testez l'animation.

animateDpAsState accepte un paramètre animationSpec facultatif qui vous permet de personnaliser l'animation. Voyons quelque chose de plus intéressant : ajoutons un rebond :

import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by rememberSaveable { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )

    Surface(
    // ...
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))

    // ...

    )
}

Notez que nous nous assurons également que la marge intérieure n'est jamais négative, car cela pourrait faire planter l'application. Cela crée un bug d'animation subtil que nous corrigerons plus tard dans la section Touches finales.

La spécification spring n'accepte aucun paramètre temporel. Au lieu de cela, elle s'appuie sur des propriétés physiques (amortissement et raideur) pour rendre les animations plus naturelles. Exécutez à présent l'application pour tester la nouvelle animation :

9efa14ce118d3835.gif

Toute animation créée avec animate*AsState peut être interrompue. Cela signifie que animate*AsState redémarre l'animation et pointe vers la nouvelle valeur si la valeur cible change au milieu de l'animation. Les interruptions sont particulièrement fluides avec les rebonds :

d5dbf92de69db775.gif

Testez différents paramètres pour spring, différentes spécifications (tween, repeatable) et différentes fonctions animateColorAsState, ou un type d'API d'animation différent pour voir les différents types d'animations possibles.

Code complet de cette section

package com.example.basicscodelab

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }

}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = List(1000) { "$it" }
) {
    LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by rememberSaveable { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

12. Appliquer un style et un thème à votre application

Jusqu'à présent, vous n'avez défini aucun style pour les composables et pourtant vous avez un style par défaut correct, avec même un mode sombre. Intéressons-nous à BasicsCodelabTheme et à MaterialTheme.

Si vous ouvrez le fichier ui/theme/Theme.kt, vous constatez que BasicsCodelabTheme utilise MaterialTheme dans son implémentation :

// Do not copy
@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    // ...

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

MaterialTheme est une fonction composable qui reflète les principes de formatage de la spécification Material Design. Ces informations de style sont appliquées en cascade jusqu'aux composants qui se trouvent dans le content, lequel peut lire ces informations pour s'appliquer le style. Dans votre interface utilisateur, vous utilisez déjà BasicsCodelabTheme comme suit :

    BasicsCodelabTheme {
        MyApp(modifier = Modifier.fillMaxSize())
    }

Étant donné que BasicsCodelabTheme encapsule MaterialTheme en interne, les propriétés définies dans le thème sont utilisées pour appliquer un style à MyApp. Vous pouvez récupérer trois propriétés de MaterialTheme depuis n'importe quel composable descendant : colorScheme, typography et shapes. Utilisez-les pour définir le style d'en-tête de l'un de vos Text :

            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))
            ) {
                Text(text = "Hello, ")
                Text(text = name, style = MaterialTheme.typography.headlineMedium)
            }

Le composable Text dans l'exemple ci-dessus définit un nouveau TextStyle. Vous pouvez créer votre propre TextStyle ou, de préférence, récupérer un style défini par un thème à l'aide de MaterialTheme.typography. Cette construction vous permet d'accéder aux styles de texte définis par Material, comme displayLarge, headlineMedium, titleSmall, bodyLarge, labelMedium, etc. Dans notre exemple, vous utilisez le style headlineMedium défini dans le thème.

Compilez à présent l'application pour voir le texte auquel vous venez d'appliquer un style :

673955c38b076f1c.png

Il est généralement préférable de conserver les couleurs, les formes et les styles de police dans un MaterialTheme. Par exemple, le mode sombre serait difficile à implémenter si vous codiez les couleurs en dur. Corriger cela nécessiterait énormément de travail et risquerait d'introduire des erreurs.

Il arrive cependant parfois que vous ayez besoin de vous écarter légèrement des couleurs et des styles de police sélectionnés. Dans ce cas, il est préférable de baser votre couleur ou votre style sur une couleur ou un style existants.

Pour cela, vous pouvez modifier un style prédéfini à l'aide de la fonction copy. Mettez le nombre en gras :

import androidx.compose.ui.text.font.FontWeight
// ...
Text(
    text = name,
    style = MaterialTheme.typography.headlineMedium.copy(
        fontWeight = FontWeight.ExtraBold
    )
)

De cette manière, si vous avez besoin de modifier la famille de polices ou tout autre attribut de headlineMedium, vous n'avez pas à vous soucier des légers écarts.

Le résultat devrait être le suivant dans la fenêtre d'aperçu :

b33493882bda9419.png

Configurer un aperçu en mode sombre

Actuellement, notre aperçu ne montre que l'application en mode clair. Ajoutez une annotation @Preview supplémentaire à GreetingPreview avec UI_MODE_NIGHT_YES :

import android.content.res.Configuration.UI_MODE_NIGHT_YES

@Preview(
    showBackground = true,
    widthDp = 320,
    uiMode = UI_MODE_NIGHT_YES,
    name = "GreetingPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

Cela ajoute un aperçu en mode sombre.

2c94dc7775d80166.png

Modifier le thème de votre application

Vous trouverez tous les éléments associés au thème actuel dans les fichiers du dossier ui/theme. Par exemple, les couleurs par défaut que nous avons utilisées jusqu'à présent sont définies dans Color.kt.

Commençons par définir de nouvelles couleurs. Ajoutez les couleurs suivantes dans Color.kt :

val Navy = Color(0xFF073042)
val Blue = Color(0xFF4285F4)
val LightBlue = Color(0xFFD7EFFE)
val Chartreuse = Color(0xFFEFF7CF)

À présent, attribuez-les à la palette de MaterialTheme dans Theme.kt :

private val LightColorScheme = lightColorScheme(
    surface = Blue,
    onSurface = Color.White,
    primary = LightBlue,
    onPrimary = Navy
)

Si vous revenez à MainActivity.kt et actualisez l'aperçu, les couleurs ne seront pas modifiées. En effet, par défaut, l'aperçu utilise des couleurs dynamiques. Vous pouvez voir la logique d'ajout de couleurs dynamiques dans Theme.kt à l'aide du paramètre booléen dynamicColor.

Pour voir la version non adaptative de votre jeu de couleurs, exécutez votre application sur un appareil avec un niveau d'API inférieur à 31 (correspondant à Android S, où les couleurs adaptatives ont été introduites). Les nouvelles couleurs s'affichent :

493d754584574e91.png

Dans Theme.kt, définissez la palette de couleurs sombres :

private val DarkColorScheme = darkColorScheme(
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)

Lorsque nous allons exécuter l'application, nous allons voir les couleurs sombres en action :

84d2a903ffa6d8df.png

Code final de Theme.kt

import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.ViewCompat

private val DarkColorScheme = darkColorScheme(
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)

private val LightColorScheme = lightColorScheme(
    surface = Blue,
    onSurface = Color.White,
    primary = LightBlue,
    onPrimary = Navy
)

@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }
        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }
    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
            ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme
        }
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

13. Touches finales

Au cours de cette étape, vous allez appliquer ce que vous avez appris et découvrir de nouveaux concepts avec quelques indications seulement. Voici le résultat final auquel vous allez aboutir :

8d24a786bfe1a8f2.gif

Remplacer le bouton par une icône

  • Utilisez le composable IconButton avec une Icon enfant.
  • Utilisez Icons.Filled.ExpandLess et Icons.Filled.ExpandMore, que vous trouverez dans l'artefact material-icons-extended. Ajoutez la ligne de code suivante aux dépendances dans votre fichier app/build.gradle.kts.
implementation("androidx.compose.material:material-icons-extended")
  • Modifiez les marges intérieures pour corriger l'alignement.
  • Ajoutez une description du contenu pour améliorer l'accessibilité (voir "Utiliser des ressources de chaîne" ci-dessous).

Utiliser des ressources de chaîne

La description du contenu pour les options "Show more" (Plus) et "Show less" (Moins) est requise. Vous pouvez l'ajouter avec une simple instruction if :

contentDescription = if (expanded) "Show less" else "Show more"

Il est déconseillé de coder les chaînes en dur. Récupérez-les depuis le fichier strings.xml.

Pour récupérer automatiquement les chaînes, vous pouvez utiliser l'option "Extract string resource" (Extraire la ressource de chaîne) pour chaque chaîne. Cette option se trouve sous "Context Actions" (Actions contextuelles) dans Android Studio.

Vous pouvez aussi ouvrir app/src/res/values/strings.xml et ajouter les ressources suivantes :

<string name="show_less">Show less</string>
<string name="show_more">Show more</string>

Afficher plus de détails

Le texte "Composem ipsum" apparaît et disparaît, modifiant la taille de chaque carte.

  • Ajoutez un nouveau Text à la colonne dans Greeting qui s'affiche lorsque l'élément est développé.
  • Supprimez extraPadding et appliquez à la place le modificateur animateContentSize à Row. Cela va automatiser le processus de création de l'animation, ce qui serait difficile à faire manuellement. De plus, cela évite d'avoir à utiliser coerceAtLeast.

Ajouter une élévation et des formes

  • Vous pouvez utiliser le modificateur shadow conjointement au modificateur clip pour obtenir l'aspect souhaité pour la carte. Cependant, il existe un composable Material Design précisément pour effectuer cette opération : Card. Vous pouvez modifier les couleurs de Card en appelant CardDefaults.cardColors et en remplaçant la couleur à modifier.

Code final

package com.example.basicscodelab

import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons.Filled
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.basicscodelab.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {
    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

    Surface(modifier, color = MaterialTheme.colorScheme.background) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }
}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = List(1000) { "$it" }
) {
    LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {
    Card(
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.primary
        ),
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        CardContent(name)
    }
}

@Composable
private fun CardContent(name: String) {
    var expanded by rememberSaveable { mutableStateOf(false) }

    Row(
        modifier = Modifier
            .padding(12.dp)
            .animateContentSize(
                animationSpec = spring(
                    dampingRatio = Spring.DampingRatioMediumBouncy,
                    stiffness = Spring.StiffnessLow
                )
            )
    ) {
        Column(
            modifier = Modifier
                .weight(1f)
                .padding(12.dp)
        ) {
            Text(text = "Hello, ")
            Text(
                text = name, style = MaterialTheme.typography.headlineMedium.copy(
                    fontWeight = FontWeight.ExtraBold
                )
            )
            if (expanded) {
                Text(
                    text = ("Composem ipsum color sit lazy, " +
                        "padding theme elit, sed do bouncy. ").repeat(4),
                )
            }
        }
        IconButton(onClick = { expanded = !expanded }) {
            Icon(
                imageVector = if (expanded) Filled.ExpandLess else Filled.ExpandMore,
                contentDescription = if (expanded) {
                    stringResource(R.string.show_less)
                } else {
                    stringResource(R.string.show_more)
                }
            )
        }
    }
}

@Preview(
    showBackground = true,
    widthDp = 320,
    uiMode = UI_MODE_NIGHT_YES,
    name = "GreetingPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

14. Félicitations

Félicitations ! Vous avez appris les principes de base de Compose !

Solution de l'atelier de programmation

Le code nécessaire à la solution de cet atelier de programmation est disponible sur GitHub :

$ git clone https://github.com/android/codelab-android-compose

Vous pouvez également télécharger le dépôt sous forme de fichier ZIP :

Et maintenant ?

Consultez les autres ateliers de programmation du parcours Compose :

Complément d'informations