テーマ切替ボタンを押したとき、一瞬だけ白い画面が表示される——Beautiful HD Wallpapersのv2.0.0に存在していた問題です。
手元の端末では再現しにくく、ユーザーレビューで「テーマが変わるとき画面が白くなる」というフィードバックが複数来るまで気づけていませんでした。Play Consoleを確認すると、端末スペックの低い機種、特にAndroid 7〜9の古いミドルレンジ機種で発生頻度が高いことが分かりました。
原因の見当はつきました。Activity.recreate() です。ただ、なぜそれが白画面を引き起こすのか、そしてどう直すのかを、Antigravityに聞いてみることにしました。
Activity.recreate()の落とし穴
Androidでテーマを動的に変更する場合、変更をUIに反映するにはActivityを再生成する必要があります。最もシンプルな実装は recreate() を呼ぶことです。
// 最もよく使われるが、白画面を引き起こす可能性がある
fun applyTheme(isDark: Boolean) {
AppCompatDelegate.setDefaultNightMode(
if (isDark) AppCompatDelegate.MODE_NIGHT_YES
else AppCompatDelegate.MODE_NIGHT_NO
)
activity.recreate() // ← これが問題
}recreate() はActivityを破棄して再生成します。このとき、旧Activityの終了と新Activityの起動の間に一瞬だけウィンドウが空になる瞬間があります。スペックの高い端末では描画が間に合って気づかないことが多いのですが、描画が遅い端末では白い画面が見えてしまいます。
Antigravityへの相談と返ってきた提案
コードとエラー状況を渡して、Antigravityに状況を整理してもらいました。
以下のコードでテーマ切替後に白画面が発生する。
- setDefaultNightMode() 後に recreate() を呼んでいる
- Android 7〜9の端末で頻度が高い
- 新端末では再現しにくい
白画面の発生メカニズムと、推奨される代替実装を教えてほしい。
返ってきた分析は明確でした。「recreate() はシステムのActivity遷移アニメーションを伴うため、テーマ変更後のウィンドウが一時的に透過またはデフォルト(白)になります。推奨される回避策は、recreate() を使わず Intent で自分自身を起動し直す方法(safeRestart)です。アニメーションをなくすフラグを組み合わせることで、白画面が発生する余地をなくせます」
AppRestarter.safeRestart の実装
Antigravityが示した方針をもとに、AppRestarter クラスとして実装しました。
object AppRestarter {
/**
* 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()
// アニメーションを無効化(白画面抑制の核心部分)
activity.overridePendingTransition(0, 0)
}
}呼び出し側はシンプルです。
fun applyTheme(isDark: Boolean) {
AppCompatDelegate.setDefaultNightMode(
if (isDark) AppCompatDelegate.MODE_NIGHT_YES
else AppCompatDelegate.MODE_NIGHT_NO
)
// recreate() の代わりに safeRestart を使う
AppRestarter.safeRestart(this)
}overridePendingTransition(0, 0) がポイントです。第1引数は入場アニメーション、第2引数は退場アニメーションで、それぞれ0(アニメーションなし)を指定します。これによって、旧Activityが消えてから新Activityが現れるまでの間隙がなくなります。
Intentのフラグ選択について
FLAG_ACTIVITY_CLEAR_TOP と FLAG_ACTIVITY_NEW_TASK の組み合わせは、バックスタックを意図した状態に保つために重要です。
FLAG_ACTIVITY_CLEAR_TOP だけでは、同じタスク内に複数のActivityが積み重なっている場合に意図しない状態になることがあります。Beautiful HD Wallpapersはカテゴリ→壁紙詳細→お気に入りといった階層構造があるため、テーマ切替後にトップ画面へ遷移させたい場合は FLAG_ACTIVITY_NEW_TASK を追加して明示的にタスクを再構築する方が安全です。
アプリの画面構造によって最適なフラグの組み合わせは変わります。シングルActivityアーキテクチャ(NavHostFragmentを使うアプリ)ではやや異なる対処が必要になります。その場合はFragmentのライフサイクルを再起動させる方法(NavControllerによる再描画)が現実的です。
修正後の変化
v2.1.0として段階公開(5% → 25% → 50% → 100%の順)したところ、テーマ切替に関するフィードバックが止まりました。Play Consoleのユーザーレビュー分析でも、「白画面」に関する言及が消えています。
2013年に個人でアプリ事業を始めてから12年以上経ちます。この種の「手元では再現しないが実機報告が来ている問題」は、一人で開発していると特にデバッグが難しい。端末の多様性が原因になっているケースが多く、自分の開発環境だけでは網羅できません。
Antigravityに渡したのはコードと状況説明だけです。どの端末で発生しやすいかという文脈も添えることで、ピンポイントな提案が返ってきました。問題の「何が起きているか」は把握していても「なぜそうなるか」が見えにくいとき、コードの外から俯瞰してもらう価値があります。
参考:ModalGate との組み合わせ
safeRestart を導入した後、別の問題が見えてきました。テーマ切替直後に広告モーダルや評価誘導ダイアログが重なって表示されることがあったのです。
これはActivity再生成のタイミングで複数のDialogが同時に show() を呼ぶために発生します。解決策として、モーダルの表示を一元管理する ModalGate オブジェクトを導入しました。
object ModalGate {
private var isShowing = false
fun requestShow(show: () -> Unit): Boolean {
if (isShowing) return false
isShowing = true
show()
return true
}
fun dismiss() {
isShowing = false
}
}PaywallDialog、ReviewInductionDialog、RewardedInterstitialDialogのいずれも ModalGate.requestShow {} 経由で表示することで、同時表示の競合が解消されました。
v2.1.0での段階公開とクラッシュ監視
修正版はv2.1.0として段階公開(5% → 25% → 50% → 100%)でリリースしました。各フェーズでPlay ConsoleのAndroid vitalsを確認し、Crash-free usersが99.7%以上を維持できていることを確認してから次の段階に進みました。
段階公開の利点は、万が一修正に問題があったときに影響を限定できる点です。AppRestarter の実装はシンプルですが、Intentフラグの組み合わせはアプリの画面構造によって最適解が変わります。5%フェーズで実際のユーザー環境での動作を観察し、問題がないことを確認してから全ユーザーへ展開する判断ができたことで、リリース後の不安が大きく減りました。
Antigravityを使ったAndroidアプリ開発の全体像については、こちらの記事もあわせてご覧ください。
Activity.recreate() から AppRestarter.safeRestart() への切り替えは1時間もかからない修正ですが、ユーザー体験への影響は明確です。同じ問題に直面している方の参考になれば幸いです。
個人開発12年で見えてきた「地味なバグほど大事にする」という感覚
2013年から個人開発でアプリを作り続けています。今では4本のiOS/Androidアプリが累計5,000万DLを超えるまでになりましたが、最初の数年は「動いていればいい」という感覚でコードを書いていました。
テーマ切替の白画面は、機能的には問題ありません。アプリは正常に動作します。ただ、毎回テーマを変えるたびに白い画面を見たユーザーが「なんか品質が低いな」と感じることで、長期的な離脱につながります。
廣川政樹として世界各地でアートの展示をしていると、作品の微細な部分への注意が鑑賞者の体験を変えることを実感します。ソフトウェアも同じです。宮大工だった両祖父が「手を動かすことが一つの信心」と言い残した言葉を、私はコードの中でも意識するようにしています。
地味に見える修正を丁寧に重ねることが、個人開発者としてのアプリの品質を作ります。
全体を振り返ってと次のアクション
Activity.recreate() から AppRestarter.safeRestart() へ移行する手順:
AppRestarterオブジェクトをプロジェクトに追加- 全てのテーマ切替箇所で
recreate()をAppRestarter.safeRestart(this)に置き換え ModalGateを導入して Dialog 同時表示の競合を防ぐ- 低スペック端末(Android 7〜9 のエミュレーター)でテーマ切替を繰り返して確認
段階的に進めるなら、まず AppRestarter だけ導入し、ModalGate は必要が生じてから追加する形で問題ありません。recreate() を置き換えるだけでも白画面問題の大半は解消されます。