Перейти к содержимому
Все записи

API SplashScreen в Android в 2026 году: холодный запуск без белой вспышки

Практическое руководство по API SplashScreen в Android — настройка темы, ограничения размера анимированной иконки, которые никто не читает, и keepOnScreenCondition для данных, которые ещё не готовы.

MFKAPPS 5 мин чтения

Каждое приложение Android показывает экран заставки начиная с Android 12, попросили вы об этом или нет. Система рисует его автоматически из иконки вашего приложения и цветов темы в тот момент, когда пользователь нажимает на значок в лаунчере — единственный выбор, который у вас действительно есть, это принять значение по умолчанию или настроить androidx.core.splashscreen так, чтобы отрисовать именно тот экран, который вы задумывали. Большинство плохих заставок, которые я видел, плохи не потому, что кто-то плохо их спроектировал. Они плохи потому, что никто вообще ничего не настроил, и лучшую догадку системы заполнила эту пустоту.

Я допустил каждую ошибку из этого поста, создавая Granyn, чьей панели нужна первая цифра баланса, загруженная из Room, прежде чем первый кадр будет достоин показа. Вот настройка, которая остановила белую вспышку и остановила заставку, задерживающуюся на лишний момент.

Почему у вас будет экран заставки, хотите вы того или нет

Начиная с Android 12 (API 31), платформа перехватывает каждый холодный запуск и показывает окно заставки, отрисованное системой, ещё до того, как ваша Activity вообще прикрепит своё представление контента. Это не функция библиотеки, которую можно пропустить — это поведение фреймворка по умолчанию. То, что даёт вам androidx.core.splashscreen — это слой совместимости, позволяющий настраивать это системное окно единообразно вплоть до API 23, вместо того чтобы получать настоящее поведение на 12+ и простой белый или чёрный прямоугольник на всём остальном.

Пропустите настройку — и получите значение системы по умолчанию: иконка вашего лаунчера на сплошном цвете, взятом из темы, без анимации, без контроля над тем, как долго она держится. Это не сломано, но редко является тем, чего хочет приложение с продуманным брендом, а на устройствах до версии 12 без библиотеки совместимости это может быть раздражающей белой вспышкой ещё до применения вашей темы.

Подключение темы

Заставка настраивается почти полностью в XML, как тема, применяемая до выполнения 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() должен выполниться до super.onCreate() — вызовите его после, и библиотека совместимости не сможет вовремя перехватить окно, и вы молча вернётесь к системному значению по умолчанию. postSplashScreenTheme — это тема, на которую переключается окно после закрытия заставки; забыть её задать означает, что ваша Activity ненадолго застрянет, отрисовываясь с атрибутами окна заставки, применёнными к реальному контенту.

Ограничение размера иконки, о котором никто не узнаёт, пока оно не укусит

У windowSplashScreenAnimatedIcon есть жёсткое ограничение: drawable рисуется внутри круга 240×240dp, и всё, что больше, молча обрезается, а не масштабируется. Это самая распространённая ошибка заставки, которую я вижу — кто-то повторно использует адаптивную иконку лаунчера, спроектированную как слой переднего плана 108×108dp внутри видимого круга 72×72dp, и она либо выходит за границы заставки, либо обрезается так, что выглядит сломанной ровно на одной плотности экрана.

Решение — отдельная иконка заставки, размеченная и отцентрованная под ограничение 240dp сама по себе, а не переиспользованный ресурс лаунчера:

res/drawable/splash_icon.xml   (единственный вектор, без адаптивных слоёв, помещается в 240x240dp)

Если иконка должна быть анимированной (AnimatedVectorDrawable), действует то же ограничение по размеру, а windowSplashScreenAnimationDuration ограничивает, сколько платформа будет её ждать — максимум 1000мс на API 31+, хотя библиотека совместимости на более старых API не применяет этот потолок так же строго, что само по себе становится источником несогласованности в тестах, если вы проверяете только одну версию ОС.

Когда данные ещё не готовы: keepOnScreenCondition

Часть, которая действительно имела значение для Granyn, была не иконка — это было время. Заставка закрывается, как только первый кадр готов к отрисовке, а для экрана на Compose это может произойти раньше, чем у ViewModel появится что показать. Если оставить как есть, это создаёт вспышку пустой панели между исчезновением заставки и загрузкой баланса полсекунды спустя — хуже, чем если бы заставка просто задержалась на этот лишний момент.

SplashScreen.setKeepOnScreenCondition держит окно заставки открытым, пока не выполнится условие:

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 — это StateFlow<Boolean>, который становится true, как только первый запрос Room для баланса панели действительно возвращает результат. Условие опрашивается на каждом кадре, поэтому держите его как дешёвое булево чтение — никогда не suspend-вызов и не обращение к базе данных напрямую внутри лямбды.

Две вещи делают это безопасным, а не способом бесконечно подвесить приложение. Во-первых, установите потолок — если isReady никогда не станет true (повреждённая база данных, зависший запрос), заставка должна истечь по таймауту и позволить интерфейсу показать собственное состояние загрузки или ошибки, вместо того чтобы навсегда замереть:

private val isReady = MutableStateFlow(false)

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

Во-вторых, setKeepOnScreenCondition только задерживает выход — он не блокирует ни ввод, ни остальной запуск приложения, поэтому не используйте его как замену настоящему состоянию загрузки где-то ниже по дереву. Он покупает вам самое большее пару сотен миллисекунд терпения, прежде чем это начнёт восприниматься как зависание приложения.

Анимация выхода, если важно плавное исчезновение

По умолчанию представление заставки просто удаляется. Если для ощущения бренда важно кастомное затухание или масштабирование при выходе, setOnExitAnimationListener даёт вам настоящее представление заставки, которое можно анимировать, прежде чем закрыть его самостоятельно:

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

Пропуск splashView.remove() в конце оставляет представление заставки прикреплённым поверх вашего контента бесконечно — лёгкая однострочная ошибка, которая выглядит нормально в цикле разработки с горячей перезагрузкой, а затем отгружает навсегда замёрзший экран запуска.

Что действительно имело значение

То значение по умолчанию, которое даёт вам система, если вы ничего не настраиваете, не сломано, но и не осознанно — устанавливайте тему заставки до super.onCreate(), размечайте иконку под реальное ограничение 240dp вместо переиспользования адаптивной иконки лаунчера, и прибегайте к keepOnScreenCondition только тогда, когда есть конкретные, привязанные ко времени данные, которые действительно нужны первому кадру. Анимация выхода — это та часть, которую легко пропустить полностью, и для большинства приложений это правильное решение — выигрыш здесь в устранении белой вспышки и мерцания пустой панели, а не в добавлении движения ради самого движения.

// По теме

Ещё из журнала

MFKAPPS 5 мин чтения

Динамический цвет Material You в Jetpack Compose: как сохранить фирменный цвет, когда побеждают обои

dynamicColorScheme() заменяет вашу палитру той, что построена из обоев пользователя. Практическое руководство по гармонизации фирменных цветов вместо их потери.

#android #engineering #kotlin
MFKAPPS 4 мин чтения

In-App Review API в Android: как просить оценку, не раздражая пользователя

Практическое руководство по In-App Review API от Google — как он на самом деле работает, когда его запускать и почему обычный попап «оцените нас» незаметно портит вашу оценку в Play Store.

#android #engineering #kotlin
MFKAPPS 4 мин чтения

Доступность на Android в 2026 году: чек-лист по TalkBack и семантике Compose, который реально работает

Практический чек-лист по доступности Android на 2026 год — TalkBack, семантика Compose, размер зон касания и тестовый прогон, который я делаю на каждом экране перед релизом.

#android #engineering #kotlin