TL;DR
Androidがアクティビティの状態を保存した後に FragmentManager.commit() を呼び出しています — 通常は、ユーザーがホームボタンを押したり画面を回転させた後に非同期コールバックが発火したことが原因です。解決策は2つあります:復元時にトランザクションが失われても問題ない場合は commit() を commitAllowingStateLoss() に置き換えるか、コミット前にライフサイクル状態を確認してガード処理を入れる方法です。
エラー全文
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at androidx.fragment.app.FragmentManager.checkStateLoss(FragmentManager.java:...)
at androidx.fragment.app.FragmentManager.enqueueAction(FragmentManager.java:...)
at androidx.fragment.app.BackStackRecord.commitInternal(BackStackRecord.java:...)
at androidx.fragment.app.BackStackRecord.commit(BackStackRecord.java:...)
発生原因
Androidが onSaveInstanceState() を呼び出すと、Fragmentのバックスタック全体を含むアクティビティのスナップショットが保存されます。このスナップショットが取得された後、FragmentManager はバックスタックへのそれ以上の変更を拒否します。その状態で変更しようとすると、このクラッシュが発生します。
主に発生するケース:
- ユーザーがホームボタンを押したり画面が回転した後にRetrofitやCoroutineのコールバックが返ってくる場合
DialogInterface.OnClickListener内でダイアログをコミットしている場合onActivityResult()内でFragmentをコミットしている場合(onResume()より前に実行される)- アクティビティがすでに一時停止した後にバックグラウンドスレッドがメインスレッドにポストする場合
- ライフサイクルが非アクティブになった後に
LiveDataオブザーバーやRxJavaストリームが発火する場合
修正1:commitAllowingStateLoss() — 手軽な応急処置
commit() を commitAllowingStateLoss() に置き換えます。これにより状態ロストのチェックが完全にスキップされます。トレードオフとして、アクティビティが再生成された場合このトランザクションは再実行されません。ローディングスピナーのような使い捨てUIには許容できますが、実際のナビゲーションには適していません。
// 変更前
supportFragmentManager.beginTransaction()
.replace(R.id.container, MyFragment())
.commit()
// 変更後
supportFragmentManager.beginTransaction()
.replace(R.id.container, MyFragment())
.commitAllowingStateLoss()
Javaバージョン:
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, new MyFragment())
.commitAllowingStateLoss();
この方法は、プログレスバーの非表示やローディングダイアログの閉じるなど、本当に使い捨てのトランザクションにのみ使用してください。ナビゲーションや設定変更後にユーザーに見せる必要があるものは、適切な修正が必要です。
修正2:コミット前にライフサイクル状態を確認する
アクティビティが安全な状態のときのみコミットが実行されるようにガード処理を入れます。
// Kotlin — Activityの場合
if (!isFinishing && !isDestroyed) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, MyFragment())
.commit()
}
// Kotlin — Fragmentの場合
if (isAdded && !requireActivity().isFinishing) {
parentFragmentManager.beginTransaction()
.replace(R.id.container, MyFragment())
.commit()
}
Coroutineベースのコードには、lifecycle.withStarted がよりすっきりした解決策です。ライフサイクルが少なくとも STARTED(つまり onStart() が実行済み)に達するまでサスペンドし、その後ブロックを実行します。
lifecycleScope.launch {
lifecycle.withStarted {
supportFragmentManager.beginTransaction()
.replace(R.id.container, MyFragment())
.commit()
}
}
lifecycle-runtime-ktx 2.4.0+ が必要です。ライフサイクルが再び STARTED に達しない場合(例:アクティビティが終了する場合)、ブロックは単純に実行されません。
修正3:適切なライフサイクルオーナーでLiveDataを使用する
ViewModelオブザーバー内でクラッシュしている場合、渡しているライフサイクルオーナーを確認してください。Fragmentでは、this はFragment自体を参照しており、ビューよりも長生きします。代わりに viewLifecycleOwner を使用することで、オブザーバーがビューとともに破棄されます。
// 誤り — ビューが破棄された後もオブザーバーが生き続ける
viewModel.result.observe(this) { navigateToDetail(it) }
// 正しい — FragmentのビューライフサイクルにバインドされているC
viewModel.result.observe(viewLifecycleOwner) { navigateToDetail(it) }
viewLifecycleOwner を使用することで、Fragmentが画面から外れた際にオブザーバーが自動的に削除されます。破棄されたFragmentに古いコールバックが発火することはなくなります。
修正4:onActivityResult() のタイミング問題
onActivityResult() は onResume() より前に発火するため、その時点では onSaveInstanceState() がすでに実行されています。そこでFragmentをコミットするとクラッシュします。代わりにアクションを保存しておき、onResume() に戻った時点で実行します。
private var pendingAction: (() -> Unit)? = null
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == MY_REQUEST && resultCode == RESULT_OK) {
pendingAction = { showResultFragment() }
}
}
override fun onResume() {
super.onResume()
pendingAction?.invoke()
pendingAction = null
}
API 29以上(または registerForActivityResult を使ったActivity Result API)では、このワークアラウンドは不要です — コールバックはすでに onResume() の後に発火します。
修正5:RetrofitおよびAsync コールバック
ネットワーク呼び出しは、本番環境でこのクラッシュが発生する最大の原因です。リクエストに2〜3秒かかる間に、ユーザーが1秒後に画面を閉じることがあります。コールバックが返ってきてFragmentをコミットしようとした瞬間にクラッシュします。
// 悪い例 — ライフサイクルを考慮せずにコミットしている
retrofitService.getData().enqueue(object : Callback<Data> {
override fun onResponse(call: Call<Data>, response: Response<Data>) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, ResultFragment.newInstance(response.body()!!))
.commit()
}
...
})
// より良い例 — 画面がバックグラウンドになった時にCallをキャンセルする
override fun onStop() {
super.onStop()
pendingCall?.cancel()
}
長期的な本質的解決策は viewModelScope を使ったCoroutineへの移行です。このスコープはViewModelがクリアされると自動的にキャンセルされるため、Fragmentトランザクションがスケジュールされることすらなくなります。
viewModelScope.launch {
val result = repository.getData() // ViewModelがクリアされると自動キャンセル
_uiState.value = result
}
これを viewLifecycleOwner で監視する StateFlow または LiveData と組み合わせることで、この問題のクラス全体を排除できます。
どの修正を使うべきか?
- Async コールバック(Retrofit、RxJava) →
viewModelScope+LiveData/StateFlowを使ったCoroutineに移行する - LiveDataオブザーバーが遅れて発火する → Fragmentで
viewLifecycleOwnerに切り替える - onActivityResult のタイミング問題 → ペンディングアクションパターンを使うか、Activity Result APIに移行する
- ローディング・プログレスダイアログ →
commitAllowingStateLoss()で問題なく対応できる - 素早い応急処置が必要 → ライフサイクルガード(
isAdded && !isFinishing)+commitAllowingStateLoss()
修正の確認方法
- クラッシュを再現する:非同期アクションをトリガーし、完了前にすぐホームボタンを押すか画面を回転させる。
- 修正を適用して同じ手順を繰り返す。
- Logcatを確認する —
IllegalStateExceptionの行がゼロになっているはずです。 - アプローチに応じて、Fragmentトランザクションが再開時に正しくコミットされたか、またはクリーンにスキップされたかを確認する。
- 高速なナビゲーションとバックグラウンドネットワーク負荷でストレステストを実施する:
adb logcat | grep IllegalStateExceptionで見落としたエッジケースを検出する。
参考資料
- AndroidX Fragment —
FragmentManagerドキュメント - Androidライフサイクル対応コンポーネントガイド
lifecycleScopeとviewModelScope— AndroidにおけるKotlin Coroutines- Activity Result API —
startActivityForResultの代替手段

