Principes de base de Jetpack Compose

Restez organisé à l'aide des collections Enregistrez et classez les contenus selon vos préférences.

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 modulables, qui sont simplement des fonctions standards annotées avec @Composable et pouvant appeler d'autres fonctions modulables. 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 modulables 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 :

Prerequisites

  • 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 modulables
  • 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 :

87f2753c576d26f2.gif

Ce dont vous avez besoin

2. Démarrer un nouveau projet Compose

Pour démarrer un nouveau projet Compose, ouvrez Android Studio Bumblebee et sélectionnez Start a new Android Studio project (Démarrer un nouveau projet Android Studio) comme illustré ci-dessous :

ec53715fe31913e6.jpeg

Si l'écran ci-dessus ne s'affiche pas, accédez à File (Fichier) > New (Nouveau) > New Project (Nouveau projet).

Lors de la création d'un projet, sélectionnez Empty Compose Activity (Activité Compose vide) dans la liste des modèles disponibles.

a67ba73a4f06b7ac.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 Compose 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 et app/build.gradle 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 : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                // A surface container using the 'background' color from the theme
                Surface(color = MaterialTheme.colors.background) {
                    Greeting("Android")
                }
            }
        }
    }
}

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

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    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/googlecodelabs/android-compose-codelabs

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 modulables

Une fonction modulable 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 modulable fournie par la bibliothèque.

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

Compose dans une application Android

Avec Compose, Activities 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 modulables 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(color = MaterialTheme.colors.background) {
                    Greeting("Android")
                }
            }
        }
    }
}

BasicsCodelabTheme permet de définir un style pour les fonctions modulables. 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 modulable 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 DefaultPreview() {
    BasicsCodelabTheme {
        Greeting(name = "Android")
    }
}

88d6e7a2cfc33ed9.png

Il est possible que l'aperçu ne s'affiche pas si l'option Code bcf00530a220eea9.png est sélectionnée. Cliquez sur Split (Diviser) aadde7eea0921d0f.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.colors.primary.

@Composable
private fun Greeting(name: String) {
    Surface(color = MaterialTheme.colors.primary) {
        Text (text = "Hello $name!")
    }
}

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

Lorsque vous ajoutez ce code au projet, un bouton Build & Refresh (Compiler et actualiser) s'affiche dans l'angle supérieur droit d'Android Studio. Appuyez dessus ou créez le projet pour que les nouvelles modifications se répercutent dans l'aperçu.

1886a2cbfefe7df3.png

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

a6cd30458c8829a2.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.material.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.

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().

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

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

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

Cliquez sur Build & Refresh (Compiler et actualiser) pour appliquer les modifications.

52d5ed8919f277f0.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.

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

@Composable
private fun MyApp() {
    Surface(color = MaterialTheme.colors.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. Votre fichier MainActivity.kt devrait se présenter comme suit :

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.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.codelab.basicstep1.ui.theme.BasicsCodelabTheme

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

@Composable
private fun MyApp() {
    Surface(color = MaterialTheme.colors.background) {
        Greeting("Android")
    }
}

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

@Preview(showBackground = true)
@Composable
private fun DefaultPreview() {
    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.

fbd450e8eab10338.png

Ce sont des fonctions modulables qui requièrent des contenus modulables 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 :

cf26127d32021cd.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
private fun Greeting(name: String) {
    Surface(color = MaterialTheme.colors.primary) {
        Column(modifier = Modifier.padding(24.dp)) {
            Text(text = "Hello,")
            Text(text = name)
        }
    }
}

Compose et Kotlin

Les fonctions modulables 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 à la Column :

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

c366a44b3e99157f.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 DefaultPreview() {
    BasicsCodelabTheme {
        MyApp()
    }
}

63ac84a794ec7a3d.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.

ecd3370d03f7130e.png

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

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

@Composable
private fun Greeting(name: String) {
    Surface(
        color = MaterialTheme.colors.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 :

203e0a9946f313cc.png

Button est un composable fourni par le package Material 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.material.Button
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
...

@Composable
private fun Greeting(name: String) {

    Surface(
        color = MaterialTheme.colors.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)
            }
            OutlinedButton(
                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 :

e3914108b7082ac0.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
private fun Greeting(name: String) {
    var expanded = false // Don't do this!

    Surface(
        color = MaterialTheme.colors.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)
            }
            OutlinedButton(
                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 modulable 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.

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

Si vous exécutez l'application dans un émulateur, vous pouvez voir que, lorsque vous cliquez 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.

825dd6d6f98bff05.gif

Voici le code jusqu'à présent :

@Composable
private fun Greeting(name: String) {
    val expanded = remember { mutableStateOf(false) }

    Surface(
        color = MaterialTheme.colors.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)
            }
            OutlinedButton(
                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
private fun Greeting(name: String) {

    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 dépend d'un état et effectue un calcul simple.

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

@Composable
private fun Greeting(name: String) {

    val expanded = remember { mutableStateOf(false) }

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

    Surface(
        color = MaterialTheme.colors.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)
            }
            OutlinedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

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

e3914108b7082ac0.gif

8. Hisser un état

Dans les fonctions modulables, 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.

8c0da5d9a631ba97.png

Ajoutez le code suivant à MainActivity.kt :

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment

...

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

    Surface {
        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() {
    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 :

@Composable
fun MyApp() {
     Greetings()
}

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

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

@Composable
fun MyApp() {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    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() {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

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

@Composable
fun OnboardingScreen(onContinueClicked: () -> Unit) {

    Surface {
        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 !

1fd101673cd56005.gif

Code complet jusqu'ici :

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
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.layout.wrapContentSize
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.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.BasicsCodelabTheme

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

@Composable
fun MyApp() {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

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

@Composable
fun OnboardingScreen(onContinueClicked: () -> Unit) {

    Surface {
        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(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
private fun Greeting(name: String) {

    val expanded = remember { mutableStateOf(false) }

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

    Surface(
        color = MaterialTheme.colors.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)
            }
            OutlinedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

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

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(names: List<String> = List(1000) { "$it" } ) {
    LazyColumn(modifier = Modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

487ca9093596fc6c.gif

10. Enregistrer l'état

Notre application présente un problème : si vous l'exécutez sur un appareil, cliquez sur les boutons, puis faites pivoter l'appareil, 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 :

    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.

63be420fc371e10f.gif

Démonstration du fait qu'une modification de la configuration (passage en mode sombre) n'entraîne pas le réaffichage de l'écran d'accueil.

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 :

50756832c3714a6f.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é. Utilisez également le délégué de propriété (le mot clé by) :

@Composable
private fun Greeting(name: String) {

    var expanded by remember { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp
    )
    Surface(
        color = MaterialTheme.colors.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)
            }
            OutlinedButton(
                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 :

@Composable
private fun Greeting(name: String) {

    var expanded by remember { 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 :

489e7c08d5c46781.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 :

354ddf3f23ebb8e0.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

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.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.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.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.BasicsCodelabTheme

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

@Composable
fun MyApp() {

    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

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

@Composable
fun OnboardingScreen(onContinueClicked: () -> Unit) {

    Surface {
        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(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) {

    var expanded by remember { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )
    Surface(
        color = MaterialTheme.colors.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)
            }
            OutlinedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

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

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.kt, vous constatez que BasicsCodelabTheme utilise MaterialTheme dans son implémentation :

@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }

    MaterialTheme(
        colors = colors,
        typography = typography,
        shapes = shapes,
        content = content
    )
}

MaterialTheme est une fonction modulable 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()
    }

É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 : colors, 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)) {
                Text(text = "Hello, ")
                Text(text = name, style = MaterialTheme.typography.h4)
            }

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 h1-h6, body1,body2, caption, subtitle1, etc. Dans notre exemple, vous utilisez le style h4 défini dans le thème.

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

5a2a3b62960cd0e9.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 :

                Text(
                    text = name,
                    style = MaterialTheme.typography.h4.copy(
                        fontWeight = FontWeight.ExtraBold
                    )
                )

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

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

619daa86b737c945.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. 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 LightColorPalette = lightColors(
    surface = Blue,
    onSurface = Color.White,
    primary = LightBlue,
    onPrimary = Navy
)

Si vous revenez à MainActivity.kt et actualisez l'aperçu, les nouvelles couleurs s'affichent :

479f2f0e8f19c3e9.png

Cependant, vous n'avez pas encore modifié les couleurs sombres. Avant cela, configurons les aperçus correspondants. Ajoutez une annotation @Preview supplémentaire à DefaultPreview avec UI_MODE_NIGHT_YES :

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

Cela ajoute un aperçu en mode sombre.

8a54a386b258277a.png

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

private val DarkColorPalette = darkColors(
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)

Nous avons appliqué un style et un thème à notre application !

19e76f3aa95940af.png

Code final de Theme.kt

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable

private val DarkColorPalette = darkColors(
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)

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

@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }

    MaterialTheme(
        colors = colors,
        typography = typography,
        shapes = shapes,
        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 :

87f2753c576d26f2.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.
implementation "androidx.compose.material:material-icons-extended:$compose_version"
  • 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.

Code final

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.Button
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons.Filled
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.codelab.basics.R
import com.codelab.basics.ui.BasicsCodelabTheme

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

@Composable
private fun MyApp() {
    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

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

@Composable
private fun OnboardingScreen(onContinueClicked: () -> Unit) {
    Surface {
        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(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) {
    Card(
        backgroundColor = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        CardContent(name)
    }
}

@Composable
private fun CardContent(name: String) {
    var expanded by remember { 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.h4.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 = "DefaultPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun DefaultPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

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

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/googlecodelabs/android-compose-codelabs

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

Étape suivante

Consultez les autres ateliers de programmation du parcours Compose :

Complément d'informations