Développement Mobile

Premiers écrans Jetpack Compose : liste, formulaire et navigation

10 دقائق للقراءة

Une fois Android Studio installé et un projet Compose ouvert, le premier réflexe utile consiste à construire trois ou quatre écrans concrets : une liste, un formulaire, un détail, et la navigation qui les relie. Ce tutoriel reprend cette construction pas à pas avec Jetpack Compose 2026.05.00 et Material 3, en s’arrêtant sur les concepts qui reviennent dans tous les projets : composables, gestion d’état, listes optimisées, navigation type-safe, et thème. À la fin, vous aurez une mini-application fonctionnelle qui sert de socle réutilisable pour vos projets suivants.

Prérequis

  • Android Studio Otter 3 Feature Drop (2025.2.3) installé et fonctionnel
  • Projet Compose vide créé via le template Empty Activity (cf. Installer Android Studio Otter 3)
  • Émulateur Android 16 (API 36) ou device physique branché
  • Notions de Kotlin (fonctions, classes, lambdas)
  • Temps estimé : 90 minutes

Étape 1 — Comprendre la structure du point d’entrée

Avant d’écrire des composables, regardons ce que le template a généré. Le fichier MainActivity.kt contient une ComponentActivity dont la méthode onCreate appelle setContent { ... }. Le bloc passé à setContent est le point d’entrée Compose : tout ce qui s’affiche à l’écran descend d’ici. C’est l’équivalent du setContentView(R.layout.activity_main) de l’ancien monde, mais sans XML — on déclare directement en Kotlin.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            HelloKotlinTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Greeting(
                        name = "Android",
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }
    }
}

Cinq éléments à retenir. enableEdgeToEdge() étend l’UI sous les barres système (statut + navigation), un standard Material 3. HelloKotlinTheme est le thème généré par le template : il définit palette, typographie et formes. Scaffold est un conteneur Material 3 qui gère TopAppBar, BottomBar, FAB et padding système. innerPadding reflète l’espace réservé par le Scaffold à éviter (encoche, barres). Greeting est le premier composable.

Étape 2 — Écrire son premier composable

Un composable est une fonction Kotlin annotée @Composable. Elle peut appeler d’autres composables, lire des états, et émettre des éléments d’UI. Modifions Greeting pour afficher une carte simple avec titre et description, puis créons un nouveau composable WelcomeCard dans un fichier dédié WelcomeCard.kt.

@Composable
fun WelcomeCard(
    title: String,
    description: String,
    modifier: Modifier = Modifier
) {
    Card(
        modifier = modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = title,
                style = MaterialTheme.typography.titleLarge
            )
            Spacer(modifier = Modifier.height(8.dp))
            Text(
                text = description,
                style = MaterialTheme.typography.bodyMedium
            )
        }
    }
}

@Preview(showBackground = true)
@Composable
fun WelcomeCardPreview() {
    HelloKotlinTheme {
        WelcomeCard(
            title = "Mon premier composable",
            description = "Une carte Material 3 affichée par Compose."
        )
    }
}

Trois conventions importantes ressortent. D’abord, modifier: Modifier = Modifier est le paramètre par défaut de tout composable réutilisable : il permet à l’appelant de positionner, dimensionner et styliser. Ensuite, on lit le thème via MaterialTheme.typography et MaterialTheme.colorScheme — jamais de couleurs ou de tailles codées en dur. Enfin, @Preview rend le composable visible dans le panneau de prévisualisation d’Android Studio sans lancer l’application, ce qui accélère énormément l’itération visuelle.

Étape 3 — Afficher une liste avec LazyColumn

Pour afficher une liste d’éléments, on n’utilise pas Column avec une boucle for : ça composerait tous les éléments en mémoire d’un coup, et la performance s’effondrerait au-delà de 50 items. À la place, LazyColumn ne compose que les éléments visibles à l’écran, comme RecyclerView le faisait dans l’ancien monde.

data class Article(val id: Int, val titre: String, val resume: String)

@Composable
fun ListeArticles(
    articles: List<Article>,
    onArticleClick: (Article) -> Unit,
    modifier: Modifier = Modifier
) {
    LazyColumn(
        modifier = modifier.fillMaxSize(),
        contentPadding = PaddingValues(vertical = 8.dp)
    ) {
        items(
            items = articles,
            key = { it.id }
        ) { article ->
            WelcomeCard(
                title = article.titre,
                description = article.resume,
                modifier = Modifier.clickable { onArticleClick(article) }
            )
        }
    }
}

Quatre points clés. items(items, key = ...) fournit une clé stable par élément — indispensable pour que Compose puisse identifier un item à travers les recompositions et éviter de tout reconstruire à l’insertion/suppression. contentPadding ajoute un espace haut/bas sans déborder les éléments. clickable est un modifier qui transforme n’importe quel composable en zone cliquable avec un effet ripple Material 3 automatique. La lambda onArticleClick remonte l’événement au parent au lieu de gérer la navigation localement (state hoisting).

Étape 4 — Gérer l’état avec remember et mutableStateOf

L’état est le cœur de Compose. Sans état, l’écran reste figé. Pour un état local et éphémère (champ de formulaire, état d’ouverture d’un menu), on utilise remember { mutableStateOf(...) }. Construisons un formulaire simple avec un champ texte et un bouton.

@Composable
fun FormulaireRecherche(
    onRechercher: (String) -> Unit,
    modifier: Modifier = Modifier
) {
    var texte by remember { mutableStateOf("") }

    Column(modifier = modifier.padding(16.dp)) {
        OutlinedTextField(
            value = texte,
            onValueChange = { texte = it },
            label = { Text("Mot-clé") },
            modifier = Modifier.fillMaxWidth(),
            singleLine = true
        )
        Spacer(modifier = Modifier.height(12.dp))
        FilledTonalButton(
            onClick = { onRechercher(texte) },
            enabled = texte.isNotBlank(),
            modifier = Modifier.align(Alignment.End)
        ) {
            Text("Rechercher")
        }
    }
}

Deux idées centrales. by remember { mutableStateOf("") } avec délégation Kotlin : texte se comporte comme une var normale, mais toute lecture pendant une recomposition est suivie par Compose, et toute écriture déclenche une nouvelle recomposition des seuls composables qui ont lu cette valeur. Et enabled = texte.isNotBlank() illustre la puissance du modèle : le bouton se désactive automatiquement quand le champ est vide, sans listener manuel.

À noter : remember survit aux recompositions, mais pas aux changements de configuration (rotation, mort de processus). Pour cela, utilisez rememberSaveable { mutableStateOf("") }. La règle pratique : tout état qui doit survivre à une rotation passe par rememberSaveable ou remonte au ViewModel.

Étape 5 — Naviguer entre écrans avec Navigation Compose

Navigation Compose 2.12 introduit l’API type-safe : on déclare des routes comme des classes ou objets Kotlin sérialisables, et le compilateur vérifie les arguments. Plus de chaînes magiques à concaténer. Configurons une navigation entre une liste et un détail.

@Serializable object Liste
@Serializable data class Detail(val articleId: Int)

@Composable
fun AppNavigation() {
    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = Liste
    ) {
        composable<Liste> {
            ListeArticles(
                articles = articlesExemple,
                onArticleClick = { article ->
                    navController.navigate(Detail(article.id))
                }
            )
        }
        composable<Detail> { backStackEntry ->
            val detail: Detail = backStackEntry.toRoute()
            DetailArticle(
                articleId = detail.articleId,
                onRetour = { navController.popBackStack() }
            )
        }
    }
}

Trois apports majeurs. Les routes sont des objets/data classes annotés @Serializable (kotlinx.serialization) : pas de risque d’erreur de typo. navController.navigate(Detail(id)) passe l’argument typé. backStackEntry.toRoute<Detail>() récupère l’argument typé côté destination. La navigation gère automatiquement la pile (back button, geste retour), et l’animation de transition s’applique par défaut.

Pour ajouter le bouton « précédent » dans la TopAppBar, on intercale un Scaffold par écran ou un Scaffold global qui change de TopAppBar selon la destination via currentBackStackEntryAsState(). La deuxième option est plus simple pour des applications à 3-5 écrans.

Étape 6 — Personnaliser le thème Material 3

Le template a généré un fichier ui/theme/Theme.kt avec un HelloKotlinTheme qui choisit entre palette claire et palette sombre selon le système. Sur Android 12+, par défaut, il utilise les couleurs dynamiques extraites du fond d’écran (Material You). Vous pouvez forcer une palette de marque en désactivant le dynamique :

@Composable
fun HelloKotlinTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = false,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context)
            else dynamicLightColorScheme(context)
        }
        darkTheme -> darkColorScheme(primary = Color(0xFF1E88E5))
        else -> lightColorScheme(primary = Color(0xFF1565C0))
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

La règle d’or pour les couleurs : on ne code jamais une couleur en dur dans un composable. On la lit depuis MaterialTheme.colorScheme (primary, secondary, surface, onSurface, etc.). Ça garantit la cohérence claire/sombre et facilite les futures évolutions de marque.

Étape 7 — Tester sa première interaction complète

Pour valider qu’on tient bien le modèle, voici un dernier exercice. Ajoutez un compteur Material 3 sur l’écran d’accueil : un texte affichant un nombre, un bouton « + » et un bouton « − ». Le code complet tient en quinze lignes Kotlin et illustre tous les concepts vus jusque-là : composable réutilisable, état avec remember, Material 3, modifier composé.

@Composable
fun Compteur(modifier: Modifier = Modifier) {
    var valeur by rememberSaveable { mutableIntStateOf(0) }
    Row(
        modifier = modifier.padding(16.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        FilledTonalIconButton(onClick = { valeur-- }) {
            Icon(Icons.Default.Remove, contentDescription = "Décrémenter")
        }
        Text(
            text = valeur.toString(),
            style = MaterialTheme.typography.headlineMedium
        )
        FilledTonalIconButton(onClick = { valeur++ }) {
            Icon(Icons.Default.Add, contentDescription = "Incrémenter")
        }
    }
}

Lancez l’application, tapez sur les boutons et observez la valeur changer. Pivotez l’écran : rememberSaveable et mutableIntStateOf (variante optimisée pour les entiers, évite l’autoboxing) préservent l’état. Vous avez désormais validé tous les blocs fondamentaux : composables, état, recomposition, Material 3, accessibilité (contentDescription), et survie aux changements de configuration. Le reste de l’apprentissage Compose consiste à empiler ces blocs et à les connecter à un ViewModel pour de la logique métier durable.

Erreurs fréquentes

Symptôme Cause Solution
L’écran ne se met pas à jour quand l’état change var x = ... au lieu de var x by remember { mutableStateOf(...) } Toujours envelopper l’état observé dans mutableStateOf
Liste lente sur 1000 items Utilisation de Column + boucle au lieu de LazyColumn Passer à LazyColumn avec key stable
Animation/clignotement à l’ajout d’item Pas de key dans items() Fournir key = { it.id }
Champ texte qui perd sa valeur à la rotation remember au lieu de rememberSaveable Utiliser rememberSaveable ou remonter au ViewModel
Bouton qui ne se désactive pas Logique enabled évaluée hors d’une lecture d’état S’assurer que la condition lit la mutableStateOf
Compose preview vide Composable lit LocalContext ou autres locals non disponibles en preview Préfixer la preview avec un CompositionLocalProvider ou injecter en paramètre

Adapter aux différentes tailles d’écran

Compose est responsive par construction. Modifier.fillMaxWidth() s’adapte automatiquement à la largeur disponible, qu’on soit sur un Galaxy Fold replié (380 dp) ou déplié (840 dp). Pour des layouts vraiment différents selon la classe d’écran, currentWindowAdaptiveInfo() fournit un WindowSizeClass (Compact / Medium / Expanded). On peut afficher une liste pleine largeur en Compact et une vue maître-détail côte-à-côte en Medium/Expanded sans dupliquer le code.

Pour les images, Image avec contentScale = ContentScale.Crop couvre la majorité des cas. Pour les images réseau, ajoutez Coil 3 : implementation("io.coil-kt.coil3:coil-compose:3.4.0") et utilisez AsyncImage(model = url, contentDescription = ...). Coil gère cache disque, cache mémoire et placeholders nativement, sans configuration.

Foire aux questions

Faut-il connaître XML pour Compose ?
Non. Compose remplace XML pour la couche UI. Vous croiserez encore du XML pour les ressources (couleurs, chaînes, drawables vectoriels) mais plus pour les layouts.

Quand utiliser Column / Row / Box ?
Column empile verticalement, Row horizontalement, Box superpose. Pour des layouts complexes, ConstraintLayout Compose existe aussi mais on l’utilise rarement : Column + Row couvrent 95 % des cas.

Comment passer une callback à un composable enfant ?
Toujours en paramètre, jamais via une variable globale. C’est le pattern state hoisting : les données descendent en paramètre, les événements remontent en lambdas.

Quelle taille pour les boutons et touches ?
Material 3 impose 48 dp minimum pour toute zone tactile (recommandation accessibilité). Les Button et IconButton respectent ça par défaut.

Comment afficher un dialogue ?
AlertDialog avec un état var showDialog by remember { mutableStateOf(false) } qu’on bascule depuis un bouton. Le dialogue se compose conditionnellement (if (showDialog) AlertDialog(...)).

Combien de composables par fichier ?
Pas de règle stricte. Un fichier par écran avec les sous-composables privés au-dessous. Si un composable dépasse 100 lignes, le découper en sous-composables.

Pour aller plus loin

Vous savez désormais construire une UI Compose statique. L’étape suivante consiste à brancher cette UI à un état métier qui survit aux changements de configuration : un ViewModel exposant un StateFlow. C’est l’objet du tutoriel ViewModel et StateFlow avec Jetpack Compose. Pour la vue panoramique de la stack Android native moderne, voir le guide principal Kotlin et Jetpack Compose.

Ressources et références

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité