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 :
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.
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
etapp/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")
}
}
Il est possible que l'aperçu ne s'affiche pas si l'option Code est sélectionnée. Cliquez sur Split (Diviser) 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 :
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)
)
}
}
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
.
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 :
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)
}
}
}
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()
}
}
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
.
À 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 :
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 :
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.
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.
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 :
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.
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 surfalse
. 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 !
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)
}
}
}
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 :
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 :
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 :
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 :
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 :
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.
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 :
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 :
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 :
Remplacer le bouton par une icône
- Utilisez le composable
IconButton
avec uneIcon
enfant. - Utilisez
Icons.Filled.ExpandLess
etIcons.Filled.ExpandMore
, que vous trouverez dans l'artefactmaterial-icons-extended
. Ajoutez la ligne de code suivante aux dépendances dans votre fichierapp/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 dansGreeting
qui s'affiche lorsque l'élément est développé. - Supprimez
extraPadding
et appliquez à la place le modificateuranimateContentSize
àRow
. Cela va automatiser le processus de création de l'animation, ce qui serait difficile à faire manuellement. De plus, cela évite d'avoir à utilisercoerceAtLeast
.
Ajouter une élévation et des formes
- Vous pouvez utiliser le modificateur
shadow
conjointement au modificateurclip
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 deCard
en appelantCardDefaults.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 :
- Mises en page de Compose
- Gérer les états dans Compose
- Personnalisation des thèmes de Compose
- Compose pour les applications existantes
Complément d'informations
- Code des principes de base de Jetpack Compose
- Guide Modèle mental de Compose
- Exemples d'application pour voir Compose en action