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éMainActivity
de notre projet BestPodcast, vous pouvez constater que la fonctionsetContent
contient 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 àGreeting
et laissons de côté BestPodcastTheme
etScaffold
dont nous découvrirons l’utilité plus tard.
Visiblement,Greeting
permet d’afficher un texte “Hello” qui peut être personnalisé avec un nom.Greeting
est 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 composantPodcastItem
gé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 condition
if (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.VISIBLE
permet de rendre visible explicitement l'icône.episodeDownloadedIcon.visibility = View.GONE
permet 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 àgone
dans 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éedownloaded
change 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 condition
if (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.DownloadDone
fournit l'icône représentant le statut téléchargé.contentDescription
fournit 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 est
Downloaded
, outre le fait de rendre visible l'icône via le codeepisodeDownloadedIcon.visibility = View.VISIBLE
il est nécessaire de masquer les autres icônes (InProgress
ouOnline
).Si le status est
InProgress
ouOnline
, 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
Remplacez le composant
Greeting
créé par défaut par Android Studio par notre composantPodcastItem
.Comparez les différences entre les paradigmes impératif et déclaratif du composant
FavoritePodcast
suivant.
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.