Les composants avec Android et Jetpack Compose

Sommaire

Introduction

Dans ce TP nous allons mettre en pratique l'écriture de composants avec Android Compose. De la logique à la réalisation, nous allons parcourir les bases de la réalisation de composants et de « la réactivité » en lien avec ceux-ci.

Aperçu du projet

L'application que nous allons réaliser ici est très simple. Il s'agit juste d'une liste et d'une vue de détail. Elle nous servira à comprendre le système de composant.

Application

Création du projet

Pour la création du projet, rien de spécial à prévoir. Il s'agit ici de suivre le processus de création d'une application comme habituellement. Pour ça nous allons utiliser « Android Studio » qui est l'IDE à utiliser pour créer une application Android.

Lors de la création, Android Studio va nous poser plusieurs questions, nous allons donc choisir :

  • Template : Empty Compose Activity
  • Language : Kotlin
  • SDK Min. : SDK 26. (ou plus)

Je vous laisse suivre les étapes de création d'un nouveau projet.

Création d'un projet Étape 1Création d'un projet Étape 2

Mais quelques petites remarques :

  • Le choix du package est très important. Comme nous avons vu ensemble en cours, le « Package » doit être unique. En effet deux applications ne peuvent pas avoir le même.
  • Choisir un min SDK qui correspond aux cibles des mobiles souhaités. (Si vous êtes en France ou dans un autre pays, il conviendra de faire le bon choix).
  • Kotlin est maintenant le langage à choisir, Java et Kotlin cohabite sans problème vous n'aurez donc aucun problème de compatibilité.

Un premier lancement

Pour débuter (et avant de tout casser), je vous laisse compiler et lancer une première fois l'application proposée par Google. Le premier résultat va être incroyable, accrochez-vous…

Première application

Dans mon cas l'application est sur la droite.

STOP ! Analysons ensemble ce que nous avons obtenu.

Analyse code source

Nous allons donc retravailler tout ça pour organiser un minimum notre code source.

Un peu de structure !

Avant d'aller plus loin, nous allons ajouter un peu de structure dans le microprojet. L'idée étant de faire une application qui ressemble à quelque chose. Nous allons ajouter la structure d'une application basique à savoir une TopAppBar.

TopAppBar

Je vous laisse modifier le code de la méthode onCreate, pour y mettre :

setContent {
    TestComposantTheme {
        Scaffold(
            topBar = { TopAppBar(title = {Text("Top App Bar")},backgroundColor = MaterialTheme.colors.primary)  },
        ) {
            Greeting(name = "Valentin")
        }
    }
}

L'importance de la sémantique

Il est important d'utiliser les bons termes. En développement mobile, nous avons une terminologie qu'il faut respecter. Ici la barre en haut ne se nomme pas « la barre en haut », mais une AppBar.

À faire :

Je vous laisse tester votre application à nouveau.

Votre premier composant

Avant de réaliser le code, nous allons dans un premier temps créer un nouveau package. Il nous servira à stocker nos composants.

Création du package, la procédure est intégrée dans Android Studio : Création package

Nommage du package, dans mon cas « composants » : Nommage

Maintenant que votre package est créé, je vous laisse créer le fichier Kotlin qui contiendra votre code :

Création fichier composantCréation fichier composant suite

Pour le nom du fichier, je vous laisse choisir. Moi dans mon cas je vais le nommer « composant_list_item.kt ».

Un instant !

Pas de classe !?

Et non avec Compose, les composants ne sont pas des classes. Ce sont des fonctions « Composable » qui seront appelées au bon moment suivant les bonnes conditions dans votre vue.

Le code de votre composant

L'idée ici est de vous faire constater le bon fonctionnement. Voilà le code de votre premier composant :

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Card
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
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
import com.example.testcomposant.R
import com.example.testcomposant.ui.theme.TestComposantTheme

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ElementList(
    title: String = "Mon titre",
    content: String = "Mon contenu",
    image: Int? = R.drawable.ic_launcher_foreground,
    onClick: () -> Unit = {}
) {
    Card(modifier = Modifier.fillMaxWidth().padding(5.dp), onClick = onClick) {
        Row(modifier = Modifier.padding(5.dp), verticalAlignment = Alignment.CenterVertically) {
            image?.let {
                Image(modifier = Modifier.height(50.dp), painter = painterResource(id = it), contentDescription = content)
            }

            Column() {
                Text(text = title)
                Text(text = content, fontWeight = FontWeight.Light, fontSize = 10.sp)
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    TestComposantTheme {
        ElementList()
    }
}

Arrêtons-nous un instant, que constatez-vous dans le code que vous avez copié-collé sans trop réfléchir 👀…

  • @Composable au-dessus de la fonction, indique l'emplacement d'un composant.
  • @Preview(showBackground = true) permet de réaliser une preview de votre composant sans la lancer sur un téléphone (pratique, testons).
  • Les variables (title, content, image, onClick) seront le moyen de customiser notre composant.

Je vous laisse mettre en place le code. Et valider que celui-ci s'affiche correctement dans la partie preview.

Preview composant

Utiliser votre premier composant

Nous avons réalisé notre premier composant, nous allons maintenant utiliser le composant dans notre application.

Pour ça nous allons créer une liste (LazyColumn étant l'équivalent d'un RecyclerView, mais en beaucoup plus simple) Celui-ci contiendra le composant que vous avez créé.

val myData = listOf("Card 1","Card 2","Card 3","Card 4","Card 5","Card 6","Card 7","Card 8","Card 9","Card 10")

LazyColumn {
    items(myData) { item ->
        ElementList(title = item) {
            // Code appelé lors du clique sur un élément de la liste.
        }
    }
}

Je vous laisse mettre le code en question à la place du Greeting dans le composant Scaffold.

Vous devez obtenir :

Vous allez obtenir

Rendre votre liste interactive

Maintenant que notre liste s'affiche, nous allons la rendre interactive lors du touch / clique de l'utilisateur sur un élément de la liste. Nous allons avoir besoin de deux choses :

  • Une variable qui permettra de connaitre quel élément à été cliqué.
  • Une condition (if) pour savoir si nous devons afficher la LazyColumn ou seulement un ElementList.

Pour la variable, la déclaration de celle-ci est un peu particulière :

var selectedItem by remember { mutableStateOf<String?>(null) }

Ajouter cette ligne après votre variable myData.

C'est à vous

Je vous laisse ajouter la condition pour :

  • Afficher la LazyColumn ou ElementList en fonction de selectedItem != null.

Gestion du bouton retour

Si tout fonctionne comme prévu, vous devez avoir la possibilité de sélectionner un élément. Mais pour l'instant pas moyen de revenir sur la liste entière.

Pour ça nous allons ajouter un nouvel attribut dans notre TopAppBar

navigationIcon = {
    if (selectedItem != null) {
        IconButton(onClick = { selectedItem = null })
        { Icon(Icons.Default.ArrowBack, "Back") }
    }
},

Cet attribut va ajouter un bouton retour si un élément est sélectionné. Je vous laisse mettre en place ça dans votre code.

Utiliser toute la puissance des composants

Vous l'avez peut-être remarqué, pour l'instant notre liste est très simple. Vous avez déclaré une liste de string, je vais vous demander d'allez un peu plus loin. Vous allez créer une liste d'objet (plutôt que string).

Évidemment, je vais vous demander d'organiser tout ça.

  • Créer un package nommé models :

Create Package

  • Ajouter dans ce package une Classe (data class pour être précis) :
data class CardContent(val title: String, val content: String, @DrawableRes val image: Int?)

Data class ?

Étrange cette classe n'est-ce pas ?

Nous créons fréquemment des classes dont le but principal est de conserver des données. Dans une telle classe, certaines fonctionnalités standard et fonctions utilitaires sont souvent dérivables mécaniquement à partir des données. Dans Kotlin, cela s'appelle une classe de données et est marqué comme data.

C'est à vous, je vous laisse modifier votre liste (nommé myData) par une liste de CardContent (la classe que vous avez créée). Cette liste doit contenir 10 éléments avec des titres, des contenus et une image différente que celle par défaut (le logo de l'ESEO par exemple).

Besoin d'aide ?

Avez-vous cherché ? Si oui, voilà un indice :

val myData = listOf(CardContent("Card 1", "Contenu de la card 1", R.drawable.logo),)

Découper encore plus

Nous avons créé un composant, c'est bien, mais nous pouvons faire bien plus. En reprenant la logique du composant ElementList, je vous laisse créer un composant nommé EseoTopAppBar qui contiendra toute la logique de votre TopAppBar :

TopAppBar(
    navigationIcon = {
        if (selectedItem != null) {
            IconButton(onClick = { selectedItem = null })
            { Icon(Icons.Default.ArrowBack, "Back") }
        }
    },
    title = { Text("Top App Bar") },
    backgroundColor = MaterialTheme.colors.primary
)

Une fois ce composant créé (dans le bon dossier), je vous laisse l'utilisé directement dans votre Scaffold.

Rappel

Paramètres

Les éléments encadrés en rouge sont les paramètres de chaque méthode. Certains sont des strings, d'autres des actions, mais un paramètre peut-être un composant. C'est le cas ici avec topBar =.

Je vous laisse tester que votre application fonctionne toujours.

Ajouter des animations

Une liste c'est bien ! Mais une liste avec des animations c'est encore mieux !

AnimatedContent (/* Votre Condition OU votre état */ ) { targetState ->
    when (targetState) {
        true -> { /* Composant affiché quand la condition est vrai */ }
        false -> { /* Composant affiché quand la condition est fausse */ }
    }
}

Je vous laisse intégrer ce code dans votre application afin d'animer le changement d'état. Après intégration vous devriez obtenir :

Aller plus loin avec les animations

Aide mémoireopen in new window

Source: Twitteropen in new window

Utiliser les ressources textes

Avoir des composants ne veut pas dire oublier les bonnes pratiques bien au contraire ! Pour finaliser votre application, je vous laisse sortir les différents textes dans le fichier. strings.xml

Une fois vos textes externalisés, vous pouvez rendre visible via :

getString(R.string.id_de_votre_string_dans_le_xml)

Je vous laisse utiliser cette méthode aux différents endroits ou vous avez mis du texte « en dur ».

Réaliser une interface avec Jetpack Compose c'est un peu comme assembler des Lego. Vos composants sont vos briques et vous allez devoir les assembler pour créer votre application.

Prenons les composants que nous avons créés :

  • Une liste d'éléments.
  • Une card représentant un élément.

Finalement, nous avons lié ces deux composants ensemble en utilisant un état. Lors de la sélection d'un élément, nous avons changé l'état de notre application pour afficher la card de l'élément sélectionné.

C'est un peu comme si nous avions une pièce dans laquelle nous pouvions entrer et sortir. Pour entrer et sortir, nous utilisons l'action onClick, le onClick nous permet de changer l'état de notre application.

Dans cet exemple, nous avons utilisé un état pour gérer l'affichage de notre card. Mais nous pouvons aussi utiliser un état pour gérer la sélection d'un élément :

(C'est un exemple, à ne pas copier-coller)

val selectedItem = remember { mutableStateOf(null) }

if (selectedItem.value == null) {
    ElementList(selectedItem)
} else {
    ElementCard(selectedItem.value!!)
}

Prenons un autre exemple, dans un cas simple de deux composants d'interface, nous pourrions écrire quelque chose comme :

val isLoggedIn = remember { mutableStateOf(false) }

if (isLoggedIn.value) {
    HomeScreen()
} else {
    LoginScreen() {
        // L'action sera émise par le composant LoginScreen. Via un bouton par exemple.
        isLoggedIn.value = true
    }
}

Ici nous avons un état qui nous permet de savoir si l'utilisateur est connecté ou non. Il faut donc comprendre que le code permet de :

  • Si l'utilisateur est connecté, afficher l'écran d'accueil.
  • Si l'utilisateur n'est pas connecté, afficher l'écran de connexion.

Évidemment, il est possible de faire plus complexe, mais pour l'instant nous allons nous concentrer sur ces deux exemples.

Exemple de code pour nos deux composants et de leur utilisation

Les deux composants :

@Composable
fun HomeScreen() {
    Text("Bienvenue sur votre application")
}

@Composable
fun LoginScreen(onLogin: () -> Unit) {
    Button(onClick = onLogin) {
        Text("Se connecter")
    }
}

L'utilisation de ces deux composants, dans un Scaffold :

@Composable
fun MyApp() {
    Scaffold(
        topBar = {
            TopAppBar(
                title = {
                    Text("Mon application")
                }
            )
        }
    ) {
        val isLoggedIn = remember { mutableStateOf(false) }

        if (isLoggedIn.value) {
            HomeScreen()
        } else {
            LoginScreen() {
                isLoggedIn.value = true
            }
        }
    }
}

Aller plus loin dans la navigation

Vous souhaitez aller plus loin ? Là c'est un exemple très simple, pour découvrir. Dans une application plus complexe, nous aurions besoin de Compose Navigation pour gérer les transitions entre les composants. Vous pouvez retrouver un exemple d'utilisation de Compose Navigation dans la documentation officielle : Compose Navigationopen in new window.

Compose Navigation est un composant qui permet de gérer les transitions entre les composants. Il permet de gérer les animations, les transitions, les arguments, etc. C'est l'équivalent d'un routeur dans une application web (react-router, vue-router, etc.)

Bonus : Adapter la TopAppBar

Actuellement votre TopAppBar est statique, elle contient toujours la même chose à savoir « Top App Bar ». Je vous propose de réfléchir à comment faire pour que celle-ci s'adapte et change en fonction de l'élément choisi, en fonctionnant en suivant la logique :

  • Aucun élément = « Nom de votre application ».
  • Un élément choisi = « Titre de l'élément choisi ».

Et les animations

N'oubliez pas, les animations sont le détail qui change tout. Je vous laisse regarder pour faire en sorte d'animer le changement de titre.

Le résultat de la TopAppBar

Mis à jour le:
Contributeurs: Valentin Brosseau