アプリにダークモード切替を実装したとき、「設定を保存 → Activity を再起動 → テーマが反映される」という流れは Android 開発者なら誰もが通る道です。多くのサンプルコードでは recreate() が使われていますが、私が長年運営している壁紙アプリでは、この実装が原因で一部ユーザーに白画面が再現していました。
個人アプリ開発を2014年から続け、累計5,000万DLを超えるアプリ群を運営してきた中で、「バグは隠れた設計の問題の表れ」だと繰り返し実感しています。今回のテーマ切替の白画面も、掘り下げてみると recreate() の非同期タイミング問題という根本原因がありました。
recreate() で白画面が出るのはなぜか
recreate() は呼び出されると現在の Activity を破棄して再生成します。問題は、破棄と生成の間に一瞬だけ「何も描画されない状態」が存在することです。
通常のデバイスでは気づかないほど短時間ですが、次の条件が重なると白画面が見えやすくなります。
- 画像の読み込みが重い Activity(壁紙アプリは該当しやすい)
- スペックの低い端末や Android 6〜8 系のデバイス
- SharedPreferences の write が
commit()ではなくapply()の場合(まだ書き込まれていない可能性がある) - AdMob など広告 SDK のコールバック処理が Activity のライフサイクルに絡んでいる場合
私のケースでは、Beautiful HD Wallpapers でテーマ切替直後に一瞬白い画面が出るというユーザーレビューが複数上がっていました。再現条件が限定的なため自分では確認しにくく、しばらく原因を特定できなかった問題です。星3〜4つのレビューに「たまに白くなる」という一文がある場合、多くのケースはこのパターンです。
AppRestarter.safeRestart とはどんなパターンか
解決の糸口になったのは、アプリを「意図的に終了 → 再起動」するアプローチです。recreate() が Activity の再生成に依存しているのに対して、AppRestarter.safeRestart パターンはアプリのプロセスごと再起動します。
実装の考え方はこうです。
// AppRestarter.kt
object AppRestarter {
fun safeRestart(context: Context, delayMs: Long = 300L) {
// 1. 起動 Intent を準備(アプリのルート Activity)
val intent = context.packageManager
.getLaunchIntentForPackage(context.packageName)
?.apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
?: return
// 2. 設定の確実な書き込みを待ってから再起動
Handler(Looper.getMainLooper()).postDelayed({
context.startActivity(intent)
// 3. 現在のプロセスを終了
android.os.Process.killProcess(android.os.Process.myPid())
}, delayMs)
}
}呼び出し側はシンプルです。
// SettingsActivity.kt — テーマ切替後の処理
fun applyTheme(isDark: Boolean) {
// SharedPreferences に確実に保存(commit で同期書き込み)
prefs.edit().putBoolean("dark_mode", isDark).commit()
// safeRestart でアプリ全体を再起動
AppRestarter.safeRestart(this)
}recreate() との実際の違い
recreate() と AppRestarter.safeRestart() の違いは、影響範囲にあります。
recreate() は 現在の Activity だけを再生成する操作です。Fragment や ViewModel のライフサイクルが複雑に絡んでいる場合、State の引き継ぎに抜けが生じることがあります。また、Glide のキャッシュや広告 SDK(AdMob など)の初期化状態が中途半端なままで Activity が再生成されることがあり、これが白画面や表示崩れの原因になります。
一方 AppRestarter.safeRestart() は、アプリプロセスをゼロから起動しなおす操作です。Application クラスの onCreate() から全部やり直すため、SDK の初期化状態が必ず整合します。
私の壁紙アプリでは AdMob の初期化が Activity のライフサイクルの早い段階で走っており、recreate() 後にこの初期化が二重になるケースがありました。これが白画面の一因でした。
SharedPreferences の apply() と commit() の選択
白画面とは直接関係ありませんが、safeRestart パターンを使う際は SharedPreferences の書き込みに commit() を使うことを推奨します。
apply() は非同期で書き込むため、killProcess() の直前に呼び出しても書き込みが完了していない可能性があります。設定を保存してからアプリを終了するシーケンスでは、必ず commit() を使って同期的に書き込んでください。
// ❌ apply() は非同期なので safeRestart 前は危険
prefs.edit().putBoolean("dark_mode", isDark).apply()
AppRestarter.safeRestart(this)
// ✅ commit() で同期書き込みしてから再起動
prefs.edit().putBoolean("dark_mode", isDark).commit()
AppRestarter.safeRestart(this)ユーザー体験への影響とトレードオフ
safeRestart アプローチに切り替えると、テーマ適用の際に一度ホーム画面を経由するような体験になります。「アプリが再起動している感」がより明示的です。
ユーザーにとってこれがマイナスに感じるかどうかは、アプリの性質によります。壁紙アプリの場合、テーマ設定は頻繁に行う操作ではなく、「設定を変えたらアプリが再起動する」という動作のほうが「変な白画面が一瞬出る」よりも信頼感があります。
delayMs の値については 300ms で大半のデバイスで問題ありません。極端に遅いデバイスを考慮するなら 500ms まで伸ばしても構いません。短くしすぎると SharedPreferences の書き込みが完了していない場合があるため、200ms 未満は避けることをおすすめします。
Rork Max で生成した Android プロジェクトへの適用
Rork Max が生成する Android コード(React Native ベース)の場合、テーマ管理は Context や Theme Provider を通じて行われます。Rork の生成コードにこのパターンを適用する場合、以下の点を確認するとスムーズです。
AppCompatActivityを継承しているか(recreate()が呼べる前提)- テーマ設定の保存先が SharedPreferences か、あるいは Jetpack Datastore か
Application.onCreate()でテーマ初期化および SDK 初期化が行われているか- AdMob や Firebase など広告・分析 SDK の初期化順序
Rork が生成する設定画面から直接テーマ切替を実装すると、デフォルトでは recreate() 相当の処理になることが多いです。本番運用を視野に入れるなら、生成後に上記のような safeRestart パターンへのリファクタリングを検討する価値があります。
ネイティブ Android コードを直接扱う Rork Max の場合、Kotlin の object として実装した AppRestarter をそのまま使えます。React Native 側から Kotlin ネイティブモジュールを呼び出す形でも実装できますが、シンプルに全面ネイティブで書く方が管理しやすいと感じています。
修正が効いているか確認する方法
実装後に本当に白画面が出なくなったかを確認するには、以下の手順が有効です。
まず開発者オプションで「アクティビティを保持しない」をオンにします。この設定を有効にすると、バックグラウンドに回った Activity が即座に破棄されるため、Recreation 時の挙動が誇張されて再現しやすくなります。
次に、スペックの低い実機か Android エミュレーターの CPU を 1 コアに制限した状態でテーマ切替を試します。recreate() を使っていた場合はこの環境で白画面が確認しやすく、safeRestart に置き換えた後は白画面が出ないことを確認できます。
また、Play Console の Android Vitals で「クラッシュと ANR」のグラフを確認することも有効です。テーマ切替に関連したクラッシュが存在していた場合、修正後のバージョンで件数が減少するはずです。私のアプリでは v2.1.0 のリリース後、テーマ切替に起因すると思われるクラッシュレポートが数週間で検出ゼロになりました。
さらに、Google Play の段階公開(5% → 25% → 50% → 100%)を使いながら Crash-free users の値を監視するアプローチも有効です。壁紙アプリのような日次アクティブユーザーが多いアプリでは、5% 公開の時点でもサンプル数が十分に確保でき、重大な不具合があれば早期に検出できます。
12年間の個人開発で見えてきた「隠れたバグ」への向き合い方
両家の祖父が宮大工だったこともあり、「手を動かすことが一つの信心」という感覚が身近にありました。表面的に動いているものでも、根本の設計に問題があれば、必ずどこかで歪みが出てきます。
recreate() のケースも同様です。多くのデバイスで動いているから問題ない、と思っていても、ユーザーレビューに散在する「たまに白い画面が出る」というコメントを集めてみると、パターンが見えてきます。個人開発で累計5,000万DLのアプリを長く維持してきた中で、こういう小さな問題への丁寧な対処が、長期的なレーティングとダウンロード数の安定につながると実感しています。
Rork を使っていても、自分でコードを書いていても、アプリの品質を上げるのは最終的にはこういう小さな問題への丁寧な対処の積み重ねです。
テーマ切替の白画面に悩んでいる方の参考になれば幸いです。まず recreate() の呼び出しタイミングと、その前後の SharedPreferences の書き込み方法を見直してみてください。それだけで解決するケースは少なくありません。
Android 開発全般のデバッグ手法についてはRork Max エラーハンドリング・デバッグ・本番監視の実践パターンも参考になります。また、個人開発アプリでの AdMob や収益化の設計については5,000万DLの個人開発者がRork Maxでアプリを作り直した記録で詳しく触れています。