JPC User Interface

Hello World

Quand on crée un nouveau projet Jetpack Compose avec Android Studio , on obtient un ensemble de fichiers avec une arborescence spécifique. Pour l'instant le seul fichier qui nous intéresse est le fichier MainActivity

Hello World
package com.example.test01

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
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 com.example.test01.ui.theme.Test01Theme

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

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

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

  • Sous Jetpack Compose, L'interface utilisateur (UI) est constituée de Composables. C'est le terme pour désigner des éléments graphiques
  • L'application proposés est juste un exemple qui affiche "Hello Android!"
  • setContent { ... } : Cette fonction est appelée dans la méthode onCreate() d'une activité pour définir le contenu de l'interface utilisateur. Tout ce qui est passé à setContent est utilisé pour composer l'interface graphique.
  • Test01Theme { ... } : Cette section applique un thème par défaut (que l'on peut personnaliser) à l'interface utilisateur. Le thème gère les couleurs, les formes, la typographie, etc., qui seront appliquées à tous les composants enfants
  • Surface(...) { ... } : Surface est un composant de base de Jetpack Compose. Il s'agit d'un conteneur. Il peut être utilisé pour appliquer un fond, une élévation ou des bordures.
    • Entre parenthèses, on place les propriétés de l'élément graphique. Dans Jetpack Compose, les propriété d'un élément sont définies de deux manières:
      • Les attributs: Ce sont des propriétés propres à l'élément
      • Les modifiers: Ce sont des propriétés qui peuvent s'appliquer à différents types d'éléments

      Dans notre exemple, l'élément Surface( ) { } est caractérisé par deux propriétés:

      • L'Attribut color qui définit la couleur de l'arrière plan. Ici c'est la couleur par défaut du thème. On aura l'occasion de revenir sur les thèmes plus tard
      • Le Modifier fillMaxSize() qui précise que l'élément va occuper toute la surface de l'écran
    • Entre Accolades, on place les éléments graphiques qui seront affichés dans la surface. Ici, on appelle la fonction composable Greeting qui va afficher le message
  • La fonction Greeting() appelle le composable Text() pour afficher Hello Android! comme indiqué sur la figure ci-dessous
  • En dessous de la fonction Greeting() on trouve la partie @Preview() qui permet de prévisualiser le résultat sur un Virtual Device. En ce qui me concerne, je supprime cette partie car je préfère visualiser sur un vrai téléphone


Hello World très basique

En fait, l'élément le plus important du fichier MainActivity est la fonction setContent {}. C'est dans cette fonction qu'il faut placer tout le contenu de l'application

Si on veut un "Hello World!" très basique, On peut se passer des propriétés par défaut du thème ainsi que de l'élément Surface(). On peut aussi placer l'élément Text() directement dans le bloc setContent{}


Hello World très basique
package com.example.hellobasic

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.Text

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Text( text = "Hello World!" )
        }
    }
}

Mise en page

Il me semble que les éléments les plus importants pour la mise en Page d'une Interface utilisateur (UI) sont les composables Column(){} et Row(){} qui permettent d'organiser les éléments composables soit en colonne soit en ligne.

On peut obtenir des mises en page assez élaborées en plaçant des lignes dans des colonnes et des colonnes dans des lignes.


Column( ){ }

  • Permet de positionner (d'empiler) les éléments (Composables) verticalement
  • Les attributs (propriétés) de la colonne sont indiqués entre (), les élément contenus dans la colone sont placés entre {}
  • verticalArrangement: Cet attribut définit la façon dont les éléments seront répartis verticalement. Il peut prendre les valeurs:
    • Arrangement.Top
    • Arrangement.Center
    • Arrangement.Bottom
    • Arrangement.SpaceAround
    • Arrangement.SpaceEvenly
    • Arrangement.SpaceBetween
    • Arrangement.spacedBy(valeur.dp)
  • horizontalAlignment Cet attribut définit la façon dont les éléments seront ajustés horizontalement. Il peut prendre les valeurs:
    • Alignment.Start
    • Alignment.CenterHorizontally
    • Alignment.End
  • D'autres propriétés comme la couleur, la bordure, les dimensions... peuvent être définis par les modificateurs

  • Row( ){ }

    • Row qui signifie ligne permet de positionner les éléments (Composables) horizontalement
    • Les attributs (propriétés) de la Row sont indiqués entre (), les élément contenus dans la Row sont placés entre {}
    • horizontalArrangement: Cet attribut définit la façon dont les éléments seront répartis horizontalement. Il peut prendre les valeurs:
      • Arrangement.Start
      • Arrangement.Center
      • Arrangement.End
      • Arrangement.SpaceAround
      • Arrangement.SpaceEvenly
      • Arrangement.SpaceBetween
      • Arrangement.spacedBy(valeur.dp)
    • verticalAlignment Cet attribut définit la façon dont les éléments seront justifiés verticalement. Il peut prendre les valeurs:
      • Alignment.Top
      • Alignment.CenterVertically
      • Alignment.Bottom
    • D'autres propriétés comme la couleur, la bordure, les dimensions... peuvent être définis par les modificateurs


    Exemple 1: Répartition verticale


    Arrangement Vertical dans une Column()
    package com.example.columns
    
    import android.os.Bundle
    import androidx.activity.ComponentActivity
    import androidx.activity.compose.setContent
    import androidx.compose.foundation.background
    import androidx.compose.foundation.layout.Arrangement
    import androidx.compose.foundation.layout.Column
    import androidx.compose.foundation.layout.Row
    import androidx.compose.foundation.layout.fillMaxHeight
    import androidx.compose.foundation.layout.fillMaxSize
    import androidx.compose.foundation.layout.fillMaxWidth
    import androidx.compose.foundation.layout.padding
    import androidx.compose.foundation.shape.RoundedCornerShape
    import androidx.compose.material3.Text
    import androidx.compose.runtime.Composable
    import androidx.compose.ui.Alignment
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.graphics.Color
    import androidx.compose.ui.unit.dp
    
    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                MyApp()
            }
        }
    }
    
    @Composable
    fun MyApp() {
        Column(  //Conteneur principal, 
            verticalArrangement = Arrangement.spacedBy(10.dp),
            modifier = Modifier.fillMaxSize()
        ) {
            Row( // moitié haute de l'écran
                horizontalArrangement = Arrangement.spacedBy(10.dp),
                modifier = Modifier
                    .fillMaxWidth()
                    .weight(1f)
            )
            {
                Column(
                    verticalArrangement = Arrangement.Top,
                    horizontalAlignment = Alignment.CenterHorizontally,
                    modifier = Modifier
                        .fillMaxHeight()
                        .weight(1f)
                        .background(Color.Cyan)
                ) {
                    Txt(" Top ")
                    Txt(" Top ")
                    Txt(" Top ")
                }
                Column(
                    verticalArrangement = Arrangement.spacedBy(20.dp),
                    horizontalAlignment = Alignment.CenterHorizontally,
                    modifier = Modifier
                        .fillMaxHeight()
                        .weight(1f)
                        .background(Color.Cyan)
                ) {
                    Txt("spacedBy")
                    Txt("spacedBy")
                    Txt("spacedBy")
                }
                Column(
                    verticalArrangement = Arrangement.Center,
                    horizontalAlignment = Alignment.CenterHorizontally,
                    modifier = Modifier
                        .fillMaxHeight()
                        .weight(1f)
                        .background(Color.Cyan)
                ) {
                    Txt("Center")
                    Txt("Center")
                    Txt("Center")
                }
                Column(
                    verticalArrangement = Arrangement.Bottom,
                    horizontalAlignment = Alignment.CenterHorizontally,
                    modifier = Modifier
                        .fillMaxHeight()
                        .weight(1f)
                        .background(Color.Cyan)
                ) {
                    Txt("Bottom")
                    Txt("Bottom")
                    Txt("Bottom")
                }
    
            }
            Row( // moitié basse de l'écran
                horizontalArrangement = Arrangement.spacedBy(10.dp),
                modifier = Modifier
                    .fillMaxWidth()
                    .weight(1f)
            )
            {
                Column(
                    verticalArrangement = Arrangement.SpaceAround,
                    horizontalAlignment = Alignment.CenterHorizontally,
                    modifier = Modifier
                        .fillMaxHeight()
                        .weight(1f)
                        .background(Color.Cyan)
                ) {
                    Txt("SpaceAround")
                    Txt("SpaceAround")
                    Txt("SpaceAround")
                }
                Column(
                    verticalArrangement = Arrangement.SpaceEvenly,
                    horizontalAlignment = Alignment.CenterHorizontally,
                    modifier = Modifier
                        .fillMaxHeight()
                        .weight(1f)
                        .background(Color.Cyan)
                ) {
                    Txt("SpaceEvenly")
                    Txt("SpaceEvenly")
                    Txt("SpaceEvenly")
                }
                Column(
                    verticalArrangement = Arrangement.SpaceBetween,
                    horizontalAlignment = Alignment.CenterHorizontally,
                    modifier = Modifier
                        .fillMaxHeight()
                        .weight(1f)
                        .background(Color.Cyan)
                ) {
                    Txt("SpaceBetween")
                    Txt("SpaceBetween")
                    Txt("SpaceBetween")
                }
            }
        }
    }
    
    @Composable
    fun Txt(str: String) {
        Text(text = str,
            modifier = Modifier
                .background(Color.Yellow, shape = RoundedCornerShape(10.dp))
                .padding(5.dp)
        )
    }
    


Dans la figure ci-dessus:

  • Le conteneur principal est une Column() qui occupe la totalité de l'écran
  • Cette Column() contient deux Row(). Une occupe la moitié haute de l'écran, l'autre la moitié basse. Pour partager l'écran équitablement entre les deux Row(), chacune est caractérisée par la propriété Modifier.fillMaxWidth() pour occuper toute la largeur et Modifier.weight(1f) pour occuper le même espace vertical que l'autre
  • La premiere Row() contient 4 Columns(). Chacune a la propriété Modifier.weight(1f) pour se partager équitablement l'espace horizontal de la Row() mère
  • La deuxième Row() contient 3 Columns(). Chacune a la propriété Modifier.weight(1f) pour se partager équitablement l'espace horizontal de la Row() mère
  • Chacune des sept Columns() contient trois composable Text()
  • Chacune des sept Columns() utilise un attribut verticalArrangement différent pour illustration
  • Toutes les colonnes utilisent l'ajustement horizontal Alignment.CenterHorizontally

Exemple 2: Répartition horizontale


Arrangement horizontal dans une Row()
package com.example.rows

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

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

@Composable
fun MyApp() {
    Column(verticalArrangement = Arrangement.spacedBy(10.dp),
        modifier = Modifier.fillMaxSize())
    {
        MyRow(Arrangement.Start)
        MyRow(Arrangement.Center)
        MyRow(Arrangement.End)
        MyRow(Arrangement.SpaceAround)
        MyRow(Arrangement.SpaceEvenly)
        MyRow(Arrangement.SpaceBetween)
        Row(horizontalArrangement = Arrangement.spacedBy(20.dp),
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier
                .fillMaxWidth()
                .background(Color.Cyan)
                .height(70.dp)
        )
        {
            Txt("spacedBy")
            Txt("spacedBy")
            Txt("spacedBy")
        }
    }
}

@Composable
fun Txt(str: String){
    Text(text = str,
        modifier = Modifier
            .background(Color.Yellow, shape = RoundedCornerShape(10.dp))
            .padding(5.dp)
    )
}

@Composable
fun MyRow(arrHor: Arrangement.Horizontal) {
    Row(horizontalArrangement = arrHor,
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier
            .fillMaxWidth()
            .background(Color.Cyan)
            .height(70.dp)
    )
    {
        Txt(arrHor.toString().substringAfter("#"))
        Txt(arrHor.toString().substringAfter("#"))
        Txt(arrHor.toString().substringAfter("#"))
    }
}



Dans la figure ci-dessus:

  • Le conteneur principal est une Column() qui occupe la totalité de l'écran
  • Cette Column() contient sept Row(), chacune est caractérisée par la propriété Modifier.fillMaxWidth() pour occuper toute la largeur et Modifier.weight(1f) pour occuper le même espace vertical que les autres
  • Chaque Row() contient 3 composable Text() et utilise une propriété horizontalArrangement différente pour illustration
  • Toutes les Row() utilise une propriété verticalAlignment = Alignment.CenterVertically
  • Pour optimiser un peu le code, j'ai crée la fonction MyRow() que j'ai invoqué plusieurs fois pour afficher chaque une Row() avec les textes qu'elle contient

LazyColumn()

Si on essaye d'afficher une liste avec un grand nombre d'éléments dans une Column(), on aura un problème car Column() ne permet pas de défilement vertical et il n'y aura que les premiers éléments qui seront visibles

LazyColumn est conçue pour gérer de grandes listes efficacement car elle offre la possibilité de défilement vertical. Son fonctionnement est optimisé car il n'y a que les éléments visibles dans la fenêtre d'affichage qui sont composés graphiquement

Il existe plusieurs façons d'ajouter des éléments dans un LazyColumn :

  1. item{ } : Utilisée pour ajouter un seul élément
  2. items(cont) { } : Utilisé pour ajouter count éléments
  3. items(maListe) { } : Utilisé pour ajouter tous les éléments de la liste maListe
  4. itemsIndexed(maListe) { } : Utilisé pour ajouter tous les éléments de la liste maListe et donne l'accès à l'index de chaque élément

LazyColumn
package com.example.lazycolun

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

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

@Composable
fun MyApp() {
    val maListe = listOf("Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche")
    LazyColumn(
        verticalArrangement = Arrangement.spacedBy(8.dp),
        modifier = Modifier
            .fillMaxWidth()
            .padding(start = 20.dp)
    )
    {
        item {
            Text(
                text = "En-tête",
                fontSize = 30.sp,
                modifier = Modifier
                    .fillMaxWidth()
                    .background(Color.Cyan)
            )
        }
        item {
            Button(onClick = { /*TODO*/ })
            { Text(text = "Bouton A") }
        }
        item {
            Button(onClick = { /*TODO*/ })
            { Text(text = "Bouton B") }
        }
        item { Spacer(modifier = Modifier.height(20.dp)) }
        items(5) { index ->
            Text("Élément numéro $index")
        }
        item { Spacer(modifier = Modifier.height(20.dp)) }
        items(maListe) { item ->
            Text(text = item)
        }
        item { Spacer(modifier = Modifier.height(20.dp)) }
        itemsIndexed(maListe) { index, item ->
            Text(text = "jour $index -> $item")
        }
    }
}


Le composable Text( )

Cet élément permet de placer un text dans un conteneur

L'apparence du texte dépend d'un certain nombre d'attributs et de modificateurs

Text(
    "le texte ici",
    attribut1 = valeur, 
    attribut2 = valeur
    …,
    modifier = Modifier
	    .modif1
	    .modif2
	    .modif3
	    …       
    )

Quelques attributs

  • color: Couleur du texte
  • color = Color.Red ou color=Color(0xFFrrvvbb)
  • fontStyle : Style de fonte.
  • fontStyle = FontStyle.Italic
  • fontSize : Taille de la fonte
  • fontSize = 30.sp
  • textAlign : Alignement du texte
  • textAlign = TextAlign.Center
  • fontWeight : poids de la police
  • fontWeight = FontWeight.Bold
  • fontFamily : Choix de la police
  • fontFamily = FontFamily.Monospace
  • Placer le curseur de la souris sur l'élément Text() pour voir tous les attributs possibles

Quelques modifiers

  • .align(alignment = Alignment.xy) Position du composable Text() par rapport à son père
  • Les positions xy possibles sont: TopStart, TopCenter, TopEnd, CenterStart, Center, CenterEnd, BottomStart, BottomCenter, BottomEnd
  • .background() : Applique une couleur de fond au texte
  • Modifier.background(Color.Gray)
  • .width : Largeur du composable
  • Modifier.width(100.dp)
  • .height : hauteur du composable
  • Modifier.height(100.dp)
  • .fillMaxWidth() : Étend la largeur pour occuper toute la largeur disponible.
  • .fillMaxHeight : Étend la hauteur pour occuper toute la hauteur disponible.
  • .fillMaxSize : Étend la largeur et la hauteur pour occuper tout l'espace disponible.
  • .padding() : Ajoute un espacement autour du texte,
    • Modifier.padding(16.dp): Padding de tout les cotés. Le texte apparaîtra dans l'espace qui reste après le padding
    • Modifier.padding(horizontal = 40.dp): Padding à gauche et à droite
    • Modifier.padding(vertical = 40.dp): Padding en haut et en bas
    • Modifier.padding(start = 40.dp): Padding gauche
    • Modifier.padding(start = 40.dp, end = 30.dp, top = 10.dp, bottom = 20.dp) : Padding de chaque coté
  • .border : Contour de l'objet
  • .border(width=2.dp, Color.Blue, shape = RoundedCornerShape(5.dp))
  • .wrapContentHeight() : Position verticale du texte dans le composable
    • .wrapContentHeight(Alignment.CenterVertically) // centrer le texte verticalement
    • .wrapContentHeight(Alignment.Top) // le texte est placé en haut du composable
    • .wrapContentHeight(Alignment.Bottom) // le texte est placé en bas du composable
Exemple composable Text()
package com.example.text

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp


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

@Composable
fun MyApp() {
    Column( horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
        .fillMaxSize()
        .background(Color.Green)) {
        Box (
            modifier = Modifier
                .background(Color.Cyan, shape = RoundedCornerShape(10.dp))
                .width(200.dp)
                .height(200.dp)
        ){
            Text(
                text = "Hello",
                fontWeight = FontWeight.Bold,
                fontStyle = FontStyle.Italic,
                fontSize = 20.sp,
                color = Color.Red,
                textAlign = TextAlign.Center, // centrer le texte horizontalement
                modifier = Modifier
                    .align(alignment = Alignment.BottomCenter) // position dans le père
                    .width(100.dp)
                    .height(100.dp)
                    .background(Color.Yellow, shape = RoundedCornerShape(5.dp))
                    .border(width=2.dp, Color.Blue, shape = RoundedCornerShape(5.dp))
                    .wrapContentHeight(Alignment.CenterVertically) // centrer le texte verticalement
            )
        }
    }
}


Dans cette exemple, on teste quelques composables: Column(), Box() et Text()

  • La Column() sert de conteneur principal
    • Elle occupe tout l'écran
    • Elle a une couleur verte
    • Ses fils seront centrés horizontalement
  • Dans la colonne On place un composable Box() qui va aussi servir de conteneur
    • Couleur de fond Cyan avec coins arrondis
    • Dimensions 200dp x 200dp
  • Dans la Box(), on place un composable Text() avec les caractéristiques:
    • Dimensions 100dp x 100dp
    • Placé au centre bas du conteneur Box()
    • Couleur de fond Jaune, bord arrondit
    • Cadre bleu, bords arrondis
    • Texte 'Hello' police 20sp, rouge, gras, italique, centré horizontalement et verticalement dans le composable
    • On remarquera que pour centrer le texte horizontalement, on utilise l'attribut textAlign, et pour centrer verticalement, on utilise le modifier wrapContentHeight(). Le plus souvent, on n'aura pas besoin de centrer le texte, si on ne précise pas les dimensions du composable, le texte prend juste l'espace nécessaire.


Button(), OutlinedButton(), TextButton()

Le bouton exécute une action lorsque l'utilisateur clique dessus. L'apparence du bouton dépend d'un certain nombre d'attributs et de modificateurs. Le bouton est un conteneur, on y place généralement du texte et des icônes

Button(
                    attribut1 = valeur, 
                    attribut2 = valeur,
                    …,
                    modifier = Modifier
                        .modif1
                        .modif2
                        .modif3
                        …       
                    ){
                        Contenu du bouton: Label, icônes ...
                    }
                

Quelques attributs

  • onClick Définit l'action qui sera exécutée quand le bouton est cliqué. C'est un attribut obligatoire.
  • onClick = {Toast.makeText(context, "Bouton cliqué", Toast.LENGTH_SHORT).show() },
  • enabled: Pour activer ou désactiver le bouton. Les valeurs possibles sont true et false. Si false, le bouton devient grisé et non cliquable.
  • enabled = true,
  • shape: Définit la forme du bouton
  • shape = RoundedCornerShape(10.dp),
  • border: Définit la bordure du bouton
  • border = BorderStroke(2.dp, Color.Blue),
  • colors: Permet de personnaliser les couleurs du bouton, comme la couleur de fond ou le contenu. pour le mode normal et le mode disabled
  • colors = ButtonDefaults.buttonColors(
                contentColor = Color.White,
                containerColor = Color(0xFF33FFFF)
            )
  • contentPadding Définit les marges intérieurs
  • contentPadding = PaddingValues(
                    start = 20.dp,
                    top = 12.dp,
                    end = 20.dp,
                    bottom = 12.dp
                ),

Quelques modifiers

  • .align(Alignment.xy) Position du composable par rapport à son père. (.align(alignment = Alignment.xy) // marche aussi)
  • Les positions xy possibles sont: TopStart, TopCenter, TopEnd, CenterStart, Center, CenterEnd, BottomStart, BottomCenter, BottomEnd
    Modifier.align(Alignment.Center)
  • Modifier.width : Largeur du composable
  • Modifier.width(100.dp)
  • .height : hauteur du composable
  • Modifier.height(100.dp)
  • .size(w.dp, h.dp) : Dimensions du composable
  • Modifier.size(200.dp, 100.dp)
  • .fillMaxWidth() : Étend la largeur pour occuper toute la largeur disponible.
  • .fillMaxHeight : Étend la hauteur pour occuper toute la hauteur disponible.
  • .fillMaxSize : Étend la largeur et la hauteur pour occuper tout l'espace disponible.
  • .padding() : Ajoute un espacement autour du texte,
    • Modifier.padding(16.dp)
      Padding de tout les cotés. Le texte apparaîtra dans l'espace qui reste après le padding
    • Modifier.padding(horizontal = 40.dp)
      Padding à gauche et à droite
    • Modifier.padding(vertical = 40.dp)
      Padding en haut et en bas
    • Modifier.padding(start = 40.dp)
      Padding gauche
    • Modifier.padding(start = 40.dp, end = 30.dp, top = 10.dp, bottom = 20.dp)
      Padding de chaque coté
  • .border : Contour de l'objet
  • Modifier.border(width=2.dp, Color.Blue, shape = RoundedCornerShape(5.dp))

Quelques boutons
package com.example.button0

import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

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

@Composable
fun MyApp() {
    val backColor1 = 0xFFFFE0B2
    val backColor2 = 0xFFB2DFDB
    val backMask = backColor1 xor backColor2
    val butColor1 = 0xFF883333
    val butColor2 = 0xFF3399AA
    val butMask = butColor1 xor butColor2
    var butColor by remember { mutableLongStateOf(butColor1) }
    var backColor by remember { mutableLongStateOf(backColor1) }
    Column(
        verticalArrangement = Arrangement.spacedBy(10.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            .fillMaxSize()
            .background(Color(backColor))
            .padding(10.dp)
    )
    {
        val context = LocalContext.current
            Button(onClick = { Toast.makeText(context, "par défaut", Toast.LENGTH_SHORT).show() },
            ) {
                Text(text = "par défaut")
            }
        Button(onClick = { Toast.makeText(context, "Rectangle", Toast.LENGTH_SHORT).show() },
            shape = RectangleShape,
        ) {
            Text(text = "Rectangle")
        }
        Button(onClick = { Toast.makeText(context, "Circulaire", Toast.LENGTH_SHORT).show() },
            shape = CircleShape,
            modifier = Modifier
                .width(120.dp)
                .height(120.dp)
        ) {
            Text(text = "Circulaire")
        }
        Button(onClick = { Toast.makeText(context, "Coins coupés", Toast.LENGTH_SHORT).show() },
            shape = CutCornerShape(5.dp),
        ) {
            Text(text = "Coins coupés")
        }
        Button(onClick = { Toast.makeText(context, "Arrondi", Toast.LENGTH_SHORT).show() },
            shape = RoundedCornerShape(10.dp),
        ) {
            Text(text = "Coins arrondis")
        }
        OutlinedButton(onClick = { Toast.makeText(context, "Outlined", Toast.LENGTH_SHORT).show() }
        ) {
            Text("Outlined")
        }
        OutlinedButton(onClick = { Toast.makeText(context, "Outlined épais", Toast.LENGTH_SHORT).show() },
            border = BorderStroke(2.dp, Color.Blue),
//            colors = ButtonDefaults.buttonColors(containerColor = Color(butColor) )
        ) {
            Text("Outlined épais")
        }
        Button(
            onClick = { backColor = backColor xor backMask },
            shape = RoundedCornerShape(10.dp),
            modifier = Modifier
                .size(140.dp, 60.dp)
        ) {
            Text(
                text = "Modifier\ncouleur fond",
                textAlign = TextAlign.Center
            )
        }
        Button(
            onClick = { butColor = butColor xor butMask },
            shape = RoundedCornerShape(10.dp),
            colors = ButtonDefaults.buttonColors(
                containerColor = Color(butColor),
                contentColor = Color.Yellow
            ),
            modifier = Modifier
                .width(120.dp)
                .height(60.dp)
        ) {
            Text(
                text = "Modifier\nma couleur",
                textAlign = TextAlign.Center
            )
        }
        TextButton(onClick = {Toast.makeText(context, "TextButton", Toast.LENGTH_SHORT).show()},
            border = BorderStroke(width = 1.dp, color = Color.Black),
            shape = RoundedCornerShape(10.dp),
            colors = ButtonDefaults.textButtonColors(
                containerColor = Color.Green.copy(alpha = 0.8f))
        )
        {
            Text(text = "+",
                fontSize = 28.sp,
                fontWeight = FontWeight.Bold,
            )
        }
    }
}



Box()

Box est un conteneur. Les élément contenus (fils) sont placés sur des plans superposés. On s'en sert par exemple s'il faut superposer un texte sur une image.

Un fils peut être positionné sur son plan grâce à ses modificateurs .align() et .offset()


Box()
package com.example.boximage

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

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

@Composable
fun MyApp() {
    Box(modifier = Modifier
        .size(200.dp, 200.dp)
        .background(Color.Cyan))
    {
        Button(onClick = { /*TODO*/ },
            modifier = Modifier
                .align(Alignment.CenterStart)
                .offset(y = -30.dp)
        )
        {
            Text(text = "Button 1")
        }
        Button(onClick = { /*TODO*/ },
            colors = ButtonDefaults.buttonColors(containerColor = Color.Magenta ),
            modifier = Modifier
                .align(Alignment.Center)
        )
        {
            Text(text = "Button 2")
        }
        Button(onClick = { /*TODO*/ },
            colors = ButtonDefaults.buttonColors(containerColor = Color.Gray ),
            modifier = Modifier
                .align(Alignment.CenterEnd)
                .offset(y = 30.dp)
        )
        {
            Text(text = "Button 3")
        }
    }
}


Icon()

Les icônes peuvent être définies de deux manières principales : avec ImageVector et avec painter.

  • ImageVector Icônes vectorielles standard. Elles sont définies en tant que vecteurs scalables, ce qui les rend adaptables à différentes tailles sans perte de qualité
  • Icon(imageVector = Icons.Default.Call, contentDescription = null)
    Dès qu'on a tapé Icons.Default. VScode affiche la liste des icônes disponibles
  • painter Représente une approche plus flexible pour afficher des images et des icônes personnalisées comme des clip Art xml ou des fichier SVG.
  • Les icônes doivent être placées dans le dossier drawable des ressources dans l'arborescence du projet.
    Pour Ajouter une icône clipArt xml dans le dossier drawable:
    Clic droit sur le dossier drawable → new → Vector Asset → clic sur l'icône affichée devant `Clip Art` → choisir
    Ensuite, ajouter les icônes à votre UI comme suit:
    Icon(painter = painterResource(id = R.drawable.outline_delete_24), contentDescription = null)

Row() contenant quelques icônes
Row(horizontalArrangement = Arrangement.SpaceAround,
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier
                .fillMaxWidth()
                .height(50.dp)
                .background(Color(0xFF33BB88)))
        {
            Icon(imageVector = Icons.Default.Call, contentDescription = null)
            Icon(imageVector = Icons.Default.Call, contentDescription = null, modifier = Modifier.size(48.dp))
            Icon(imageVector = Icons.Default.Build, contentDescription = null, tint = Color.Magenta)
            Icon(painter = painterResource(id = R.drawable.outline_bluetooth_24), contentDescription = null)
            Icon(painter = painterResource(id = R.drawable.outline_download_for_offline_24),
                contentDescription = null, tint = Color.Blue)
            Icon(painter = painterResource(id = R.drawable.outline_delete_24), contentDescription = null)
        }


IconButton(), FilledIconButton()

C'est tout simplement l'utilisation d'une Icône comme un bouton


IconButton(), FilledIconButton()
package com.example.iconbutton

import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp

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

@Composable
fun MyApp() {
    val context = LocalContext.current
    Row(horizontalArrangement = Arrangement.SpaceAround,
        modifier = Modifier
            .fillMaxWidth()
            .height(50.dp)
            .background(Color(0xFF43928B))
    )
    {
        IconButton(onClick = { Toast.makeText(context, "IconButton", Toast.LENGTH_SHORT).show() }) {
            Icon(imageVector = Icons.Default.Favorite, contentDescription = null)
        }
        IconButton(
            onClick = { Toast.makeText(context, "IconButton", Toast.LENGTH_SHORT).show() },
            colors = IconButtonDefaults.filledTonalIconButtonColors(
                containerColor = Color(0xFF8BC34A),
                contentColor = Color.Red
            )
        )
        {
            Icon(imageVector = Icons.Default.Favorite, contentDescription = null)
        }
        FilledIconButton(onClick = {
            Toast.makeText(context, "FilledIconButton", Toast.LENGTH_SHORT).show()
        }) {
            Icon(imageVector = Icons.Default.Favorite, contentDescription = null)
        }
        FilledIconButton(
            onClick = { Toast.makeText(context, "FilledIconButton", Toast.LENGTH_SHORT).show() },
            colors = IconButtonDefaults.filledTonalIconButtonColors(
                containerColor = Color(0xFF8BC34A),
                contentColor = Color.Red
            )
        )
        {
            Icon(imageVector = Icons.Default.Favorite, contentDescription = null)
        }

        FilledIconButton(
            onClick = { Toast.makeText(context, "FilledIconButton", Toast.LENGTH_SHORT).show() },
            shape = RoundedCornerShape(12.dp),  // Coins arrondis de 12.dp
            colors = IconButtonDefaults.filledTonalIconButtonColors(
                containerColor = Color(0xFF8BC34A),
                contentColor = Color.Red
            )
        ) {
            Icon(
                imageVector = Icons.Default.Favorite,
                contentDescription = "Favorite Icon",
            )
        }
    }
}



FloatingActionButton (FAB)

Le FloatingActionButton (FAB) est un bouton utilisé pour représenter une action principale ou très importante dans une interface utilisateur. Il s'agit d'un bouton qui "flotte" au-dessus du reste du contenu de l'interface, avec une icône à l'intérieur.

En réalité, le FAB est un bouton comme les autres. Il se distingue seulement par sa conception qui attire l'attention de l'utilisateur, grâce à son élévation qui lui donne un aspect flottant

Par contre, du point de vue positionnement, le FAB ne flotte pas au dessus des autres composants comme son nom pourrait le faire croire, et il n'est pas positionné automatiquement en bas de l'écran comme on peut le lire dans certains document.

Le positionnement du FAB dépend dans quel conteneur il est placé. Par exemple, si on le place dans une colonne qui contient déjà deux composables, il se positionnera normalement derrières ces composants. On pourra seulement le positionner horizontalement à l'aide du modifier align

Pour réellement faire flotter le FAB au dessus du reste du contenu et avoir beaucoup plus de souplesse de positionnement, il faut utiliser un Box() comme conteneur principal occupant tout l'écran. Dans ce Box(), on placera le contenu (dans une colonne par exemple) et le FAB, qui dans ce cas va se superposer à tout le reste et on peut le positionner horizontalement et verticalement à l'aide du modifier align


FloatingActionButton()
package com.example.floatingbutton

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Button
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

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

@Composable
fun MyApp() {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.LightGray)
    ) {
        Column(
            verticalArrangement = Arrangement.spacedBy(20.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            modifier = Modifier.fillMaxSize()
        ) {
            Button(onClick = { /*TODO*/ }) {
                Text("Button 1")
            }
            Button(onClick = { /*TODO*/ }) {
                Text("Button 2")
            }
        }

        FloatingActionButton(
            onClick = { /*TODO*/ },
            modifier = Modifier
                .align(Alignment.BottomEnd)  // Positionner en bas à droite
                .padding(16.dp)  // Ajouter un padding pour éloigner du bord
        ) {
            Icon(
                imageVector = Icons.Default.Add,
                contentDescription = "Add"
            )
        }
    }
}


Spacer()

Spacer permet d'ajouter un espace vide entre les éléments. Pratique pour gérer l'espacement dans les Row, Column,

Spacer(modifier = Modifier.height(20.dp)) // Espacement vertical
Spacer(modifier = Modifier.width(20.dp))  // Espacement horizontale
                    

Image()

Ce composable permet de charger et d'afficher une images à partir du dossier drawable ou même des images provenant d'une URL

Commencer par copier les images dans le dossier drawable de votre application. Le nom des fichiers ne doit contenir que des lettres minuscule (a - z), des chiffres (0 - 9) et le caractère underscore '_'



    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.fillMaxWidth()
    )
    {
        Image(
            painter = painterResource(id = R.drawable.android),
            contentDescription = null, modifier = Modifier.size(150.dp, 150.dp)
        )
        Spacer(modifier = Modifier.height(20.dp))
        Image(
            painter = painterResource(id = R.drawable.jpc),
            contentDescription = null
        )
        Spacer(modifier = Modifier.height(20.dp))
        Box(
            modifier = Modifier
                .size(200.dp, 200.dp)
                .background(Color.Cyan)
        ){
            Image(
                painter = painterResource(id = R.drawable.android),
                contentDescription = null,
                modifier = Modifier.align(Alignment.CenterEnd)
            )
        }
    }



TextField()

permet à l'utilisateur de saisir du texte pour interagir avec l'application

C'est un composant qui nécessite beaucoup de pratique pour cerner toutes ses possibilités

Concept de base

  • A l'aide de l'attribut value, on affecte une variable de type mutableStateOf("") au TextField
  • Quand on place le curseur dans le TextField, celui-ci prend le focus et le clavier apparaît,
  • A l'aide de l 'attribut onValueChange, on fait de sorte que chaque changement dans le champs de saisie est affectée à la variable. Comme cette dernière est de type mutableState, ceci affectera toutes les parties de l'UI qui utilisent cette variable
  • Si on on utilise l'attribut keyboardOptions pour afficher la touche de validation ✔ du clavier, alors, quand on clique sur cette touche, les tâches définies par l'action onDone de l'attribut keyboardActions sont exécutées

L'exemple ci-dessous crée deux champs de saisie TextField() et deux champs d'affichage Text(). On va les appeler Textfield1, Textfield2, Text1 et Text2. Les deux TextFields sont légèrement différents


TextField, premier exemple
package com.example.textfield

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
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.graphics.Color
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp

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

@Composable
fun MyApp() {
    var tf1variable by remember { mutableStateOf("") }
    var tf2variable by remember { mutableStateOf("") }
    var txt2 by remember { mutableStateOf("") }

    Column(
        verticalArrangement = Arrangement.spacedBy(20.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.fillMaxSize()
    )
    {
        Spacer(modifier = Modifier.height(40.dp))
        
        TextField(
            value = tf1variable,
            onValueChange = { tf1variable = it },
        )

        TextField(
            value = tf2variable,
            onValueChange = { tf2variable = it },
            keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),  // touche de validation
            keyboardActions = KeyboardActions(
                onDone = {
                    txt2 = tf2variable
                    tf2variable = ""
                }
            )
        )
        
        Text(
            text = tf1variable,
            modifier = Modifier
                .size(200.dp, 40.dp)
                .background(Color.LightGray)
                .padding(8.dp)
        )
        
        Text(
            text = txt2,
            modifier = Modifier
                .size(200.dp, 40.dp)
                .background(Color.LightGray)
                .padding(8.dp)
        )
    }
}

Si on clique dans Textfield1, le clavier apparaît comme indiqué sur la figure ci-dessous. On peut faire quelques observations:

  • Le trait en bas de Textfield1 devient épais pour indiquer que le composant a le focus
  • Le clavier n'a pas de touche de validation. La touche ↵ permet seulement de revenir à la ligne
  • Comme on a affecté la même variable à TextField1 et à Text1, tous ce que l'on tape dans le premier est affecté à la variable et donc apparaît aussi dans Text1
TextField 1

Si on clique dans TextField2, le clavier apparaît comme indiqué sur la figure ci-dessous. On peut faire quelques observations:

  • Le trait en bas de Textfield2 devient épais pour indiquer que le composant a le focus
  • Grace à l'attribut keyboardOptions, le clavier a une touche de validation ✔
  • Grace à l'action onDone de l 'attribut keyboardActions, quand on clique sur la touche de validation:
    1. Le contenu de TextFiels2 est affecté à la variable text2, donc il sera affiché dans le champ d'affichage Text2
    2. On affecte une chaîne vide à la variable tf2variable pour effacer le contenu de TextField2. C'est plus pratique pour se préparer à une nouvelle saisie

Apparence

L'apparence d'un TextField peut être ajustée à l'aide de ces attributs et ses modifiers.

  • L'attribut textStyle permet de définir divers aspects du texte comme la taille de la police, la famille de polices, la couleur, l'espacement des lettres, etc.
    textStyle = TextStyle(
                color = Color.Blue,              // Couleur du texte
                fontSize = 20.sp,                // Taille de la police
                fontWeight = FontWeight.Bold,    // Poids de la police (ex: gras)
                lineHeight = 24.sp,
                letterSpacing = 0.5.sp
            ),
  • L'attribut colors permet également de personnaliser les couleurs de différents éléments du TextField, tels que le texte, le curseur, les bordures, etc.
    colors = TextFieldDefaults.colors(
                    unfocusedContainerColor = Color(0xFFD6E4DE),
                    focusedContainerColor = Color.Cyan,
                    cursorColor = Color.Red,           // Couleur du curseur
                    focusedIndicatorColor = Color.Red // Couleur du trait bas lorsque le TextField est focalisé
                ),
  • L'attribut label est utilisé pour afficher un texte indicatif au-dessus du TextField, pour décrire le type de contenu que l'utilisateur doit saisir. Le label flotte au-dessus du champ de texte lorsque celui-ci est en focus ou que l'utilisateur a déjà entré du texte, et il revient à l'intérieur du champ lorsqu'il est vide et non focalisé
  • L'attribut placeholder est utilisé pour afficher un texte indicatif à l'intérieur du TextField lorsque celui-ci est vide. Il disparaît dès que l'utilisateur commence à saisir du texte
  • l'attribut shape permet de personnaliser la forme du champ de texte en utilisant des formes prédéfinies comme RoundedCornerShape, CutCornerShape, ou encore des formes personnalisées.
    • shape = RoundedCornerShape(...) : Permet de créer des coins arrondis avec un rayon spécifique
    • shape = CutCornerShape(...) : Crée une forme avec des coins coupés (pour un effet angulaire)
  • TextField(
        value = tf1variable,
        onValueChange = { tf1variable = it },
        label = { Text("Nom") },
        placeholder = { Text(text = "Entrez votre nom")},
        textStyle = TextStyle(
            color = Color.Blue,              // Couleur du texte
            fontSize = 20.sp,                // Taille de la police
            fontWeight = FontWeight.Bold,    // Poids de la police (ex: gras)
            letterSpacing = 2.sp,       // espacement des caractères
        ),
        colors = TextFieldDefaults.colors(
            unfocusedContainerColor = Color(0xFFD6E4DE), // couleur de fond non-focussé
            focusedContainerColor = Color.Cyan,     // couleur de fond quand focussé
            cursorColor = Color.Red,           // Couleur du curseur
            focusedIndicatorColor = Color.Red // Couleur du trait bas lorsque le TextField est focalisé
        ),
        shape = RoundedCornerShape(topStart = 15.dp, topEnd=15.dp),  // Coins arrondis de 12.dp
        modifier = Modifier.size(300.dp, 70.dp)   // Largeur et hauteur
                        )

    La figure ci-dessous montre l'apparence du TextField avant d'avoir le focus, après avoir le focus et après avoir entré un texte


Clavier

Le composable TextField gère plusieurs aspects liés au clavier virtuel, permettant de personnaliser l'apparence du clavier et les actions qu'il déclenche

  1. L'attribut keyboardOptions
  2. Ce paramètre permet de spécifier quelques options comme:
    • KeyboardType : Permet de choisir le type de clavier à afficher en fonction du type de contenu attendu dans le TextField. Par exemple, vous pouvez indiquer si le champ de texte doit accepter uniquement des chiffres, du texte multiligne, des mots de passe, etc
      • KeyboardType.Text : Clavier standard pour la saisie de texte.
      • KeyboardType.Number : Clavier numérique, idéal pour saisir des nombres.
      • KeyboardType.Decimal : Clavier numérique, similaire à Number.
      • KeyboardType.Phone : Clavier adapté à la saisie de numéros de téléphone.
      • KeyboardType.Email : Clavier optimisé pour la saisie d'adresses e-mail (comprend un accès rapide à "@" et ".").
      • KeyboardType.Password : Clavier pour la saisie de mots de passe, souvent utilisé en conjonction avec visualTransformation.
    • imeAction : Permet de choisir le type du bouton de validation (souvent en bas à droite du clavier) comme "Done", "Go", "Next", "Search", etc.
      • ImeAction.Done : Le clavier utilise la touche de validation ✔. On utilise cette option pour indiquer que l'utilisateur a terminé l'édition.
      • ImeAction.Next : Le clavier utilise la touche de validation . Cette option déplace le focus vers le champ suivant. Utilisé pour la navigation entre plusieurs TextField.
      • ImeAction.Previous : Le clavier utilise la touche de validation . Cette option déplace le focus vers le champ précédent. Utilisé pour la navigation entre plusieurs TextField.
      • ImeAction.Go : Le clavier utilise la touche de validation . Cette option indique que l'utilisateur est prêt à "aller" à l'action principale associée au champ de texte. Souvent utilisé pour déclencher une action immédiate comme une recherche ou une connexion
      • ImeAction.Search : Le clavier utilise la touche de validation 🔍 Cette option indique une action de recherche. Habituellement utilisé dans des champs de texte où l'utilisateur entre une requête de recherche
      • ImeAction.Send : Le clavier utilise la touche de validation ➤ Cette option est uUtilisés pour envoyer un message ou un e-mail.
    • capitalization : Première lettre en majuscule
      • KeyboardCapitalization.Words : La premiere lettre de chaque mot en majuscule
      • KeyboardCapitalization.Sentences : La premiere lettre de la saisie en majuscule
      • keyboardOptions = KeyboardOptions( capitalization = KeyboardCapitalization.Sentences)
  3. L'attribut keyboardActions
  4. Ce paramètre permet de définir des actions spécifiques à effectuer lorsque l'utilisateur interagit avec les boutons du clavier virtuel, comme "Done", "Next", "Search", etc.
    • onDone = {} : Action à effectuer lorsque l'utilisateur appuie sur le bouton "Done" du clavier.
    • onGo = {} : Action à effectuer lorsque l'utilisateur appuie sur le bouton "Go" du clavier.
    • onNext = {} : Action à effectuer lorsque l'utilisateur appuie sur le bouton "Next" du clavier.
    • onPrevious = {} : Action à effectuer lorsque l'utilisateur appuie sur le bouton "Previous" du clavier.
    • onSearch = {} : Action à effectuer lorsque l'utilisateur appuie sur le bouton "Search" du clavier.
    • onSend = {} : Action à effectuer lorsque l'utilisateur appuie sur le bouton "Send" du clavier.
  5. Fermer le clavier
  6. Quand on termine une saisie en cliquant sur la touche de validation, le clavier reste ouvert. Si on désire le fermer, une façon de le faire consiste à retirer le focus au TextField.

    1. Déclarer un FocusManager. Par exemple :
      val focusManager1= LocalFocusManager.current
    2. Ajouter l'instruction suivante à l'option onDone de l'attribut keyboardActions
      focusManager1.clearFocus()  // pour fermer le clavier

Donner/Retirer le focus

  1. Déclarer un Focusrequester et un FocusManager. Par exemple :
    val focusRecuaster1 = remember { FocusRequester() }
    val focusManager1= LocalFocusManager.current
  2. Ajouter le modifier focusRequester au TextField
    Modifier.focusRequester(focusRecuaster1)
  3. Utiliser les instructions suivantes dans un bloc de type onDone{ }, onClick{ }, .clickable{} , etc
    focusManager1.clearFocus()  // pour retirer le focus
    focusRecuaster1.requestFocus()    //pour donner le focus
    

    On peut aussi utiliser un LaunchedEffect pour exécuter ces instructions d'une façon asynchrone sans passer par un cliquable. Par exemple pour donner le focus à un composable au moment de l'exécution de l'application
    LaunchedEffect(focusRecuaster1) {
            focusRecuaster1.requestFocus()   // pour donner le focus
    }
    
    LaunchedEffect(focusManager1) {
            focusManager1.clearFocus()   // pour retirer le focus
    }
    

Détecter si TextField a le focus

  1. Déclarer une variable booléenne de type MutableState. Par exemple :
    var isTFfocused by remember { mutableStateOf(false) }
  2. Ajouter ce modifier au TextField
    Modifier.onFocusChanged {isTFfocused = it.isFocused }
  3. Maintenant vous pouvez utiliser la variable dans votre code. Elle aura la valeur true ou false selon si le TextField est focussé ou non

Problème de la taille verticale

Comme on peut le voir sur la figure ci-dessous, Le TextField (jaune) est un container qui contient un objet Text() (vert). Les marges entre les deux sont fixées par défaut et on ne peut pas les modifier. Quand on diminue la taille verticale du TextField(.weight ou .height), les marges sont conservées et c'est la taille du Text() qui diminue d'où le 'clipage' du texte. Si on veut agir sur ces marges, il faut utiliser BasicTextField()


OutlinedTextField()

OutlinedTextField est le même composable que TextField avec quelques différence visuelles. OutlinedTextField se distingue par:

  • Une bordure visible qui entoure le champ de texte avec les 4 coins arrondis
  • C'est la bordure qui joue le rôle d'indicateur de focus, il n'y a pas de trait en bas du composable
  • Le label s'affiche par dessus la bordure, ce qui lui donne une allure plutôt sympa

  • var tf2variable by remember { mutableStateOf("") }        
            OutlinedTextField(
                value = tf2variable,
                label = { Text("Prénom") },
                placeholder = { Text(text = "Taper votre prénom")},
                onValueChange ={textsaisi ->
                    tf2variable = textsaisi },
                textStyle = TextStyle(fontSize = 20.sp),
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(horizontal = 20.dp),
                shape = RoundedCornerShape(size = 10.dp),
                colors = TextFieldDefaults.colors(
                    unfocusedContainerColor = Color(0xFFD6E4DE),
                    focusedContainerColor = Color(0xFFE1F5F3),
                    cursorColor = Color.Blue,           // Couleur du curseur
                    focusedIndicatorColor = Color.Red // Couleur de bordure lorsque le TextField est focalisé
                ),
                keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), // bouton OK
                keyboardActions = KeyboardActions(
                    onDone = {
                        txt2 = tf2variable
                        tf2variable = ""
                        focusManager1.clearFocus()  // pour fermer le clavier
                    }
                ),
            )
    


Checkbox()

Le composable Checkbox fournit une case à cocher que l'utilisateur peut cocher ou décocher. Il est associé à une variable booléenne qui détermine son état, ainsi qu'une fonction lambda qui gère les modifications de cet état. Le composable ne propose pas un champ pour afficher un label à coté du Checkbox, il faut utiliser une Row() et y placer le Checkbox() et un Text()


Checkbox
package com.example.checkbox

import android.os.Bundle
import android.widget.Toast
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.material3.Checkbox
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.platform.LocalContext
import androidx.compose.ui.unit.dp

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

@Composable
fun MyApp() {
    var cb1state by remember { mutableStateOf(false) }
    var cb2state by remember { mutableStateOf(false) }
    var cb3state by remember { mutableStateOf(false) }
    val context = LocalContext.current
    Column(verticalArrangement = Arrangement.spacedBy(20.dp)) {
        Row(
            verticalAlignment = Alignment.CenterVertically
        ) {
            Checkbox(
                checked = cb1state,
                onCheckedChange = {
                    Toast.makeText(context, "Condition 1 est ${if (it) "Acceptée" else "refusée"}", Toast.LENGTH_SHORT).show()
                    cb1state = it }
            )
            Text("Accepter condition 1 ")
        }
        Row(
            verticalAlignment = Alignment.CenterVertically
        ) {
            Checkbox(
                checked = cb2state,
                onCheckedChange = { newstate ->
                    Toast.makeText(context, "Condition 2 est ${if (newstate) "Acceptée" else "refusée"}", Toast.LENGTH_SHORT).show()
                    cb2state = newstate }
            )
            Text("Accepter condition 2 ")
        }
        Row( verticalAlignment = Alignment.CenterVertically){
            Checkbox(
                checked = cb3state,
                onCheckedChange = { newstate ->
                    Toast.makeText(context, "Condition 3 est ${if (newstate) "Acceptée" else "refusée"}", Toast.LENGTH_SHORT).show()
                    cb3state = newstate }
            )
            Text("Accepter condition 3 ")
        }
    }
}



Switch()

Le composable Switch() fonctionne exactement comme un Checkbox(), il n'y a que le dessin qui n'est pas le même

Switch()
package com.example.switch

import android.os.Bundle
import android.widget.Toast
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.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
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.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp

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

@Composable
fun MyApp() {
    var switchState1 by remember { mutableStateOf(false) }
    var switchState2 by remember { mutableStateOf(false) }
    val context = LocalContext.current
    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        Row(
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text("Condition 1")
            Spacer(modifier = Modifier.width(8.dp))
            Switch(
                checked = switchState1,
                onCheckedChange = {
                    Toast.makeText(context, "Condition 1 est ${if (it) "Acceptée" else "refusée"}", Toast.LENGTH_SHORT).show()
                    switchState1 = it }
            )
        }

        Row(
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text("Condition 2")
            Spacer(modifier = Modifier.width(8.dp))
            Switch(
                checked = switchState2,
                colors = SwitchDefaults.colors(
                    checkedThumbColor = Color.Blue,
                    uncheckedThumbColor = Color.Red,
                    checkedTrackColor = Color.Magenta,
                    uncheckedTrackColor = Color.DarkGray
                ),
                onCheckedChange = {
                    Toast.makeText(context, "Condition 2 est ${if (it) "Acceptée" else "refusée"}", Toast.LENGTH_SHORT).show()
                    switchState2 = it }
            )
        }
    }
}



RadioButton()

RadioButton() permet de choisir une option parmi plusieurs


RadioButton()
package com.example.radiobutton

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.padding
import androidx.compose.material3.RadioButton
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.unit.dp

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

@Composable
fun MyApp() {
    val options = listOf("Option 1", "Option 2","Option 3", "Option 4", "Option 5")
    var selectedOption by remember { mutableStateOf(options[0]) }
    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(10.dp)
    ) {

        options.forEach { option ->
            Row(verticalAlignment = Alignment.CenterVertically) {
                RadioButton(
                    selected = selectedOption == option,
                    onClick = { selectedOption = option }
                )
                Text(option)
            }
        }
        Text(text = "L'option sélectionnée est : $selectedOption")
    }//fin colonne
}


Composable personnalisé

Il arrive qu'on soit amené à répéter plusieurs fois un composable avec une lourde mise en forme ce qui peut être assez fastidieux.

Dans ce cas il peut être intéressant de créer une fonction composable pour cet élément


TextButton personnalisé
package com.example.personalcomposable

import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

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

@Composable
fun MyApp() {
    val context = LocalContext.current
    Column(verticalArrangement = Arrangement.spacedBy(20.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.fillMaxSize())
    {
        Button(onClick = { /*TODO*/ }) {
            Text(text = "B0")
        }
        MyTextButton(
            text = "B1",
            onClick = { Toast.makeText(context, "B1", Toast.LENGTH_SHORT).show()}
        )
        MyTextButton(
            text = "B2",
            onClick = {Toast.makeText(context, "B2", Toast.LENGTH_SHORT).show()}
        )
        MyTextButton(
            text = "B3",
            onClick = {}
        )
        MyTextButton(
            text = "B4",
            onClick = {}
        )
        MyTextButton(
            text = "B5",
            onClick = {}
        )
    }
}
@Composable
fun MyTextButton(
    text: String,
    onClick: () -> Unit,
) {
    TextButton(
        onClick = onClick,
        border = BorderStroke(width = 1.dp, color = Color.Red),
        shape = RoundedCornerShape(10.dp),
        colors = ButtonDefaults.textButtonColors(
            containerColor = Color.Green.copy(alpha = 0.3f)),
        modifier = Modifier.size(width = 80.dp, height = 40.dp)
    ) {
        Text(text = text,
            fontSize = 20.sp,
            fontWeight = FontWeight.Bold)
    }
}

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



Alert Dialog

Le composant AlertDialog affiche une boite de dialogue pop-up par dessus l'interface utilisateur pour permettre à l'utilisateur d'effectuer des taches comme:

  • Confirmation d'une action, par exemple lors de la suppression d'un fichier,
  • Demander des entrées utilisateur, par exemple dans une application de liste de tâches,
  • Présenter une liste d'options, comme choisir un pays dans la configuration d'un profil

Le composable AlertDialog peut être personnalisé à l'aide de plusieurs paramètres spécifiques:

En général, les plus utilisés sont:

  • onDismissRequest = { ... }
  • permet de définir les tâches qui seront exécutées quand on désire quitter en cliquant en dehors du Dialogue
  • title = { ... }
  • Permet de définir le titre du dialogue qui s'affiche en haut de la boite. C'est un conteneur, en général, le titre est placé dans un composable Text(). On peut essayer d'y placer d'autres composables
  • texte = { ... }
  • C'est aussi un conteneur. Il s'affiche en dessous du titre. En général on y place un message dans un composable Text(). On peut aussi y placer d'autres composables comme un TextField() pour saisir une donnée,
  • confirmButton = { ... }
  • C'est aussi un conteneur. On y place le bouton de confirmation,
  • dismissButton = { ... }
  • C'est aussi un conteneur. On y place le bouton d'annulation,

Exemple
package com.example.alertdialog1

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
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.graphics.Color
import androidx.compose.ui.unit.sp

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

@Composable
fun MyApp() {
    var statusMSG by remember { mutableStateOf("Status inconu") }
    var showDialog1 by remember { mutableStateOf(false) }
    Column(horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.fillMaxSize()) {
        Button(onClick = {
            showDialog1 = true
        }) {
            Text(text = "Montrer Dialog")
        }
        Text(text = statusMSG)
    }
    if (showDialog1) {
        AlertDialog(
            onDismissRequest = {
                statusMSG = "Ignoré"
                showDialog1 = false
            },
            title = { Text(text = "Alerte!", color = Color.Red) },
            text = {  Text(text = "Voulez-vous vraiment supprimer le fichier", fontSize = 20.sp)  },
            confirmButton = {
                Button(onClick = {
                    statusMSG = "Confirmé"
                    showDialog1 = false
                }) {
                    Text(text = "Confirmer")
                }
            },
            dismissButton = {
                Button(onClick = {
                    statusMSG = "Annulé"
                    showDialog1 = false
                }) {
                    Text("Annuler")
                }
            }
        )
    }
}

L'interface utilisateur contient un bouton Montrer Dialog et un champ de texte. Quand on clique sur le bouton, une boite de dialogue s'ouvre. Au retour du Dialogue, le champ de texte affichera un status correspondant au choix qui a été fait dans le dialogue.

  • Si on quitte le dialogue en cliquant en dehors de la boite, on retourne avec le status Ignoré.
  • Si on quitte le dialogue en cliquant sur le bouton Confirmer, on retourne avec le status Confirmé.
  • Si on quitte le dialogue en cliquant sur le bouton Annuler, on retourne avec le status Annulé.

  • L'appel à AlertDialog ne peut se faire qu'à partir d'une fonction composable. C'est pour cette raison qu'on ne peut pas l'appeler dans le onClick du bouton Montrer Dialog. De ce fait, on crée une variable d'état booléenne (ici showDialog1 ) qui nous permettra de controller l'apparition de la boite de dialogue.

    L'appel à AlertDialog est placé dans la fonction composable MyApp() mais il est conditionné par l'état de la variable showDialog1

    Quand on clique sur le bouton Montrer Dialog, on place la variable à true. Le changement de la variable provoque la recomposition de MyApp() qui conduit à l'ouverture de la boite de dialogue puisque l'état de la variable est true

    A partir du dialogue, si on clique sur Confirmer, Annuler ou en dehors de la boite, alors la variable est placée à false. Le changement provoque la recomposition de L'UI entraînant la fermeture de la boîte de dialogue lors de la recomposition.


Les Modifiers

Chaque composable peut être caractérisé par un ensemble de paramètres (attributs) qui définissent ses propriétés spécifiques: taille, couleur, style ...

Parmi ces paramètre, il y en a un qui n'est pas comme les autres, c'est le paramètre modifier. A première vue, il semble avoir le même rôle que les autres attributs car il sert aussi à modifier l'apparence du composable. Mais il a un fonctionnement spécifique, on peut lui affecter l'objet (classe) Modifier auquel on peut attribuer plusieurs options, et ces options seront exécutées séquentiellement pour formater le composable.

Rien de mieux qu'un petit exemple

     Box(modifier = Modifier
        .size(300.dp, 300.dp)
        .background(Color.Yellow)
        .padding(40.dp)
        .background(Color.Cyan)
        .padding(40.dp)
        .background(Color.Magenta)
        .padding(40.dp)
        .border(width=5.dp, Color.Blue)
        .background(Color.Green)
    )