Every time a user tapped the theme toggle in Beautiful HD Wallpapers, there was a brief white flash before the new theme appeared. On high-end devices it was barely noticeable. On older mid-range phones running Android 7–9, it was clearly visible — and multiple user reviews mentioned it.
I knew the culprit was Activity.recreate(). What I didn't fully understand was why it caused this, and whether there was a clean way to fix it without major refactoring. I ran the problem by Antigravity, and the explanation and solution it returned were both clear and immediately applicable.
Why Activity.recreate() Causes a White Screen
The standard approach for applying a dynamic theme change in Android is to call recreate() after setting the new night mode:
// Common but problematic for low-end devices
fun applyTheme(isDark: Boolean) {
AppCompatDelegate.setDefaultNightMode(
if (isDark) AppCompatDelegate.MODE_NIGHT_YES
else AppCompatDelegate.MODE_NIGHT_NO
)
activity.recreate() // ← the problem
}recreate() destroys and rebuilds the Activity using the system's standard transition mechanism. Between the old Activity finishing and the new one rendering, there's a brief moment where the window is empty or defaults to the app's background — which on many devices is white. Fast processors complete this before a single frame renders. Slower ones don't.
What Antigravity Diagnosed
I passed the code and context to Antigravity:
Theme switching causes a white flash on Android 7–9 mid-range devices.
Implementation: setDefaultNightMode() followed by recreate().
High-end devices don't reproduce it.
Explain why this happens and what the recommended alternative is.
The response: "recreate() triggers the standard Activity transition animation, which includes a brief window where neither the old nor new Activity is fully rendered. The standard fix is to bypass recreate() entirely — launch the Activity via an explicit Intent with FLAG_ACTIVITY_CLEAR_TOP and FLAG_ACTIVITY_NEW_TASK, then call overridePendingTransition(0, 0) to disable the enter/exit animation. This removes the gap between the two Activity instances."
The AppRestarter Implementation
Based on that guidance, I extracted the logic into a dedicated object:
object AppRestarter {
/**
* Safe theme-switch restart that avoids the white screen
* caused by Activity.recreate().
*/
fun safeRestart(activity: Activity) {
val intent = activity.intent.apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
activity.startActivity(intent)
activity.finish()
// Disable transition animation — this is the key line
activity.overridePendingTransition(0, 0)
}
}Calling it is straightforward:
fun applyTheme(isDark: Boolean) {
AppCompatDelegate.setDefaultNightMode(
if (isDark) AppCompatDelegate.MODE_NIGHT_YES
else AppCompatDelegate.MODE_NIGHT_NO
)
AppRestarter.safeRestart(this)
}overridePendingTransition(0, 0) suppresses both the enter animation (first argument) and exit animation (second argument). Without the animation gap, there's no visible moment when the window is empty.
Choosing the Right Intent Flags
The combination of FLAG_ACTIVITY_CLEAR_TOP and FLAG_ACTIVITY_NEW_TASK is deliberate.
FLAG_ACTIVITY_CLEAR_TOP alone can leave the back stack in an inconsistent state when multiple Activities are stacked. Beautiful HD Wallpapers has a category → detail → favorites hierarchy, so after a theme switch I want to land back at the top-level screen cleanly. Adding FLAG_ACTIVITY_NEW_TASK rebuilds the task explicitly, which produces more predictable behavior.
If your app uses a single-Activity architecture with NavHostFragment, this approach needs adjustment. In that case, re-triggering a full recompose via the NavController or a global state observer is typically more appropriate than relaunching the Activity.
The ModalGate Pattern for Post-Restart Conflicts
After deploying safeRestart, a secondary issue surfaced: occasionally, a paywall dialog and a review prompt appeared simultaneously right after the theme switch.
The cause is that Activity recreation triggers multiple initialization paths at once, each potentially calling dialog.show() without knowing the others exist. The fix is a centralized gate:
object ModalGate {
private var isShowing = false
/**
* Request permission to show a modal.
* Returns false if another modal is already visible.
*/
fun requestShow(show: () -> Unit): Boolean {
if (isShowing) return false
isShowing = true
show()
return true
}
fun dismiss() {
isShowing = false
}
}Usage in each dialog:
// PaywallDialog
ModalGate.requestShow {
PaywallDialog(context).show()
}
// ReviewInductionDialog
ModalGate.requestShow {
ReviewInductionDialog(context).show()
}Only one dialog can show at a time. The second call returns false and does nothing. When the user dismisses the active dialog, ModalGate.dismiss() releases the lock and the next dialog can show.
Results After v2.1.0 Staged Rollout
The fix shipped in v2.1.0 with a staged rollout: 5% → 25% → 50% → 100%. After full rollout, user reviews mentioning the white screen stopped completely.
I've been building apps independently since 2013. Bugs that don't reproduce on your own devices but consistently appear on users' older hardware are among the most frustrating to track down. The white screen issue was exactly that kind — subtle, intermittent, and device-dependent.
What Antigravity added wasn't just a code fix but a clear explanation of why the mechanism failed. Understanding overridePendingTransition(0, 0) as the solution would have taken me longer to arrive at through documentation alone, since the mental model of "animation gap = visible empty window" isn't spelled out clearly in Android docs.
If you're seeing the same white screen on theme switches, the change from recreate() to AppRestarter.safeRestart() takes under an hour and has a noticeable impact on user experience across a wide range of devices.
The Bigger Picture: Why Small Fixes Matter in Indie Development
I've been building apps as an indie developer since 2013. Four iOS and Android apps — Beautiful HD Wallpapers, Ukiyo-e Wallpapers, Relaxing Healing, and Law of Attraction Everyday — now exceed 50 million cumulative downloads. Getting there wasn't about shipping flashy features. It was largely about fixing the small things that users notice but rarely articulate.
A white screen on theme switch doesn't break anything. The app keeps working. But users perceive it as low quality, even if they can't name what's wrong. That perception erodes trust over time.
My grandfathers were temple carpenters (宮大工). They believed that careful handwork is itself a form of devotion. I carry that same approach into software. As Masaki Hirokawa — an artist who has exhibited work internationally and received 17 international art awards — I've found that attention to detail in creative work and in software development come from the same place.
The recreate() → AppRestarter migration is the kind of fix that doesn't make a changelog dramatic. But it matters to the people who use the app every day.
Quick Reference: Migration Steps
- Add
AppRestarterto your project - Replace every
recreate()call in theme-switching logic withAppRestarter.safeRestart(this) - Add
ModalGateif you have multiple dialogs that can appear after restart - Test on an Android 7–9 emulator with repeated theme toggling
You can introduce AppRestarter first and add ModalGate later if needed — the white screen fix alone is worth the change.