L'API SplashScreen d'Android en 2026 : un démarrage à froid sans flash blanc
Un guide pratique de l'API SplashScreen d'Android — configuration du thème, les limites de taille de l'icône animée que personne ne lit, et keepOnScreenCondition pour les données pas encore prêtes.
Chaque application Android affiche un écran de démarrage depuis Android 12, que vous l’ayez demandé ou non. Le système en dessine un automatiquement à partir de l’icône de votre application et des couleurs de votre thème dès que l’utilisateur touche le lanceur — le seul choix que vous ayez réellement est d’accepter le défaut ou de configurer androidx.core.splashscreen pour dessiner celui que vous aviez prévu de livrer. La plupart des écrans de démarrage ratés que j’ai vus ne sont pas ratés parce que quelqu’un les a mal conçus. Ils sont ratés parce que personne n’a rien configuré du tout, et que la meilleure estimation du système a comblé le vide.
J’ai fait chaque erreur de cet article en construisant Granyn, dont le tableau de bord a besoin d’un premier montant de solde chargé depuis Room avant que la première image ne vaille la peine d’être affichée. Voici la configuration qui a arrêté le flash blanc et arrêté l’écran de démarrage de traîner un peu trop longtemps.
Pourquoi vous avez un écran de démarrage que vous le vouliez ou non
À partir d’Android 12 (API 31), la plateforme intercepte chaque démarrage à froid et affiche une fenêtre de démarrage dessinée par le système avant même que votre Activity n’attache sa vue de contenu. Ce n’est pas une fonctionnalité de bibliothèque que vous pouvez ignorer — c’est le comportement par défaut du framework. Ce que androidx.core.splashscreen vous offre, c’est une couche de compatibilité qui vous permet de configurer cette fenêtre système de manière cohérente jusqu’à l’API 23, au lieu d’obtenir le vrai comportement sur 12+ et un simple rectangle blanc ou noir sur tout le reste.
Sautez la configuration et vous obtenez le défaut du système : l’icône de votre lanceur sur une couleur unie tirée de votre thème, aucune animation, aucun contrôle sur sa durée d’affichage. Ce n’est pas cassé, mais c’est rarement ce qu’une application avec une identité de marque souhaite, et sur les appareils antérieurs à 12 sans la bibliothèque de compatibilité, cela peut être un flash blanc gênant avant même que votre thème ne s’applique.
Câbler le thème
L’écran de démarrage se configure presque entièrement en XML, comme un thème appliqué avant l’exécution de setContentView :
<!-- res/values/themes.xml -->
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/brand_background</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<item name="windowSplashScreenAnimationDuration">500</item>
<item name="postSplashScreenTheme">@style/Theme.App</item>
</style>
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
setContent { AppTheme { GranynApp() } }
}
}
installSplashScreen() doit s’exécuter avant super.onCreate() — l’appeler après, et la bibliothèque de compatibilité ne peut pas intercepter la fenêtre à temps, et vous revenez silencieusement au défaut du système. postSplashScreenTheme est le thème vers lequel la fenêtre bascule une fois l’écran de démarrage fermé ; oublier de le définir laisse votre Activity brièvement coincée à rendre avec des attributs de fenêtre de démarrage appliqués au contenu réel.
La limite de taille d’icône que personne ne lit avant de s’y faire mordre
windowSplashScreenAnimatedIcon a une contrainte stricte : le drawable est rendu à l’intérieur d’un cercle de 240×240dp, et tout ce qui est plus grand est silencieusement rogné plutôt que mis à l’échelle. C’est le bug d’écran de démarrage le plus courant que je vois — quelqu’un réutilise l’icône de lanceur adaptative, conçue pour une couche de premier plan de 108×108dp à l’intérieur d’un cercle visible de 72×72dp, et elle déborde soit du cadre de l’écran de démarrage, soit se retrouve rognée d’une manière qui semble cassée sur exactement une seule densité d’écran.
La solution est une icône de démarrage dédiée, dimensionnée et centrée pour la contrainte de 240dp par elle-même, et non une ressource de lanceur détournée :
res/drawable/splash_icon.xml (un seul vecteur, sans couches adaptatives, tient dans 240x240dp)
Si l’icône doit être animée (un AnimatedVectorDrawable), le même plafond de taille s’applique, et windowSplashScreenAnimationDuration limite le temps que la plateforme attend pour elle — 1000ms maximum sur API 31+, bien que la bibliothèque de compatibilité sur les API plus anciennes n’applique pas ce plafond de la même manière, ce qui est en soi une source d’incohérence de test si vous ne vérifiez qu’une seule version d’OS.
Quand les données ne sont pas prêtes : keepOnScreenCondition
La partie qui comptait vraiment pour Granyn n’était pas l’icône — c’était le timing. L’écran de démarrage se ferme dès que la première image est prête à être dessinée, ce qui pour un écran Compose peut arriver avant que le ViewModel n’ait quoi que ce soit à afficher. Laissé tel quel, cela produit un flash de tableau de bord vide entre la disparition de l’écran de démarrage et le chargement du solde une demi-seconde plus tard — pire que l’écran de démarrage restant simplement affiché ce moment supplémentaire.
SplashScreen.setKeepOnScreenCondition maintient la fenêtre de démarrage ouverte jusqu’à ce qu’une condition se libère :
class MainActivity : ComponentActivity() {
private val viewModel: DashboardViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
splashScreen.setKeepOnScreenCondition {
!viewModel.isReady.value
}
setContent { AppTheme { GranynApp(viewModel) } }
}
}
isReady ici est un StateFlow<Boolean> qui passe à vrai une fois que la première requête Room pour le solde du tableau de bord retourne réellement un résultat. La condition est interrogée à chaque image, gardez-la donc comme une lecture booléenne bon marché — jamais un appel suspendu ou un accès à la base de données directement dans le lambda.
Deux choses rendent cela sûr plutôt qu’un moyen de bloquer l’application indéfiniment. D’abord, fixez un plafond — si isReady ne passe jamais à vrai (une base de données corrompue, une requête bloquée), l’écran de démarrage doit expirer et laisser l’interface afficher son propre état de chargement ou d’erreur plutôt que de rester figée indéfiniment :
private val isReady = MutableStateFlow(false)
init {
viewModelScope.launch {
withTimeoutOrNull(2000) {
dashboardFlow.first { it != null }
}
isReady.value = true
}
}
Ensuite, setKeepOnScreenCondition ne retarde que la sortie — il ne bloque ni la saisie ni le démarrage du reste de l’application, donc ne l’utilisez pas comme substitut à un véritable état de chargement plus bas dans l’arborescence. Il vous achète au maximum quelques centaines de millisecondes de patience avant que cela ne se lise comme un blocage de l’application.
L’animation de sortie, si le fondu compte
Par défaut, la vue de démarrage est simplement supprimée. Si un fondu ou une mise à l’échelle personnalisée en sortie compte pour l’identité de marque, setOnExitAnimationListener vous donne la vue réelle de démarrage à animer avant de la fermer vous-même :
splashScreen.setOnExitAnimationListener { splashView ->
splashView.view.animate()
.alpha(0f)
.setDuration(200)
.withEndAction { splashView.remove() }
.start()
}
Oublier splashView.remove() à la fin laisse la vue de démarrage attachée indéfiniment au-dessus de votre contenu — un oubli d’une seule ligne facile à faire, qui semble correct dans une boucle de développement à rechargement à chaud puis livre un écran de démarrage définitivement figé.
Ce qui comptait vraiment
Le défaut que le système vous donne quand vous ne configurez rien n’est pas cassé, mais il n’est pas non plus intentionnel — installez le thème de démarrage avant super.onCreate(), dimensionnez l’icône pour la contrainte réelle de 240dp au lieu de réutiliser l’icône de lanceur adaptative, et ne recourez à keepOnScreenCondition que lorsqu’il y a une donnée spécifique et chronométrée dont la première image a réellement besoin. L’animation de sortie est la partie qu’il est facile de sauter entièrement, et pour la plupart des applications, c’est le bon choix — le gain ici est de supprimer le flash blanc et le scintillement du tableau de bord vide, pas d’ajouter du mouvement pour lui-même.
// À lire aussi
D’autres notes du journal
La couleur dynamique Material You dans Jetpack Compose : garder sa couleur de marque quand le fond d'écran gagne
dynamicColorScheme() remplace votre palette par une palette construite à partir du fond d'écran de l'utilisateur. Un guide pratique pour harmoniser plutôt que perdre ses couleurs de marque.
L'API In-App Review d'Android : demander une note sans être pénible
Un guide pratique de l'API In-App Review de Google — comment elle fonctionne réellement, quand la déclencher, et pourquoi la popup classique « notez-nous » nuit discrètement à votre note sur le Play Store.
L'accessibilité sur Android en 2026 : une checklist TalkBack et sémantique Compose qui tient la route
Une checklist pratique d'accessibilité Android pour 2026 — TalkBack, sémantique Compose, zones tactiles et la passe de test que je fais sur chaque écran avant publication.