エラー内容
android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@4f3a2c1 is not valid; is your activity running?
このクラッシュは、ダイアログを表示しようとした瞬間にCrashlyticsで検出されることがほとんどです。スタックトレースを確認してみてください — ほぼ必ず、非同期コールバックや Handler.postDelayed() の内部に埋め込まれた dialog.show() が原因として浮かび上がります。
発生する原因
Androidのダイアログはすべて、コンテキスト(通常はActivity)から取得したウィンドウトークンを必要とします。dialog.show() を呼び出すと、Androidはそのトークンにダイアログウィンドウをアタッチしようとします。Activityが終了済み = トークンが無効 = クラッシュ、という流れです。
よくある原因:
- ユーザーがすでに「戻る」を押した後に、ネットワークコールバックや
AsyncTaskが完了してしまう。ダイアログが破棄済みのActivityに対して表示を試みる。 finish()が呼び出された後にdialog.show()を呼び出している。- ダイアログのコンストラクタに
getApplicationContext()を渡している — アプリケーションコンテキストにはウィンドウトークンがない。 - リクエスト処理中に画面が回転する: 古いActivityは消えているが、コールバックがそのActivityへの古い参照を保持したままになっている。
onSaveInstanceState()の後に表示されるFragmentダイアログ —IllegalStateExceptionクラッシュのいとこのような問題。
手順別の修正方法
1. 表示前にActivityが生存しているか確認する
すべての dialog.show() 呼び出しの前に、このガードを追加してください:
// Kotlin
fun showDialogSafely(activity: Activity, dialog: AlertDialog) {
if (!activity.isFinishing && !activity.isDestroyed) {
dialog.show()
}
}
// Java
private void showDialogSafely(Activity activity, AlertDialog dialog) {
if (!activity.isFinishing() && !activity.isDestroyed()) {
dialog.show();
}
}
なぜ両方のチェックが必要なのか? isFinishing() は finish() が呼び出された瞬間にtrueになりますが、Activityはまだ完全には消えていません。isDestroyed() は完全に破棄された状態をカバーします。両方のチェックが必要です。
2. アプリケーションコンテキストではなくActivityコンテキストを使用する
ダイアログにはウィンドウトークンを持つUIコンテキストが必要です。getApplicationContext() にはそれがないため、常にクラッシュします:
// WRONG — will crash
AlertDialog.Builder builder = new AlertDialog.Builder(getApplicationContext());
// CORRECT
AlertDialog.Builder builder = new AlertDialog.Builder(MyActivity.this);
// Kotlin:
AlertDialog.Builder(this@MyActivity)
3. メインスレッドでダイアログを表示する
バックグラウンドスレッドからUIを操作することはできません。ダイアログがコールバックや非同期ブロックの中にある場合は、先にメインスレッドに処理を移してください:
// Kotlin — コルーチンまたはコールバック内
runOnUiThread {
if (!isFinishing && !isDestroyed) {
AlertDialog.Builder(this)
.setMessage("Done")
.show()
}
}
// Java — Handlerを使用
new Handler(Looper.getMainLooper()).post(() -> {
if (!isFinishing() && !isDestroyed()) {
new AlertDialog.Builder(MyActivity.this)
.setMessage("Done")
.show();
}
});
4. 非同期処理・コルーチンのパターンを修正する
手動でライフサイクルを確認するのは応急処置に過ぎません。根本的な解決策は、非同期処理をActivityのライフサイクルに紐付け、画面が閉じられたときに自動的にキャンセルされるようにすることです:
// Kotlin — lifecycleScopeはActivityが破棄されると自動的にキャンセルされる
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) {
fetchData() // バックグラウンドスレッドで実行
}
// メインスレッドに戻る — ここではActivityの生存が保証されている
AlertDialog.Builder(this@MyActivity)
.setMessage(result)
.show()
}
isFinishing() のチェックは不要です。lifecycleScope はActivityが終了すると自動的にキャンセルされるため、コルーチンが show() に到達することはありません。
5. Fragmentの場合 — DialogFragmentを使用する
Fragmentからダイアログを表示する場合は、dialog.show() を直接呼び出さないでください。DialogFragment を使用し、Fragmentがまだアタッチされているか確認してください:
// Fragmentがまだアタッチされているか確認
if (isAdded && !requireActivity().isFinishing) {
MyDialogFragment().show(parentFragmentManager, "my_dialog")
}
状態保存後にどうしても表示する必要がある場合は、FragmentTransaction に対して commitAllowingStateLoss() を使用してください — ただし、ダイアログの状態が失われても問題ない場合に限ります。
6. onDestroyでダイアログを閉じる
ダイアログへの参照を保持している場合は、Activityが終了する前に後片付けをしてください。そうしないと、Androidがクラッシュという形で後片付けをすることになります:
private var progressDialog: AlertDialog? = null
override fun onDestroy() {
progressDialog?.dismiss()
progressDialog = null
super.onDestroy()
}
修正の確認
- 非同期処理を開始し、すぐに「戻る」ボタンを押すか画面を回転させる。クラッシュしなければ修正完了。
- デプロイ後はCrashlyticsを監視する —
BadTokenExceptionの発生率は1〜2日以内にゼロになるはずです。 - エッジケースを繰り返し試しながら
adb logcat | grep BadTokenExceptionを実行する。何も出力されないことが目標です。 - ユニットテストで、ダイアログ表示に関わるコードパスが
show()を呼び出す前にActivityの状態を確認していることをアサートする。
クイックリファレンス
- 常に確認:
dialog.show()の前に!activity.isFinishing() && !activity.isDestroyed()をチェック - 使用禁止: ダイアログのコンテキストとして
getApplicationContext()を使わない - 推奨: UIを操作する非同期処理には、生のスレッドやAsyncTaskではなく
lifecycleScope.launchを使用する - 閉じるタイミング: ダイアログへの参照を持っている場合は
onDestroy()で閉じる - 必ずテスト: 非同期処理とダイアログを組み合わせた画面では、必ず画面回転のテストを行う

