• 12 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 01/10/2024

Comprenez le fonctionnement de Jetpack Compose

C'est maintenant le moment d’explorer votre premier composant et vous familiariser avec les particularités de Jetpack Compose.

Appréhendez votre premier composant en Jetpack Compose

Découvrez le composant “Greeting”

Si vous regardez à nouveau le code présent dans l’activitéMainActivityde notre projet BestPodcast, vous pouvez constater que la fonctionsetContentcontient le code suivant :

setContent {
   BestPodcastTheme {
       Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
           Greeting(
               name = "Android",
               modifier = Modifier.padding(innerPadding)
           )
       }
   }
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
   Text(
       text = "Hello $name!",
       modifier = modifier
   )
}

Pour le moment, focalisons notre attention sur le code relatif àGreetinget laissons de côté  BestPodcastThemeetScaffolddont nous découvrirons l’utilité plus tard.

Visiblement,Greetingpermet d’afficher un texte “Hello” qui peut être personnalisé avec un nom.Greetingest en fait un composant Jetpack Compose. Ce qui doit vous sauter aux yeux c’est le fait que ce composant soit déclaré sous forme de fonction et non plus comme une classe ou un objet dans une interface utilisateur traditionnelle.

Découvrez le composantPodcastItem

Continuons notre découverte de Jetpack Compose avec un cas concret. Dans notre application BestPodcast finale, il existe un composant qui permet d’afficher le titre d’un épisode et, à côté, une icône indiquant si l’épisode est téléchargé sur l’appareil de la personne utilisatrice. Avant que je vous montre le code permettant de réaliser ce composant en Jetpack Compose, voyons comment nous l’aurions fait en XML et Kotlin.

Option 1 : Développer en XML et Kotlin 

Pour développer ce composant en XML et Kotlin, il faut écrire quelque chose comme :

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical">

   <TextView
       android:id="@+id/episodeTitleText"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>

   <ImageView
       android:id="@+id/episodeDownloadedIcon"
       android:layout_width="30dp"
       android:layout_height="30dp"
       android:contentDescription=”Téléchargé"
       android:src="@drawable/ic_downloaded"/>

</LinearLayout>

Pour dynamiser ce composant dans le code Kotlin de l’activité, du fragment ou de la vue associée, cela donne :

val episodeTitleText = this.findViewById<TextView>(R.id.episodeTitleText)
val episodeDownloadedIcon = this.findViewById<ImageView>(R.id.episodeDownloadedIcon)

episodeTitleText.text = title

if (downloaded) {
   episodeDownloadedIcon.visibility = View.VISIBLE
} else {
   episodeDownloadedIcon.visibility = View.GONE
}

Option 2 : Développer en Kotlin avec Jetpack Compose 

Maintenant avec Jetpack Compose, le code de ce composant ressemble à ça :

@Composable
fun PodcastItem(
   title: String,
   downloaded: Boolean,
) {
   Row {
       Text(
           text = title,
       )
       if (downloaded) {
           Icon(
               imageVector = Icons.Default.DownloadDone,

               contentDescription = "Téléchargé",
           )
       }
   }

 Comparez les deux options 

Quand on compare les deux manières de faire, elles n’ont pas grand chose en commun ! Au-delà d’une syntaxe qui paraît beaucoup plus concise avec Jetpack Compose, voici les grandes différences à remarquer entre ces deux options.

  • L’usage d’un seul langage, Kotlin, avec Jetpack Compose, contre la nécessité d’utiliser à la fois XML et Kotlin pour la première option. 

  • En Jetpack Compose, la déclaration et la dynamisation de l'interface se font au même endroit, éliminant ainsi le besoin d'associer un identifiant à un composant pour le dynamiser.

  • L'utilisation d'une fonction pour définir un composant, notamment grâce à l'annotation @Composable. Nous y reviendrons plus en détail dans le prochain chapitre “Explorez la recomposition et la gestion d’état”

  • Avant, il était important de masquer, par exemple, la visibilité de l'icône episodeDownloadedIcon pour garantir la cohérence de notre interface. L’objectif était de représenter fidèlement l'état actuel des données (par exemple, quels épisodes ont été téléchargés), pour offrir une vue précise et actualisée à l'utilisateur. Avec Jetpack Compose, ce n'est pas nécessaire. Cette différence découle directement du paradigme de programmation utilisé.

  • D'un côté, Jetpack Compose adopte la programmation déclarative, tandis que la version XML et Kotlin utilisent la programmation impérative.

Prenons le temps de rentrer dans le détail de cette dernière observation.

Explorez le paradigme déclaratif

Comprenez la différence entre approches déclarative et impérative

Imaginez que vous cuisiniez un plat traditionnel à la main, en suivant scrupuleusement chaque étape de la recette. C'est comme écrire du code ligne par ligne, contrôlant chaque détail de l'implémentation. C’est ce qu’il se passe quand on adopte l’approche impérative. Ça peut être chronophage et sujet aux erreurs ! 

Maintenant, imaginez plutôt que vous utilisiez un robot de cuisine pour préparer le plat. Vous ajoutez les ingrédients, appuyez sur un bouton et le robot effectue le travail pour vous. Vous vous concentrez simplement sur le résultat final, tout comme dans une approche déclarative en programmation où vous décrivez simplement ce que vous voulez obtenir, laissant le framework générer le code nécessaire pour vous.

Ce qu’il faut comprendre c’est que la programmation impérative est un paradigme qui se concentre sur la description des résultats souhaités mais aussi sur les étapes pour les obtenir. Avec cette approche, on se préoccupe à la fois du  “Quoi ?” et du "Comment ?" Alors qu’avec la programmation déclarative, on répond simplement à la question "Quoi ?". Grâce à cette approche, nous n'avons pas à nous inquiéter des modifications (ajout, suppression ou mise à jour de données) qui pourraient survenir dans le temps. En revanche, avec la version XML et Kotlin associée à l’approche impérative, il est nécessaire d'indiquer au programme comment modifier l'interface en fonction des éventuels changements sur les données. 

Gérez la visibilité d’une icône avec l’approche impérative (XML et Kotlin)

Pour mieux comprendre, reprenons la partie du composantPodcastItemgérant la visibilité de l’icône de téléchargement.

if (downloaded) {
   episodeDownloadedIcon.visibility = View.VISIBLE
} else {
   episodeDownloadedIcon.visibility = View.GONE
}

Analysons ce code.

  • La conditionif (downloaded)vérifie si l'épisode a été téléchargé.

  • Si l'épisode est téléchargé, nous définissons la visibilité de l'icône à VISIBLE, sinon à GONE.

  • episodeDownloadedIcon.visibility = View.VISIBLEpermet de rendre visible explicitement l'icône.

  • episodeDownloadedIcon.visibility = View.GONEpermet de masquer explicitement l'icône.

Dans cette approche, nous manipulons directement les propriétés des vues en fonction de l'état. Nous devons explicitement indiquer à l'interface utilisateur ce qu'il faut faire lorsque l'état change.

Mais pourquoi avec l’option XML et Kotlin ne peut-on pas gérer uniquement le cas visible sans se soucier du cas invisible ? 

L’état initial de notre composant serait défini àgonedans l’XML :

 <ImageView
       android:id="@+id/episodeDownloadedIcon"
        /*...*/
   android:visibility="gone"


/>

Ensuite, cela reviendrait à gérer les mises à jour de la visibilité dans le code Kotlin uniquement :

if (downloaded) {
   episodeDownloadedIcon.visibility = View.VISIBLE
}

Ça ne fonctionnerait pas dans tous les cas, parce qu'avec la syntaxe XML et Kotlin, il est nécessaire de prendre en compte tous les cas particuliers possibles. Vous savez, ces situations spécifiques qui surviennent dans des scénarios bien précis, mais que l'on oublie souvent.... Dans notre exemple, imaginons que la donnéedownloadedchange au fil du temps parce que le téléchargement de l'épisode vient de se terminer, ou inversement, parce qu'il n'est plus disponible localement. Si l'utilisateur de votre application reste sur la même page, il ne remarquera pas ce changement car aucune instruction explicite n'aura été donnée pour masquer l'icône à nouveau. L'icône de téléchargement resterait visible alors qu'elle ne devrait plus l'être. Pour que l'interface réagisse correctement à ces changements d'état, il faut également gérer le cas où l'icône doit être masquée. D'où la nécessité d'écrire un code qui traite les deux cas, comme ceci :

if (downloaded) {
   episodeDownloadedIcon.visibility = View.VISIBLE
} else {
   episodeDownloadedIcon.visibility = View.GONE


}

À mesure que la logique métier devient plus complexe avec la programmation impérative, il devient nécessaire de prendre en considération les différents changements possibles. Et ça, ça entraîne naturellement une augmentation de la complexité du code.

Gérez la visibilité d’une icône avec l’approche déclarative (Jetpack Compose)

Regardons maintenant comment est gérée la visibilité de l’icône dans la version Kotlin. Dans cette approche, nous déclarons simplement que si la condition est remplie, l'icône doit être affichée. Jetpack Compose se charge de mettre à jour l'interface utilisateur en fonction de l'état actuel.

if (downloaded) {
   Icon(
       imageVector = Icons.Default.DownloadDone,
       contentDescription = "Téléchargé",
   )
}

Analysons ce code en détail.

  • La conditionif (downloaded)vérifie si l'épisode a été téléchargé.

  • Si la condition est vraie (l'épisode a été téléchargé), une icône est affichée à l'écran.

  • imageVector = Icons.Default.DownloadDonefournit l'icône représentant le statut téléchargé.

  • contentDescriptionfournit une description de l'icône pour les personnes aveugles et malvoyantes utilisant un lecteur d’écran, ici"Téléchargé".

Gérez le statut d’une icône avec l’approche impérative dans un cas complexe

Pour illustrer la complexité que résout la programmation déclarative, prenons par exemple le cas où l'icône à côté du nom de l'épisode varie en fonction du statut de téléchargement : disponible en ligne, en cours de téléchargement ou déjà téléchargé. Ce statut de téléchargement est représenté par l'énumération suivante.

enum class DownloadStatus {
   Online, InProgress, Downloaded
}

Avec la version XML et Kotlin, le code permettant d’afficher correctement le statut devient celui-ci.

when(status) {
   DownloadStatus.Downloaded -> {    
    episodeDownloadedIcon.visibility = View.VISIBLE
    	episodeOnlineIcon.visibility = View.GONE
    	episodeInProgressIcon.visibility = View.GONE
   }
   DownloadStatus.InProgress -> {
  	episodeInProgressIcon.visibility = View.VISIBLE
       episodeDownloadedIcon.visibility = View.GONE
       episodeOnlineIcon.visibility = View.GONE
   }
   DownloadStatus.Online -> {
       episodeOnlineIcon.visibility = View.VISIBLE
       episodeDownloadedIcon.visibility = View.GONE
       episodeInProgressIcon.visibility = View.GONE
   }
}

Vous remarquez ici que :

  • Si le statut estDownloaded, outre le fait de rendre visible l'icône via le code  episodeDownloadedIcon.visibility = View.VISIBLEil est nécessaire de masquer les autres icônes (InProgressouOnline). 

  • Si le status estInProgressouOnline, la même logique est appliquée. 

Cette logique liée au statut illustre la nécessité de prévoir toutes ses évolutions possibles afin de garantir que l'écran affiche correctement les icônes à tout moment, sans en afficher plusieurs à la fois.

Gérez le statut d’une icône avec l’approche déclarative dans un cas complexe

Voici l’équivalent en Jetpack Compose.

@Composable
fun PodcastItem(
   title: String,
   status: DownloadStatus,
) {
   Row {
       Text(
           text = title,
       )
       when (status) {
           DownloadStatus.Online ->
               Icon(
                   imageVector = Icons.Default.Cloud,
                   contentDescription = "Disponible sur le réseau",
               )
           DownloadStatus.InProgress ->
               Icon(
                   imageVector = Icons.Default.Downloading,
                   contentDescription = "Téléchargement en cours",
               )

           DownloadStatus.Downloaded ->
               Icon(
                   imageVector = Icons.Default.DownloadDone,
                   contentDescription = "Téléchargé",
               )
       }
   }
}

Ici, grâce au paradigme déclaratif utilisé par Jetpack Compose, vous remarquez que nous n'avons pas besoin de nous préoccuper de masquer les autres icônes pour que notre interface reflète correctement le statut de téléchargement à tout moment.

À vous de jouer !

Contexte

Vous savez maintenant à quoi ressemble un composant avec Jetpack Compose et en quoi l’approche de programmation est différente. Il est maintenant temps de développer un premier composant pour votre application BestPodcast.

Consignes

  1. Remplacez le composantGreetingcréé par défaut par Android Studio par notre composant  PodcastItem.

  2. Comparez les différences entre les paradigmes impératif et déclaratif du composant  FavoritePodcastsuivant. 

Exemple en XML avec code Kotlin associé (impératif)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:padding="8dp">
    <TextView
        android:id="@+id/PodcastItem"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"/>
    <ImageButton
        android:id="@+id/favoriteButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_favorite_border"
        android:contentDescription="Favorite"/>
</LinearLayout>
data class Podcast(val title: String, var isFavorite: Boolean)
class MainActivity : AppCompatActivity() {
    private lateinit var podcast: Podcast
    private lateinit var PodcastItem: TextView
    private lateinit var favoriteButton: ImageButton

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        podcast = Podcast("Sample Podcast", false)
        PodcastItem = findViewById(R.id.PodcastItem)
        favoriteButton = findViewById(R.id.favoriteButton)
        updateUI()
        favoriteButton.setOnClickListener {
            podcast.isFavorite = !podcast.isFavorite
            updateUI()
        }
    }
    private fun updateUI() {
        PodcastItem.text = podcast.title
        if (podcast.isFavorite) {
            favoriteButton.setImageResource(R.drawable.ic_favorite)
            favoriteButton.contentDescription = "Remove from favorites"
        } else {
            favoriteButton.setImageResource(R.drawable.ic_favorite_border)
            favoriteButton.contentDescription = "Add to favorites"
        }
    }
}
Exemple en Jetpack Compose (déclaratif)
@Composable
fun FavoritePodcast(podcast: Podcast, onFavoriteClick: (Podcast) -> Unit) {
    Row(
        modifier = Modifier.fillMaxWidth().padding(8.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            text = podcast.title,
            modifier = Modifier.weight(1f)
        )
        IconButton(onClick = { onFavoriteClick(podcast) }) {
            Icon(
                imageVector = if (podcast.isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
                contentDescription = if (podcast.isFavorite) "Remove from favorites" else "Add to favorites"
            )
        }
    }
}

data class Podcast(val title: String, var isFavorite: Boolean)

Livrables

Votre projet sur Android Studio doit être conforme aux consignes ci-dessus. Pour réaliser la seconde tâche, vous pouvez utiliser un format texte.

En résumé

  • Avec Jetpack Compose,  l'interface utilisateur est écrite avec un seul langage, Kotlin.

  • Chaque composant est défini par une fonction annotée avec @Composable.

  • La programmation déclarative, caractéristique de Jetpack Compose, se concentre sur le "Quoi".

  • La programmation impérative utilisée précédemment en XML, se concentrait sur le “Quoi” et le "Comment", ce qui augmentait la complexité du code.

Après avoir compris les différences entre la programmation déclarative et impérative, il est essentiel d'explorer la recomposition et la gestion d'état avec Jetpack Compose. Ces concepts sont clés pour maximiser les avantages de ce paradigme. 

Exemple de certificat de réussite
Exemple de certificat de réussite