Aller au contenu
Tous les articles

Edge-to-edge et retour prédictif sur Android en 2026 : ce qui est désormais obligatoire, et comment ne pas casser votre interface

L'edge-to-edge est désormais imposé sur API 35+ et le retour prédictif est le geste par défaut. Un guide pratique sur les insets Compose, WindowInsets et PredictiveBackHandler.

MFKAPPS 6 min de lecture

Deux changements d’interface Android ne sont plus optionnels ce cycle. Les applications ciblant l’API 35+ ont l’edge-to-edge imposé — on ne peut plus s’en désister et laisser le système dessiner des barres opaques à votre place. Et le retour prédictif, ce geste de balayage arrière qui prévisualise votre destination avant que vous ne le validiez, est désormais l’interaction par défaut sur les appareils à navigation gestuelle, et non plus un indicateur expérimental. Aucun des deux n’est une refonte. Les deux vont visiblement casser une interface qui n’a pas été conçue pour eux, et le mode d’échec est le même dans les deux cas : ça fonctionne parfaitement sur l’émulateur que vous avez testé et ça semble cassé sur un vrai téléphone avec une encoche ou une navigation à trois boutons désactivée.

Voici ce qui a réellement changé, et le code Compose qui garde les écrans corrects sur cinq applications, dans les deux cas.

L’edge-to-edge n’est plus un choix

Avant l’API 35, enableEdgeToEdge() était quelque chose que vous activiez pour un écran spécifique — un plus agréable pour les applications qui voulaient que le contenu s’écoule sous une barre de statut translucide. Cibler l’API 35 supprime ce choix : le comportement de Window.setDecorFitsSystemWindows(false) est imposé, les barres système deviennent transparentes, et votre contenu racine se dessine derrière elles, que vous ayez appelé quoi que ce soit ou non.

L’effet pratique : toute mise en page qui supposait que la barre de statut et la barre de navigation réservaient leur propre espace voit désormais le contenu commencer au pixel physique zéro. Le titre d’une barre d’application supérieure se retrouve sous l’horloge. Les icônes d’une barre de navigation inférieure se retrouvent sous la pilule de geste. Ce n’est pas un problème de finition visuelle — sur un vrai appareil, cela signifie que les utilisateurs ne peuvent pas toucher la rangée inférieure de votre interface.

La solution n’est pas « ajouter du padding partout ». C’est appliquer les WindowInsets à la bonne limite, une seule fois, pour que les barres système réservent de l’espace exactement là où le contenu en a besoin, et nulle part ailleurs.

setContent {
    AppTheme {
        Scaffold(
            contentWindowInsets = WindowInsets.safeDrawing,
            topBar = { TopAppBar(title = { Text("Stocky") }) },
        ) { padding ->
            LazyColumn(contentPadding = padding) {
                items(pantryItems) { PantryRow(it) }
            }
        }
    }
}

Scaffold prend déjà en compte la barre supérieure et la barre inférieure qu’on lui donne, donc la plupart des écrans n’ont besoin que de connecter ce seul paramètre. Le piège est de le faire deux fois : Scaffold applique les insets safe-drawing à sa valeur padding, et si un enfant appelle aussi .systemBarsPadding() par-dessus, vous obtenez le double de l’écart sous la barre de statut — une bande vide qui ressemble à un bug de design, mais qui est en réalité un bug dans le code des insets.

Où les insets doivent être gérés à la main

Tous les écrans ne passent pas par Scaffold. Trois endroits que j’ai dû corriger à la main sur Granyn, Stocky et Mintly :

  • Les feuilles inférieures (bottom sheets) et les dialogues. Un ModalBottomSheet dessine sa propre surface en dehors du padding du Scaffold parent, donc son contenu a besoin de son propre .navigationBarsPadding() sur la colonne la plus interne, sinon la dernière ligne se retrouve sous la barre de geste.
  • Le clavier. WindowInsets.ime est un inset séparé de navigationBars, et il s’anime à mesure que le clavier s’ouvre et se ferme. Un champ de texte épinglé en bas d’un écran a besoin de .imePadding(), pas de .navigationBarsPadding() — les deux se chevauchent quand le clavier est fermé et divergent dès qu’il s’ouvre.
  • Les images ou en-têtes plein cadre. Quand vous voulez délibérément que le contenu se dessine sous la barre de statut — une galerie de captures d’écran, une image héros — appliquez les insets aux contrôles interactifs superposés (bouton retour, icône de partage) avec .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top)) plutôt que de mettre du padding sur tout le conteneur, sinon vous perdez l’effet recherché.

L’habitude qui rattrape la plupart de ces problèmes avant qu’un utilisateur ne le fasse : activez la navigation gestuelle et la personnalisation « Demo mode » de la barre de statut dans l’émulateur, et vérifiez chaque écran avec une encoche simulée activée. La navigation à trois boutons et une barre de statut simple masquent presque tous les bugs d’insets que vous risquez de livrer.

Retour prédictif : d’une coupe franche à une prévisualisation

L’autre changement est comportemental, pas une question de mise en page. Le retour prédictif permet au système de montrer où atterrira un geste de retour — une prévisualisation rétrécissante de l’écran sous-jacent — avant que l’utilisateur ne lève le doigt, afin qu’il puisse voir la destination et annuler en cours de geste. Les applications qui n’ont pas encore adopté ce comportement obtiennent toujours un échange d’écran instantané et plat ; le geste fonctionne, mais il ressemble à une coupe brutale à côté de chaque autre application compatible avec le retour prédictif sur l’appareil.

Adopter ce comportement nécessite un indicateur dans le manifeste plus un callback Compose :

<!-- AndroidManifest.xml -->
<application android:enableOnBackInvokedCallback="true">
PredictiveBackHandler(enabled = showDetail) { progress ->
    try {
        progress.collect { backEvent ->
            // backEvent.progress: 0f (début) → 1f (validé)
            scale = 1f - (backEvent.progress * 0.1f)
        }
        showDetail = false // geste validé
    } catch (e: CancellationException) {
        scale = 1f // geste annulé — retour à l'état initial
    }
}

PredictiveBackHandler (de androidx.activity.compose) vous donne un Flow d’événements de progression au lieu d’un simple callback, ce qui est la partie qui piège les gens venant de BackHandler. Vous le collectez pendant toute la durée du geste, animez en fonction de backEvent.progress, et la CancellationException sur un geste annulé est le chemin de sortie normal, pas une erreur — c’est la façon dont le système vous dit que l’utilisateur a relâché avant de valider. J’ai utilisé exactement cela pour l’écran de détail de session de Mintly : la vue du minuteur se réduit maintenant légèrement et révèle la liste derrière elle au fur et à mesure du balayage arrière, et revient proprement à sa taille normale si vous relâchez tôt, au lieu que l’écran disparaisse simplement.

Ce qu’il faut vraiment vérifier avant de publier

  • Chaque écran s’affiche correctement avec la navigation gestuelle activée et la navigation à trois boutons désactivée — c’est le mode dans lequel la plupart des nouveaux appareils sont livrés.
  • Aucun espace en double sous la barre de statut (le piège Scaffold + .systemBarsPadding() manuel ci-dessus).
  • Les feuilles inférieures et les snackbars dégagent la barre de geste sur un appareil sans boutons de navigation matériels.
  • Les champs de texte près du bas d’un écran utilisent .imePadding() et ne sautent pas quand le clavier s’ouvre.
  • Au moins un écran avec une vraie navigation (pas seulement le premier que vous testez) a le retour prédictif câblé, pour que le geste ne semble pas incohérent d’un écran à l’autre.

Rien de tout cela n’est difficile une fois qu’on y est confronté. C’est juste invisible sur un émulateur standard avec navigation à trois boutons — ce qui est exactement la configuration que la plupart d’entre nous laissent par défaut — et exactement pourquoi ça se retrouve cassé plus souvent qu’il ne le devrait.