
Automates native splash asset generation and creates a matching runtime transition layer to eliminate startup flicker; single-build config, project file patching and dark‑mode support.
Professional Splash Screens for Compose Multiplatform, configured in seconds.
Creating a seamless startup experience in Compose Multiplatform is notoriously difficult. Between the native Android SplashScreen API and iOS UILaunchScreen, developers often face a "white flash" gap between the native boot sequence and the Jetpack Compose Multiplatform runtime.
KMP Splash bridges this gap. It automates the generation of native splash assets and provides a Compose-ready transition layer, ensuring your app feels premium from the very first pixel.
build.gradle.kts.Assets.xcassets for iOS and themes.xml for Android.SplashConfig composable to prevent the flicker when shifting from native boot to Compose UI..pbxproj and Info.plist automatically. No more Storyboards.androidx.core:core-splashscreen 1.2.0+
UILaunchScreen plist key)Ensure you have mavenCentral() in your settings.gradle.kts:
pluginManagement {
repositories {
google()
gradlePluginPortal()
mavenCentral()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}In your gradle/libs.versions.toml:
[versions]
kmpSplash = "<version>"
[libraries]
kmpSplash-runtime = { module = "io.github.kmpbits:splash-runtime", version.ref = "kmpSplash" }
[plugins]
kmpSplash = { id = "io.github.kmpbits.splash", version.ref = "kmpSplash" }In your Compose App module build.gradle.kts:
plugins {
alias(libs.plugins.kmpSplash)
}
splashScreen {
backgroundColor = SplashColor.hex("#FFFFFF") // Light mode background
backgroundColorNight = SplashColor.hex("#1A1A2E") // Optional: dark mode background
logo = SplashLogo.resource("splash_logo.png") // File in composeResources/drawable/ — 512×512 px recommended
logoDark = SplashLogo.resource("logo_dark.png") // Optional: dark mode logo
exitAnimation = ExitAnimation.FadeOut(300) // Optional: exit animation (Android + iOS)
iosProjectPath = "iosApp/iosApp" // Optional: defaults to "iosApp/iosApp"
androidAppPath = "androidApp" // Required if using the new KMP module structure
}Newer KMP project templates separate the Android entry point into a dedicated androidApp module, independent from composeApp. In this case, androidAppPath is required — without it the plugin targets the current module's androidMain sourcesets and the Android app module will not be configured.
// composeApp/build.gradle.kts
splashScreen {
backgroundColor = SplashColor.white
androidAppPath = "androidApp" // Path to the androidApp module, relative to the root project
}When androidAppPath is set, the plugin generates everything into the module's build/generated/kmpSplash/ folder — the resources, the splash Kotlin source, and a patched copy of the AndroidManifest.xml — and registers those generated directories into the androidApp module's source sets. Your src/main/ files are never modified. The preBuild task in androidApp is automatically wired to run generateAndroidSplash first — no manual setup required.
SplashColor.hex("#FFFFFF") // Hex string — accepts both #RRGGBB and RRGGBB
SplashColor.rgb(255, 255, 255) // RGB values (0–255 each)
SplashColor.white // Named constant
SplashColor.black // Named constantSplashLogo.resource("logo.png") // File in composeResources/drawable/
SplashLogo.path("src/commonMain/composeResources/drawable/logo.png") // Custom path relative to moduleExitAnimation.None // No animation — splash disappears instantly (default)
ExitAnimation.FadeOut(300) // Fade out over 300ms
ExitAnimation.SlideUp(400) // Slide upward to reveal the app
ExitAnimation.SlideDown(400) // Slide downward to reveal the appThe duration parameter is optional — the values above are the defaults.
iosProjectPathshould point to the inner folder that containsInfo.plistandAssets.xcassets— typicallyiosApp/iosApp, not the rootiosAppfolder.
androidAppPathis required when your project uses the new KMP module structure where the Android app lives in a dedicated module separate fromcomposeApp. Set it to the path of that module relative to the root project (e.g."androidApp"). Leave it unset for the classic structure where Android is part ofcomposeApp.
In your Compose App module build.gradle.kts:
commonMain.dependencies {
implementation(libs.kmpSplash.runtime)
}
androidMain.dependencies {
implementation("androidx.core:core-splashscreen:1.2.0")
}KMP Splash is integrated into the Gradle build process. On both Android and iOS, the splash assets are generated automatically when you build or run your app.
If you ever want to trigger the generation manually, you can run:
# Generate iOS assets (Info.plist, pbxproj, xcassets)
./gradlew generateLaunchScreen
# Generate Android assets (themes.xml, logo)
./gradlew generateAndroidSplash[!IMPORTANT] iOS Simulator Caching: iOS heavily caches the launch screen. If you change the background color or logo and don't see the changes in the simulator, you must restart the simulator (or sometimes even delete and reinstall the app) for the new assets to be reflected.
[!WARNING] Avoid naming your logo file
logo.pngon iOS. The filename becomes theUIImageNameused byUILaunchScreen. The namelogoconflicts with iOS internals and causes the image to be displayed fullscreen instead of at its natural size. Use a more specific name such assplash_logo.png,app_logo.png, oric_splash.png.
[!TIP] Recommended logo size: 512×512 px. On iOS, the native launch screen renders the image at its natural point size (pixels ÷ screen scale). A 512 px PNG displays at ~171 pt on @3x iPhones — roughly 45% of the screen width, which looks correct as a centred logo. Smaller sources (e.g. 250 px) will appear too large; larger sources will appear smaller.
Extend SplashActivity in your MainActivity:
class MainActivity : SplashActivity() {
override suspend fun isReady(): Boolean {
delay(1000) // Load data, check auth, etc.
return true
}
override fun onFinished() {
setContent {
App()
}
}
}If you need to call enableEdgeToEdge(), override onPreCreate() instead of onCreate(). This hook runs at exactly the right moment — after installSplashScreen() but before super.onCreate() — which is the order Android requires:
class MainActivity : SplashActivity() {
override fun onPreCreate() {
enableEdgeToEdge()
}
override suspend fun isReady(): Boolean { ... }
override fun onFinished() { ... }
}[!IMPORTANT] Do not call
enableEdgeToEdge()inside your ownonCreate()override. Doing so runs it beforeinstallSplashScreen(), which causes a stray toolbar to appear on the first frame.
Call SplashConfig in your MainViewController, passing your app content as the trailing lambda:
fun MainViewController() = ComposeUIViewController {
SplashConfig(
isReady = {
delay(1500) // Your initialization logic
true
}
) {
App()
}
}SplashConfig manages the splash/content transition internally — no state boilerplate needed. Colors and logo are picked up automatically from your Gradle configuration.
The Gradle plugin does the heavy lifting at build time so you never touch XML or native config files manually:
themes.xml (and values-night), copies your logo drawable, and writes a patched copy of the AndroidManifest.xml — all into the build/ folder — to apply the splash theme and register a ContentProvider that initialises runtime config before your Activity starts. Your source files are never modified.SplashBackground color asset and logo imageset in Assets.xcassets, and patches Info.plist and project.pbxproj to wire up UILaunchScreen — no Storyboard or Xcode required.| Platform | Native (Booting) | Compose (Loading) |
|---|---|---|
| Android |
themes.xml + a patched copy of AndroidManifest.xml generated into build/. Uses installSplashScreen(). |
SplashActivity controls visibility and runs the exit animation via setOnExitAnimationListener. |
| iOS | Patches Info.plist with UILaunchScreen, generates SplashBackground color asset and logo imageset in Assets.xcassets. |
SplashConfig uses isSystemInDarkTheme() to match the native screen exactly, then animates the exit with AnimatedVisibility. |
When a KMP app starts on iOS, the OS displays the native launch screen immediately. Once the Kotlin runtime and Compose initialize (which can take 500ms+), the screen usually flashes white or black before your first Composable is rendered.
KMP Splash ensures the SplashConfig composable is visually identical to the native launch screen, providing seamless continuity that keeps your branding on screen until the app is actually ready.
App-level dark mode overrides
If your app has its own appearance setting (e.g. a dark mode toggle independent of the system setting), the native splash screen will not respect it. Both iOS UILaunchScreen and Android's SplashScreen API are rendered by the OS before any app code runs, they read the system dark mode setting directly. There is no way for any library to work around this.
The Compose layer (SplashConfig / SplashActivity) does run app code, so it can respond to your app's own preference. For the native layer, the options are:
This is a system limitation, not a bug in the library.
Contributions are welcome! If you find a bug or have a feature request, please open an issue or a pull request.
Copyright 2026 KMP Bits
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Professional Splash Screens for Compose Multiplatform, configured in seconds.
Creating a seamless startup experience in Compose Multiplatform is notoriously difficult. Between the native Android SplashScreen API and iOS UILaunchScreen, developers often face a "white flash" gap between the native boot sequence and the Jetpack Compose Multiplatform runtime.
KMP Splash bridges this gap. It automates the generation of native splash assets and provides a Compose-ready transition layer, ensuring your app feels premium from the very first pixel.
build.gradle.kts.Assets.xcassets for iOS and themes.xml for Android.SplashConfig composable to prevent the flicker when shifting from native boot to Compose UI..pbxproj and Info.plist automatically. No more Storyboards.androidx.core:core-splashscreen 1.2.0+
UILaunchScreen plist key)Ensure you have mavenCentral() in your settings.gradle.kts:
pluginManagement {
repositories {
google()
gradlePluginPortal()
mavenCentral()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}In your gradle/libs.versions.toml:
[versions]
kmpSplash = "<version>"
[libraries]
kmpSplash-runtime = { module = "io.github.kmpbits:splash-runtime", version.ref = "kmpSplash" }
[plugins]
kmpSplash = { id = "io.github.kmpbits.splash", version.ref = "kmpSplash" }In your Compose App module build.gradle.kts:
plugins {
alias(libs.plugins.kmpSplash)
}
splashScreen {
backgroundColor = SplashColor.hex("#FFFFFF") // Light mode background
backgroundColorNight = SplashColor.hex("#1A1A2E") // Optional: dark mode background
logo = SplashLogo.resource("splash_logo.png") // File in composeResources/drawable/ — 512×512 px recommended
logoDark = SplashLogo.resource("logo_dark.png") // Optional: dark mode logo
exitAnimation = ExitAnimation.FadeOut(300) // Optional: exit animation (Android + iOS)
iosProjectPath = "iosApp/iosApp" // Optional: defaults to "iosApp/iosApp"
androidAppPath = "androidApp" // Required if using the new KMP module structure
}Newer KMP project templates separate the Android entry point into a dedicated androidApp module, independent from composeApp. In this case, androidAppPath is required — without it the plugin targets the current module's androidMain sourcesets and the Android app module will not be configured.
// composeApp/build.gradle.kts
splashScreen {
backgroundColor = SplashColor.white
androidAppPath = "androidApp" // Path to the androidApp module, relative to the root project
}When androidAppPath is set, the plugin generates everything into the module's build/generated/kmpSplash/ folder — the resources, the splash Kotlin source, and a patched copy of the AndroidManifest.xml — and registers those generated directories into the androidApp module's source sets. Your src/main/ files are never modified. The preBuild task in androidApp is automatically wired to run generateAndroidSplash first — no manual setup required.
SplashColor.hex("#FFFFFF") // Hex string — accepts both #RRGGBB and RRGGBB
SplashColor.rgb(255, 255, 255) // RGB values (0–255 each)
SplashColor.white // Named constant
SplashColor.black // Named constantSplashLogo.resource("logo.png") // File in composeResources/drawable/
SplashLogo.path("src/commonMain/composeResources/drawable/logo.png") // Custom path relative to moduleExitAnimation.None // No animation — splash disappears instantly (default)
ExitAnimation.FadeOut(300) // Fade out over 300ms
ExitAnimation.SlideUp(400) // Slide upward to reveal the app
ExitAnimation.SlideDown(400) // Slide downward to reveal the appThe duration parameter is optional — the values above are the defaults.
iosProjectPathshould point to the inner folder that containsInfo.plistandAssets.xcassets— typicallyiosApp/iosApp, not the rootiosAppfolder.
androidAppPathis required when your project uses the new KMP module structure where the Android app lives in a dedicated module separate fromcomposeApp. Set it to the path of that module relative to the root project (e.g."androidApp"). Leave it unset for the classic structure where Android is part ofcomposeApp.
In your Compose App module build.gradle.kts:
commonMain.dependencies {
implementation(libs.kmpSplash.runtime)
}
androidMain.dependencies {
implementation("androidx.core:core-splashscreen:1.2.0")
}KMP Splash is integrated into the Gradle build process. On both Android and iOS, the splash assets are generated automatically when you build or run your app.
If you ever want to trigger the generation manually, you can run:
# Generate iOS assets (Info.plist, pbxproj, xcassets)
./gradlew generateLaunchScreen
# Generate Android assets (themes.xml, logo)
./gradlew generateAndroidSplash[!IMPORTANT] iOS Simulator Caching: iOS heavily caches the launch screen. If you change the background color or logo and don't see the changes in the simulator, you must restart the simulator (or sometimes even delete and reinstall the app) for the new assets to be reflected.
[!WARNING] Avoid naming your logo file
logo.pngon iOS. The filename becomes theUIImageNameused byUILaunchScreen. The namelogoconflicts with iOS internals and causes the image to be displayed fullscreen instead of at its natural size. Use a more specific name such assplash_logo.png,app_logo.png, oric_splash.png.
[!TIP] Recommended logo size: 512×512 px. On iOS, the native launch screen renders the image at its natural point size (pixels ÷ screen scale). A 512 px PNG displays at ~171 pt on @3x iPhones — roughly 45% of the screen width, which looks correct as a centred logo. Smaller sources (e.g. 250 px) will appear too large; larger sources will appear smaller.
Extend SplashActivity in your MainActivity:
class MainActivity : SplashActivity() {
override suspend fun isReady(): Boolean {
delay(1000) // Load data, check auth, etc.
return true
}
override fun onFinished() {
setContent {
App()
}
}
}If you need to call enableEdgeToEdge(), override onPreCreate() instead of onCreate(). This hook runs at exactly the right moment — after installSplashScreen() but before super.onCreate() — which is the order Android requires:
class MainActivity : SplashActivity() {
override fun onPreCreate() {
enableEdgeToEdge()
}
override suspend fun isReady(): Boolean { ... }
override fun onFinished() { ... }
}[!IMPORTANT] Do not call
enableEdgeToEdge()inside your ownonCreate()override. Doing so runs it beforeinstallSplashScreen(), which causes a stray toolbar to appear on the first frame.
Call SplashConfig in your MainViewController, passing your app content as the trailing lambda:
fun MainViewController() = ComposeUIViewController {
SplashConfig(
isReady = {
delay(1500) // Your initialization logic
true
}
) {
App()
}
}SplashConfig manages the splash/content transition internally — no state boilerplate needed. Colors and logo are picked up automatically from your Gradle configuration.
The Gradle plugin does the heavy lifting at build time so you never touch XML or native config files manually:
themes.xml (and values-night), copies your logo drawable, and writes a patched copy of the AndroidManifest.xml — all into the build/ folder — to apply the splash theme and register a ContentProvider that initialises runtime config before your Activity starts. Your source files are never modified.SplashBackground color asset and logo imageset in Assets.xcassets, and patches Info.plist and project.pbxproj to wire up UILaunchScreen — no Storyboard or Xcode required.| Platform | Native (Booting) | Compose (Loading) |
|---|---|---|
| Android |
themes.xml + a patched copy of AndroidManifest.xml generated into build/. Uses installSplashScreen(). |
SplashActivity controls visibility and runs the exit animation via setOnExitAnimationListener. |
| iOS | Patches Info.plist with UILaunchScreen, generates SplashBackground color asset and logo imageset in Assets.xcassets. |
SplashConfig uses isSystemInDarkTheme() to match the native screen exactly, then animates the exit with AnimatedVisibility. |
When a KMP app starts on iOS, the OS displays the native launch screen immediately. Once the Kotlin runtime and Compose initialize (which can take 500ms+), the screen usually flashes white or black before your first Composable is rendered.
KMP Splash ensures the SplashConfig composable is visually identical to the native launch screen, providing seamless continuity that keeps your branding on screen until the app is actually ready.
App-level dark mode overrides
If your app has its own appearance setting (e.g. a dark mode toggle independent of the system setting), the native splash screen will not respect it. Both iOS UILaunchScreen and Android's SplashScreen API are rendered by the OS before any app code runs, they read the system dark mode setting directly. There is no way for any library to work around this.
The Compose layer (SplashConfig / SplashActivity) does run app code, so it can respond to your app's own preference. For the native layer, the options are:
This is a system limitation, not a bug in the library.
Contributions are welcome! If you find a bug or have a feature request, please open an issue or a pull request.
Copyright 2026 KMP Bits
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0