RORK LABJP
DEADLINE — From August 31, 2026, Google Play requires target API level 36 (Android 16) or higher for both new apps and updates to existing ones. Thirteen days leftEXTENSION — If you cannot make the date, the deadline extension form in Play Console buys you until November 1. The extension is not automatic, so the request itself has to land before August 31TARGET SDK — Even for Expo and React Native apps produced by builders like Rork, the targetSdkVersion is yours to verify. A template pinned to an older SDK will not meet the requirement on its ownPOLICY — The spam and minimum functionality policy has been tightened around high-quality features and content experience, which puts thin, mass-produced apps squarely in scopePRIVACY — You are expected to explain in detail what data is collected, how it is used, and whether it is shared, including analytics SDKs and advertising identifiers a builder wires in for youRORK — Where the original Rork emits React Native and Expo, Rork Max generates SwiftUI. There is a free tier to start with, and paid plans begin at $25 per monthDEADLINE — From August 31, 2026, Google Play requires target API level 36 (Android 16) or higher for both new apps and updates to existing ones. Thirteen days leftEXTENSION — If you cannot make the date, the deadline extension form in Play Console buys you until November 1. The extension is not automatic, so the request itself has to land before August 31TARGET SDK — Even for Expo and React Native apps produced by builders like Rork, the targetSdkVersion is yours to verify. A template pinned to an older SDK will not meet the requirement on its ownPOLICY — The spam and minimum functionality policy has been tightened around high-quality features and content experience, which puts thin, mass-produced apps squarely in scopePRIVACY — You are expected to explain in detail what data is collected, how it is used, and whether it is shared, including analytics SDKs and advertising identifiers a builder wires in for youRORK — Where the original Rork emits React Native and Expo, Rork Max generates SwiftUI. There is a free tier to start with, and paid plans begin at $25 per month
Articles/Dev Tools
Dev Tools/2026-05-16Intermediate

Fixing the White Screen on Theme Switch Without recreate() — When a Process Restart Is the Right Call

Fixing a white screen on Android theme switching by separating the cases AppCompatDelegate can handle from the ones that need an AppRestarter.safeRestart, plus the situations where killProcess should never be used.

Android45theme switchwhite screenAppCompatDelegateAppRestarterrecreateindie dev29

A three-star review contained exactly one useful line: "sometimes I get a white screen." No stack trace, no steps, no device model. Toggling dark mode in the settings screen apparently flashed a blank white panel for a fraction of a second — on some devices, not mine.

The culprit was how I was calling recreate(). Working solo on a wallpaper app, this class of bug is the hardest to close: nothing crashes, so nothing shows up in Crashlytics, and the reproduction window is narrow enough that you cannot hit it by hand.

Why recreate() Causes a White Screen

When recreate() is called, Android destroys the current Activity and creates a new one. The problem is that there's a brief moment between destruction and creation when nothing is rendered — and that gap can become visible.

The conditions that make this noticeable are:

  • Activities that load heavy assets at startup (wallpaper apps are a prime example)
  • Lower-spec devices or older Android versions (6 through 8)
  • SharedPreferences written with apply() instead of commit() — the setting may not be flushed before the Activity restarts
  • Ad SDK callbacks such as AdMob wired into the Activity lifecycle

If your reviews contain "occasional white flash" at three or four stars, this is usually the pattern. One-star reviews saying "won't launch" are a different problem — reading the star distribution alongside the wording helps you separate the two before you start digging.

First, Check Whether AppCompatDelegate Is Enough

Before reaching for a process restart, there's something worth ruling out. If all you're switching is dark mode, AppCompatDelegate.setDefaultNightMode() usually covers it.

// Restore the saved preference in Application.onCreate()
AppCompatDelegate.setDefaultNightMode(
    if (prefs.getBoolean("dark_mode", false)) AppCompatDelegate.MODE_NIGHT_YES
    else AppCompatDelegate.MODE_NIGHT_NO
)
 
// Called from the settings toggle
fun applyNightMode(isDark: Boolean) {
    prefs.edit().putBoolean("dark_mode", isDark).apply()
    AppCompatDelegate.setDefaultNightMode(
        if (isDark) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO
    )
}

This is handled as a uiMode configuration change, and AppCompat applies it to live Activities for you. Because you never call recreate() yourself, you never open the destroy-then-create gap. If your theme differences live entirely in res/values and res/values-night, the white screen disappears with it.

In my case it wasn't enough. The app read its color palette once at launch and held the resolved values in an Application-level singleton, and Glide's placeholder color was pulled from there. Changing uiMode left the old colors in place.

The right fix is to drop the singleton and resolve colors from resources on demand. But the white screen was live in production, and I wanted it stopped in the current version. So the design change went to the next minor release, and the process restart went out first.

The AppRestarter.safeRestart Pattern

The fix came from rethinking the scope of the restart. Instead of relying on recreate(), which only restarts the current Activity, I moved to a pattern that restarts the entire app process.

// AppRestarter.kt
object AppRestarter {
    fun safeRestart(context: Context, delayMs: Long = 300L) {
        val intent = context.packageManager
            .getLaunchIntentForPackage(context.packageName)
            ?.apply {
                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
            }
            ?: return
 
        Handler(Looper.getMainLooper()).postDelayed({
            context.startActivity(intent)
            android.os.Process.killProcess(android.os.Process.myPid())
        }, delayMs)
    }
}

The call site is clean and explicit:

// SettingsActivity.kt
fun applyTheme(isDark: Boolean) {
    // Use commit() to guarantee the write before restart
    prefs.edit().putBoolean("dark_mode", isDark).commit()
    AppRestarter.safeRestart(this)
}

FLAG_ACTIVITY_CLEAR_TASK matters here. Without it, the old Activity can survive in the back stack, and pressing back drops the user onto a screen still rendered in the previous theme.

recreate() vs. safeRestart: The Real Difference

recreate() restarts only the current Activity. When your app has complex Fragment hierarchies, ViewModel state, or third-party SDKs initialized at the Application level, there's no guarantee that everything lands in a clean state after recreation.

In my wallpaper app, AdMob was being initialized early in the Activity lifecycle. After recreate(), the SDK occasionally landed in a state where it had already received initialization callbacks but the UI hadn't fully rebuilt. This race condition produced the white flash.

AppRestarter.safeRestart() restarts the entire process, which means Application.onCreate() runs again from scratch. Every SDK starts clean. No partial states.

Choosing Between apply() and commit()

This isn't strictly about the white screen, but if you use the safeRestart pattern, write your preferences with commit().

apply() writes asynchronously, so calling it immediately before killProcess() gives you no guarantee that the value reached disk. Any sequence that saves a setting and then terminates the app needs the synchronous write.

// Risky: apply() is asynchronous, and the process is about to die
prefs.edit().putBoolean("dark_mode", isDark).apply()
AppRestarter.safeRestart(this)
 
// Safe: commit() writes synchronously before the restart
prefs.edit().putBoolean("dark_mode", isDark).commit()
AppRestarter.safeRestart(this)

If you stay on the AppCompatDelegate path, apply() is fine — the process keeps running, so there's nothing to cut the write short. The rule of thumb: the write method depends on whether you're about to kill the process.

When Not to Use safeRestart

killProcess() terminates the process immediately. Application.onTerminate() doesn't run, and neither does the finally block of a coroutine in flight. Anything that assumes the process will stay alive is at risk.

SituationWhy to avoid it
A foreground service is running (audio playback, downloads)The service dies with the process. Even with START_STICKY, in-memory state such as download progress is gone
A Google Play Billing purchase is in flightIf the process dies before PurchasesUpdatedListener receives the result, acknowledgement never runs. This is only survivable if you already restore state with queryPurchasesAsync at launch
Analytics or Crashlytics events were just recordedThe buffer may not have been flushed yet
The user has a partially filled form openUnless you persist drafts, the input is lost outright

Put the other way around: calling it from a settings screen — where nothing else is in flight — keeps the technique on the safe side. Keeping settings in its own Activity pays off precisely here.

The User Experience Trade-off

Switching to safeRestart means that when a user changes the theme, the app visibly restarts rather than instantly toggling. There's a brief return to the launcher before the app comes back with the new theme applied.

Whether this feels natural depends on the app. For a wallpaper app, theme switching is a rare, deliberate action. A clean restart actually feels more intentional than a brief white flash. After shipping this fix in v2.1.0, the complaint rate around theme switching in user reviews dropped noticeably.

300ms works on most devices. Extending to 500ms is safe if you support especially slow hardware, but stay above 200ms so the preference write has time to land.

One small refinement: set android:windowBackground to the post-switch background color so the first frame after the restart already carries the new theme. It takes the edge off the relaunch.

Applying This to Rork Max Projects

When Rork Max generates an Android project, theme management typically goes through Context or a Theme Provider. If you're building a production app with Rork and want to implement theme switching reliably, here's what to verify before shipping:

  • Confirm the Activity extends AppCompatActivity
  • Check whether theme preferences are stored in SharedPreferences or Jetpack Datastore
  • Look at where SDK initialization (AdMob, Firebase, etc.) happens — is it in Application.onCreate() or the Activity?

Rork-generated settings screens tend to implement theme switching with recreate() or equivalent behavior by default. For production use, it's worth refactoring to the safeRestart pattern before launch.

If you're working with native Android code through Rork Max, the Kotlin object above drops in as-is. You can also expose it as a native module and call it from the React Native side, but I find keeping it fully native easier to maintain.

Verifying That the Fix Actually Landed

To confirm the white screen is gone, force the worst case rather than hoping to catch it.

Turn on "Don't keep activities" in developer options. Background Activities are then destroyed immediately, which exaggerates recreation behavior and makes the flash far easier to reproduce. You can toggle it from adb, which also means you won't forget to turn it back off.

# Enable "Don't keep activities"
adb shell settings put global always_finish_activities 1
 
# Always restore it after testing
adb shell settings put global always_finish_activities 0

To confirm the process really is being recreated, watch the process lifecycle in logcat:

adb logcat | grep -E "ActivityManager.*(Start proc|Killing)"

Then test theme switching on a low-spec physical device, or an emulator limited to a single CPU core. With recreate() the white screen shows up readily in that environment; after switching to safeRestart it should be absent.

Play Console's Android Vitals is the other half of the check. If theme switching was producing crashes, the count should fall in the fixed version. In my app, crash reports attributable to theme switching reached zero within a few weeks of the v2.1.0 release.

Staged rollout gives you the same signal earlier. Watching crash-free users across 5% → 25% → 50% → 100% catches serious regressions while the exposed audience is still small — and with a large daily active user base, even the 5% stage carries enough samples to be meaningful.

Keep the Order of Investigation

Under one small feature sit three layers: resource qualifiers, SDK initialization order, and the timing of the preference write. A white screen is the signal that one of them is out of step.

Which is why the order matters more than the fix. Try AppCompatDelegate first. If it isn't enough, be able to say in one sentence why it isn't. Only then reach for the process restart. Investigating in that sequence leaves a trace of your reasoning, which is what you'll need when you go back to correct the design properly.

If you're stuck on the same symptom, put the recreate() call and the preference write immediately before it side by side. Measuring the distance between those two lines has been enough to solve it more than once.

For the day-to-day side of catching crashes after release, see Two Weeks of Maintenance After v2.1.0 — Running Crashlytics Triage Through Claude in Chrome. For how the rollout itself is structured, see A Phased Release Strategy for Rork Apps.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-04-11
Building a Live Streaming App with Rork and Agora SDK — Real-Time Video, Gifting, and Monetization
Build a production-ready live streaming app using Rork and Agora SDK. Covers iOS/Android setup, host and audience roles, real-time chat, gift monetization, and scaling strategies with full code examples.
Dev Tools2026-06-01
When Your Android Vitals ANR Rate Climbs: Notes on Keeping the Main Thread Free in React Native
How I traced and fixed a rising Android Vitals ANR rate in a Rork-built React Native app, plus the main-thread rules I rolled out across six apps in production.
Dev Tools2026-05-29
Diagnosing 'Network request failed' That Only Hits Android Emulator in Rork
Your fetch returns fine in the iOS simulator but throws 'Network request failed' the moment you switch to Android. Here is the diagnosis order I use to separate localhost, cleartext, certificate, and proxy issues, with code that actually compiles.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →