星3つのレビューに、一行だけ「たまに白い画面が出る」と書かれていました。手元の端末では何度切り替えても再現せず、しばらく放置していた指摘です。設定画面のダークモードのトグルを押した直後に、白い板が一瞬だけ挟まる。そういう報告でした。
原因は recreate() の呼び方でした。個人開発で壁紙アプリを長く運用していると、こうした「一部の端末でだけ起きる見た目の破れ」が一番手強く感じます。落ちないのでクラッシュレポートにも上がらず、再現条件が狭いので自分の手では踏めません。
recreate() で白画面が出るのはなぜか
recreate() は呼び出されると現在の Activity を破棄して再生成します。問題は、破棄と生成の間に一瞬だけ「何も描画されない状態」が存在することです。
通常のデバイスでは気づかないほど短時間ですが、次の条件が重なると白画面が見えやすくなります。
- 画像の読み込みが重い Activity(壁紙アプリは該当しやすい)
- スペックの低い端末や Android 6〜8 系のデバイス
- SharedPreferences の write が
commit()ではなくapply()の場合(まだ書き込まれていない可能性がある) - AdMob など広告 SDK のコールバック処理が Activity のライフサイクルに絡んでいる場合
星3〜4つのレビューに「たまに白くなる」という一文がある場合、多くのケースはこのパターンです。逆に星1つで「起動しない」と書かれているものは別の原因なので、レビューの星の分布と文言をあわせて眺めると切り分けの当たりがつきます。
その前に AppCompatDelegate で足りないかを確かめる
いきなりプロセス再起動へ進む前に、確かめておきたいことがあります。ダークモードの切替だけであれば、AppCompatDelegate.setDefaultNightMode() で済むケースが大半です。
// Application.onCreate() で保存済みの設定を復元
AppCompatDelegate.setDefaultNightMode(
if (prefs.getBoolean("dark_mode", false)) AppCompatDelegate.MODE_NIGHT_YES
else AppCompatDelegate.MODE_NIGHT_NO
)
// 設定画面のトグルから呼ぶ
fun applyNightMode(isDark: Boolean) {
prefs.edit().putBoolean("dark_mode", isDark).apply()
AppCompatDelegate.setDefaultNightMode(
if (isDark) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO
)
}この呼び出しは uiMode の構成変更として扱われ、AppCompat が生存中の Activity へ適用します。recreate() を自分で呼ばないため、破棄と生成の隙間を自分で作らずに済みます。res/values と res/values-night でリソースを分けているだけの構成なら、これで白画面ごと消えます。
私の場合はここで足りませんでした。起動時に一度だけカラーパレットを読み込み、Application 側のシングルトンに値そのものを持たせていたためです。Glide のプレースホルダ色もそこから引いていたので、uiMode が変わっても古い色が残りました。
本来はシングルトンをやめて、都度リソースから引く設計に直すのが筋です。ただ、公開中のバージョンで先に白画面を止める必要がありました。設計変更は次のマイナーリリースに回し、まずプロセス再起動で挙動を揃える判断をしています。
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)
}FLAG_ACTIVITY_CLEAR_TASK を付けているのは、再起動後にバックスタックへ古い Activity が残らないようにするためです。これを忘れると、戻るボタンで前のテーマの画面へ帰ってしまうことがあります。
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)なお AppCompatDelegate だけで済ませる場合は apply() で構いません。プロセスを落とさないため、非同期の書き込みが途中で切れる心配がないからです。書き込み方法を切り替える基準は「このあとプロセスを殺すかどうか」だと考えると迷いません。
safeRestart を使ってはいけない場面
killProcess() はプロセスを即座に断つ操作です。Application.onTerminate() も、実行中のコルーチンの finally も走りません。そのため、プロセスが生きていることを前提にした処理が動いている間は使えません。
| 状況 | 避けるべき理由 |
|---|---|
| フォアグラウンドサービスが動作中(音楽再生・ダウンロード) | サービスも一緒に落ちます。START_STICKY で再生成されても、進行状況などのメモリ上の状態は失われます |
| Google Play Billing の購入フロー中 | PurchasesUpdatedListener が結果を受け取る前にプロセスが消えると、確定処理が走りません。起動時に queryPurchasesAsync で復元する実装が入っていることが前提になります |
| Analytics / Crashlytics のイベント送信直後 | バッファがフラッシュされる前にプロセスが消える可能性があります |
| ユーザーが入力途中のフォームを開いている | 下書きを永続化していない場合、入力内容がそのまま消えます |
逆に言えば、設定画面のように「他に走っている処理がない」画面からの呼び出しであれば、この手法は安全側に寄せられます。設定画面を独立した Activity にしておく設計が、ここで効いてきます。
ユーザー体験への影響とトレードオフ
safeRestart アプローチに切り替えると、テーマ適用の際に一度ホーム画面を経由するような体験になります。「アプリが再起動している感」がより明示的です。
ユーザーにとってこれがマイナスに感じるかどうかは、アプリの性質によります。壁紙アプリの場合、テーマ設定は頻繁に行う操作ではなく、「設定を変えたらアプリが再起動する」という動作のほうが「変な白画面が一瞬出る」よりも信頼感があります。
delayMs の値については 300ms で大半のデバイスで問題ありません。極端に遅いデバイスを考慮するなら 500ms まで伸ばしても構いません。短くしすぎると SharedPreferences の書き込みが完了していない場合があるため、200ms 未満は避けることをおすすめします。
再起動中の見た目を整えたい場合は、android:windowBackground にテーマ適用後の背景色を指定しておくと、起動直後の一瞬が新しいテーマの色になります。白いスプラッシュが挟まる印象を薄められる小さな工夫です。
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 が即座に破棄されるため、再生成時の挙動が誇張されて再現しやすくなります。adb からも切り替えられるので、検証の前後で戻し忘れずに済みます。
# 「アクティビティを保持しない」を有効化
adb shell settings put global always_finish_activities 1
# 検証が終わったら必ず戻す
adb shell settings put global always_finish_activities 0プロセスが実際に作り直されているかは、logcat のプロセス生成ログで確認できます。
adb logcat | grep -E "ActivityManager.*(Start proc|Killing)"次に、スペックの低い実機か Android エミュレーターの CPU を 1 コアに制限した状態でテーマ切替を試します。recreate() を使っていた場合はこの環境で白画面が確認しやすく、safeRestart に置き換えた後は白画面が出ないことを確認できます。
また、Play Console の Android Vitals で「クラッシュと ANR」のグラフを確認することも有効です。テーマ切替に関連したクラッシュが存在していた場合、修正後のバージョンで件数が減少するはずです。私のアプリでは v2.1.0 のリリース後、テーマ切替に起因すると思われるクラッシュレポートが数週間で検出ゼロになりました。
さらに、Google Play の段階公開(5% → 25% → 50% → 100%)を使いながら Crash-free users の値を監視するアプローチも有効です。日次アクティブユーザーが多いアプリでは、5% 公開の時点でもサンプル数が十分に確保でき、重大な不具合があれば早期に検出できます。
切り分けの順番だけは変えない
テーマ切替という小さな機能の裏に、リソース修飾子・SDK の初期化順序・設定の書き込みタイミングという三つの層がありました。白画面は、そのどこかがずれているという合図です。
ですから順番だけは守りたいところです。まず AppCompatDelegate で足りるかを試す。足りないなら、なぜ足りないのかを一文で言えるようにする。そこまで進んでからプロセス再起動へ手を伸ばす。この順で確かめると、あとから設計を戻すときにも判断の跡が残ります。
同じ症状で止まっている方は、recreate() の呼び出し位置と、その直前の SharedPreferences の書き込み方法を並べて見てみてください。この二行の距離を測るだけで解けることも、実際にありました。お読みいただきありがとうございました。
公開後にクラッシュを拾い続ける運用についてはAndroid 壁紙アプリ v2.1.0 公開後の保守 — Crashlytics で回す日々の運用にまとめています。段階公開そのものの組み方はRork アプリの段階的リリース戦略で詳しく触れています。