Saltar al contenido
Todas las entradas

La API SplashScreen de Android en 2026: arranque en frío sin el destello blanco

Una guía práctica de la API SplashScreen de Android — configuración del tema, los límites de tamaño del icono animado que nadie lee, y keepOnScreenCondition para datos que aún no están listos.

MFKAPPS 6 min de lectura

Toda aplicación Android muestra una pantalla de bienvenida desde Android 12, lo hayas pedido o no. El sistema dibuja una automáticamente a partir del icono de tu aplicación y los colores de tu tema en el instante en que el usuario toca el lanzador — la única elección que realmente tienes es aceptar el valor predeterminado o configurar androidx.core.splashscreen para dibujar la que pensabas lanzar. La mayoría de las malas pantallas de bienvenida que he visto no son malas porque alguien las diseñó mal. Son malas porque nadie configuró nada, y la mejor suposición del sistema llenó el vacío.

Cometí todos los errores de este artículo construyendo Granyn, cuyo panel necesita una cifra de saldo inicial cargada desde Room antes de que el primer fotograma valga la pena mostrarse. Esta es la configuración que detuvo el destello blanco y detuvo que la pantalla de bienvenida se quedara un momento de más.

Por qué obtienes una pantalla de bienvenida la quieras o no

A partir de Android 12 (API 31), la plataforma intercepta cada arranque en frío y muestra una ventana de bienvenida dibujada por el sistema antes de que tu Activity siquiera adjunte su vista de contenido. Esto no es una función de biblioteca que puedas omitir — es el comportamiento predeterminado del framework. Lo que androidx.core.splashscreen te da es una capa de compatibilidad que te permite configurar esa ventana del sistema de forma consistente hasta la API 23, en lugar de obtener el comportamiento real en 12+ y un simple rectángulo blanco o negro en todo lo demás.

Omite la configuración y obtienes el valor predeterminado del sistema: el icono de tu lanzador sobre un color sólido extraído de tu tema, sin animación, sin control sobre cuánto tiempo se mantiene. Eso no está roto, pero rara vez es lo que quiere una aplicación con identidad de marca, y en dispositivos anteriores a la 12 sin la biblioteca de compatibilidad puede ser un destello blanco molesto antes de que tu tema siquiera se aplique.

Conectando el tema

La pantalla de bienvenida se configura casi por completo en XML, como un tema aplicado antes de que se ejecute 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() tiene que ejecutarse antes de super.onCreate() — llámalo después, y la biblioteca de compatibilidad no puede interceptar la ventana a tiempo, y vuelves silenciosamente al valor predeterminado del sistema. postSplashScreenTheme es el tema al que cambia la ventana una vez cerrada la pantalla de bienvenida; olvidar configurarlo deja tu Activity brevemente atascada renderizando con atributos de ventana de bienvenida aplicados al contenido real.

El límite de tamaño del icono que nadie lee hasta que le muerde

windowSplashScreenAnimatedIcon tiene una restricción estricta: el drawable se renderiza dentro de un círculo de 240×240dp, y cualquier cosa más grande se recorta silenciosamente en lugar de escalarse. Este es el error de pantalla de bienvenida más común que veo — alguien reutiliza el icono de lanzador adaptativo, diseñado para una capa de primer plano de 108×108dp dentro de un círculo visible de 72×72dp, y o bien desborda el límite de la pantalla de bienvenida o queda recortado de una manera que se ve rota en exactamente una densidad de pantalla.

La solución es un icono de bienvenida dedicado, dimensionado y centrado para la restricción de 240dp por sí solo, no un recurso de lanzador reutilizado:

res/drawable/splash_icon.xml   (un solo vector, sin capas adaptativas, cabe en 240x240dp)

Si el icono necesita animación (un AnimatedVectorDrawable), aplica el mismo tope de tamaño, y windowSplashScreenAnimationDuration limita cuánto espera la plataforma por él — 1000ms como máximo en API 31+, aunque la biblioteca de compatibilidad en APIs más antiguas no aplica ese tope de la misma manera, lo cual es en sí una fuente de inconsistencia en las pruebas si solo verificas una versión de sistema operativo.

Cuando los datos no están listos: keepOnScreenCondition

La parte que realmente importó para Granyn no fue el icono — fue el tiempo. La pantalla de bienvenida se cierra en cuanto el primer fotograma está listo para dibujarse, lo cual para una pantalla Compose puede ocurrir antes de que el ViewModel tenga algo que mostrar. Dejado así, esto produce un destello de panel vacío entre la desaparición de la pantalla de bienvenida y la carga del saldo medio segundo después — peor que la pantalla de bienvenida simplemente permaneciendo ese momento extra.

SplashScreen.setKeepOnScreenCondition mantiene abierta la ventana de bienvenida hasta que una condición se libera:

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 aquí es un StateFlow<Boolean> que pasa a verdadero una vez que la primera consulta de Room para el saldo del panel realmente devuelve algo. La condición se sondea en cada fotograma, así que mantenla como una lectura booleana barata — nunca una llamada suspendida o un acceso a base de datos directamente dentro del lambda.

Dos cosas hacen que esto sea seguro en lugar de una forma de colgar la aplicación indefinidamente. Primero, establece un tope — si isReady nunca pasa a verdadero (una base de datos corrupta, una consulta que se cuelga), la pantalla de bienvenida debe expirar y dejar que la interfaz muestre su propio estado de carga o error en lugar de quedarse congelada para siempre:

private val isReady = MutableStateFlow(false)

init {
    viewModelScope.launch {
        withTimeoutOrNull(2000) {
            dashboardFlow.first { it != null }
        }
        isReady.value = true
    }
}

Segundo, setKeepOnScreenCondition solo retrasa la salida — no bloquea la entrada ni el resto del arranque de la aplicación, así que no lo uses como sustituto de un estado de carga real más abajo en el árbol. Te compra como mucho un par de cientos de milisegundos de paciencia antes de que se lea como que la aplicación se ha colgado.

La animación de salida, si el desvanecido importa

Por defecto, la vista de bienvenida simplemente se elimina. Si un desvanecido o escalado personalizado al salir importa para la sensación de marca, setOnExitAnimationListener te da la vista real de bienvenida para animarla antes de descartarla tú mismo:

splashScreen.setOnExitAnimationListener { splashView ->
    splashView.view.animate()
        .alpha(0f)
        .setDuration(200)
        .withEndAction { splashView.remove() }
        .start()
}

Omitir splashView.remove() al final deja la vista de bienvenida adjunta indefinidamente sobre tu contenido — un descuido de una sola línea fácil de cometer, que se ve bien en un bucle de desarrollo con recarga en caliente y luego lanza una pantalla de inicio permanentemente congelada.

Lo que realmente importó

El valor predeterminado que te da el sistema cuando no configuras nada no está roto, pero tampoco es intencional — instala el tema de bienvenida antes de super.onCreate(), dimensiona el icono para la restricción real de 240dp en lugar de reutilizar el icono de lanzador adaptativo, y recurre a keepOnScreenCondition solo cuando hay un dato específico y cronometrado que el primer fotograma realmente necesita. La animación de salida es la parte que es fácil omitir por completo, y para la mayoría de las aplicaciones esa es la decisión correcta — la ganancia aquí es eliminar el destello blanco y el parpadeo del panel vacío, no añadir movimiento por sí mismo.