Splash screens on Android 12+ in Modern Android & Kotlin
If you've shipped an Android app recently, you've likely noticed the new splash screen behavior on Android 12 and above. Gone are the days of rolling your own splash Activity—the platform now enforces a consistent, animated transition that every user sees when launching your app. Understanding how to work with this system rather than against it is essential for modern Android development.
Why the SplashScreen API exists
Before Android 12, every app on the platform implemented splash screens differently. Some used a dedicated Activity with a custom theme, others showed a static drawable, and a few skipped it entirely. This fragmentation meant inconsistent user experience across the ecosystem. Google saw an opportunity to standardize the behavior, so they introduced the SplashScreen API as part of Android 12's design language overhaul.
The SplashScreen API isn't optional—it runs whether you use it or not. On Android 12+, the system automatically creates a splash screen from your app's theme. The question isn't whether you get one; it's whether you take control of the experience or let the defaults take over. For a production app, you want control.
Understanding installSplashScreen()
The core mechanism for splash screens on Android 12+ in Modern Android & Kotlin is the installSplashScreen() function. You call this in your launcher Activity's onCreate() method, before calling setContentView(). This returns a SplashScreen object that lets you configure timing and behavior.
Here's a straightforward setup:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Install the splash screen
val splashScreen = installSplashScreen()
setContentView(R.layout.activity_main)
// Your initialization code follows
}
}When you call installSplashScreen(), it reads your app theme for the splash screen icon and background. By default, it pulls from android:windowBackground and android:windowSplashScreenAnimatedIcon. The splash screen displays immediately and dismisses once your Activity is ready to show content.
Make sure your theme inherits from a Material Design theme (like Theme.Material3.DayNight) to ensure compatibility. If you're still on older Material themes, upgrade now—it's a one-line change in most cases.
Keeping the splash screen visible while loading
One of the most common production scenarios is needing to load critical data before showing your app's main content. You might fetch user authentication state, load configuration, or prepare your database. The splash screen should stay visible during this time, giving users visual feedback that the app is initializing.
You control this behavior with a condition callback:
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val splashScreen = installSplashScreen()
// Set a condition to keep splash visible
splashScreen.setKeepOnScreenCondition {
viewModel.isLoading.value
}
setContentView(R.layout.activity_main)
setupUI()
}
private fun setupUI() {
lifecycleScope.launch {
viewModel.loadInitialData()
}
}
}The setKeepOnScreenCondition() lambda is called repeatedly by the system. As long as it returns true, the splash screen remains visible. Once your ViewModel's isLoading state changes to false, the splash screen animates away to reveal your main content.
This approach feels natural in a modern Android stack using ViewModels and Flow. Your ViewModel manages the loading state, and the Activity simply observes it. The UI layer doesn't need to know about splash screen mechanics—it just tracks whether data is ready.
In a real e-commerce app, this might look like:
class CheckoutViewModel : ViewModel() {
private val _isLoading = MutableStateFlow(true)
val isLoading: StateFlow = _isLoading.asStateFlow()
init {
viewModelScope.launch {
try {
val cart = cartRepository.fetchCart()
val user = userRepository.fetchUserProfile()
_isLoading.value = false
} catch (e: Exception) {
handleError(e)
_isLoading.value = false
}
}
}
}The Activity just needs to pass viewModel.isLoading.value to the condition. The splash screen stays up until your repositories finish their network calls.
Animated icons with AnimatedVectorDrawable
The splash screen API supports animated icons, which creates a more polished, intentional feel. Instead of a static logo, you can define an animated vector drawable that plays while the splash screen is visible. This is particularly effective in travel and ride-hailing apps where motion is part of the brand.
Set up an AnimatedVectorDrawable first. Create a drawable resource like animated_splash_icon.xml:
<?xml version="1.0" encoding="utf-8"?>
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/splash_icon_vector">
<target
android:name="logo_group"
android:animation="@animator/splash_icon_animation" />
</animated-vector>Then reference it in your theme's splash screen attributes:
<style name="Theme.MyApp" parent="Theme.Material3.DayNight">
<item name="android:windowBackground">@color/splash_bg</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/animated_splash_icon</item>
<item name="android:windowSplashScreenAnimationDuration">1000</item>
</style>The animation plays automatically. The system handles looping and timing. If your splash screen stays visible for longer than the animation duration (which it often will during loading), the animation repeats seamlessly. Users see continuous visual feedback without any jarring pauses.
Do not add a separate splash Activity
This is a critical point for teams migrating to modern Android practices: do not create a dedicated SplashActivity. The old pattern of a lightweight Activity that loads, waits a fixed duration, then launches the main Activity is now anti-pattern on Android 12+.
Here's what you might see in legacy code:
// DON'T do this anymore
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Handler(Looper.getMainLooper()).postDelayed({
startActivity(Intent(this, MainActivity::class.java))
finish()
}, 2000)
}
}Problems with this approach on modern Android:
- You're creating two Activities and two transitions instead of one, adding overhead and latency.
- The system's built-in splash screen still runs before your SplashActivity, so users see two splash screens in sequence.
- Fixed delays (like 2 seconds) don't scale—apps with heavy initialization wait unnecessary time, while fast apps feel sluggish.
- It's harder to coordinate with real loading state, leading to timing bugs.
Instead, let your main launcher Activity use the system splash screen directly with installSplashScreen() and condition callbacks. This is simpler, faster, and aligns with platform expectations.
Styling the splash screen through your theme
All splash screen configuration flows through your app's theme. This keeps everything in one place and ensures consistency with your design system.
Here are the key attributes:
| Attribute | Purpose | Example value |
|---|---|---|
android:windowBackground |
Splash screen background color | @color/primary_brand |
android:windowSplashScreenAnimatedIcon |
Animated icon drawable | @drawable/animated_logo |
android:windowSplashScreenAnimationDuration |
Animation duration in milliseconds | 1000 |
android:windowSplashScreenBrandingImage |
Optional branding image at bottom (deprecated, avoid) | @drawable/brand_mark |
A production-ready theme might look like this:
<style name="Theme.MyApp" parent="Theme.Material3.DayNight">
<item name="colorPrimary">@color/md_theme_primary</item>
<item name="colorSecondary">@color/md_theme_secondary</item>
<!-- Splash screen config -->
<item name="android:windowBackground">@color/md_theme_primary</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/animated_app_logo</item>
<item name="android:windowSplashScreenAnimationDuration">800</item>
</style>If you need different splash screens for light and dark themes, create theme overlays:
<style name="Theme.MyApp.Light">
<item name="android:windowBackground">@color/white</item>
</style>
<style name="Theme.MyApp.Dark">
<item name="android:windowBackground">@color/dark_bg</item>
</style>The system automatically selects the right variant based on the device's theme settings.
Common mistakes
Forgetting to set the condition callback: If you don't use setKeepOnScreenCondition(), the splash screen dismisses immediately after your Activity's onCreate() finishes, often before your data loads. Users see content pop in mid-initialization, creating a janky impression.
Returning a constant true from the condition: If your condition lambda always returns true, the splash screen never dismisses. Users get stuck staring at your logo. Make sure your loading state actually transitions to false.
Using fixed delays instead of real state: Posting a delayed runnable to set loading to false is tempting but wrong. You might be done loading in 500ms but leave the splash up for 2 seconds, or hit a network error and show a broken screen. Always tie splash visibility to actual data readiness.
Calling setContentView() before installSplashScreen(): The order matters. Install the splash screen first, then set your content view. If you do it backwards, the splash screen configuration won't apply correctly.
Trying to customize the splash screen beyond theme attributes: You can't add buttons, text fields, or custom logic to the system splash screen. If you need interactive elements during initialization, use a proper loading overlay in your main Activity instead. The splash screen is for branding and blocking, not interaction.
Key takeaways
- Use
installSplashScreen()in your launcher Activity'sonCreate()beforesetContentView()—this is the modern way to handle splash screens on Android 12+. - Keep the splash screen visible during critical loading with
setKeepOnScreenCondition(), passing a lambda that returns your ViewModel's loading state. - Leverage AnimatedVectorDrawable for polished, branded animations that reinforce your app's identity while users wait.
- Never create a separate SplashActivity—this adds unnecessary overhead, creates double transitions, and conflicts with the system splash screen.
- Configure everything through your app's theme attributes; avoid hardcoding splash behavior in Activities.
- Tie splash screen dismissal to actual data readiness, not fixed delays, so users never see broken or incomplete UI.






Comments
Loading comments…