※ChatGPTを使用して記事を作成しています。
■ 序章:突然、戻れなくなった日
ある日のこと。
いつものようにAndroid Studioを立ち上げ、Navigationを使って画面遷移を確認していた。
アプリの動作は一見問題なし。
しかし、ふと「戻る」ボタンを押しても画面が戻らない。
いや、正確に言えば「戻る」ボタンを押すたびに、アプリ全体が強制終了してホーム画面に戻る。
「え、なにこれ?」
Logcatを見ると、そこには無慈悲な一行。
java.lang.IllegalArgumentException: No destination with ID 2131362031 is on the NavController's back stack.
NavigationのBackStackが、跡形もなく消えていた。
■ 第一章:Navigationの甘い罠
当時の私は、Jetpack Navigationを「Fragmentの置き換えツール」程度に考えていた。
画面遷移をXMLで管理できて、findNavController().navigate()でサクッと飛ばせる。
しかも戻るボタンも自動で効く。便利なことこの上ない。
だが、ある日を境に戻るボタンが効かなくなった。
コードはこうだった。
// FragmentA から FragmentB へ遷移
findNavController().navigate(R.id.action_A_to_B)
そして FragmentB の中で、ユーザー操作に応じて別の画面へ遷移。
// FragmentB から FragmentC へ
findNavController().navigate(R.id.action_B_to_C)
ここまでは普通。
だが、C画面から「戻る」ボタンを押すと…
なぜか A に戻らず、アプリがクラッシュ。
「BackStackは自動で積まれるはずだろ?」と信じていた私は、そこで初めてNavigationの仕組みを深く調べることになる。
■ 第二章:BackStackが消える条件
NavigationのBackStackが吹き飛ぶ原因は、意外なところにあった。
犯人は、NavHostFragmentを複数置いた構成だった。
アプリでは、メイン画面にBottomNavigationViewを採用しており、各タブごとに別のNavHostFragmentを持っていた。
<fragment
android:id="@+id/nav_host_home"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="@navigation/nav_home"
app:defaultNavHost="false" />
<fragment
android:id="@+id/nav_host_settings"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="@navigation/nav_settings"
app:defaultNavHost="false" />
この構成で、ユーザーがタブを切り替えるたびに
別のNavControllerに切り替えていた。
val navController = when (selectedTab) {
Tab.HOME -> homeNavController
Tab.SETTINGS -> settingsNavController
}
しかし、ここで致命的な問題が。
NavigationはNavHostFragment単位でBackStackを保持している。
つまり、タブを切り替えた瞬間、他のタブのBackStackは破棄されてしまう。
結果として「Cから戻ろうとしたら、Aがもう存在していない」という地獄が生まれた。
■ 第三章:失敗例コード
当時の実装を簡略化したものがこちら。
// Activity上でNavHostFragmentを動的に切り替え
private fun switchTab(tab: Tab) {
val transaction = supportFragmentManager.beginTransaction()
val fragment = when (tab) {
Tab.HOME -> homeFragment
Tab.SETTINGS -> settingsFragment
}
transaction.replace(R.id.container, fragment)
transaction.commit()
}
このように、replace()を使ってFragmentを切り替えていた。
だがこれを行うと、前のNavHostFragmentが破棄されるため、BackStackも同時に消える。
そのため、FragmentC → 戻る、の操作が次のような悲劇を招いた。
java.lang.IllegalArgumentException:
No destination with ID ... is on the NavController's back stack.
NavigationのBackStack管理はNavControllerに紐づいており、破棄された時点で履歴は消滅する。
■ 第四章:修正版 ― NavHostFragmentを再生成しない
解決策はシンプルだが、発想の転換が必要だった。
NavHostFragmentを破棄せず、show/hideで切り替える。
修正版コードはこちら。
private fun switchTab(tab: Tab) {
val transaction = supportFragmentManager.beginTransaction()
listOf(homeFragment, settingsFragment).forEach { transaction.hide(it) }
val target = when (tab) {
Tab.HOME -> homeFragment
Tab.SETTINGS -> settingsFragment
}
transaction.show(target)
transaction.commitAllowingStateLoss()
}
このようにしておけば、各タブのNavControllerが保持され、BackStackも個別に生き続ける。
実際に戻るボタンを押しても、きちんと前の画面に戻るようになった。
■ 第五章:NavigationとBackStackの正しい付き合い方
Navigationは便利だが、内部の挙動を理解していないと簡単に破綻する。
特に気をつけたいポイントは次の3つだ。
- NavHostFragmentはNavControllerを1つしか持たない
→ NavControllerごとにBackStackが別管理される。 - replace()はNavHostFragmentごと破棄する
→ BackStackも消える。 - show()/hide()で切り替えるのが安全
→ 状態保持が可能。
■ 終章:あの日の学び
Navigationは魔法ではない。
内部ではしっかりとFragmentManagerとBackStackを使って管理されている。
私は「自動でやってくれる」と思い込み、BackStackの存在を軽視していた。
だが、Navigationは便利さの裏に構造的な前提がある。
それを理解しないまま「戻るボタンが効かない」と嘆いても、それは自分の責任だ。
あの日、BackStackが消えたのは単なるバグではなく、「理解しないまま便利さに頼ること」の代償だった。
■ 今日の教訓
Navigationは優秀だが、万能ではない。
BackStackは、あなたが守らなければならない。
■ まとめ
- replace()でNavHostFragmentを切り替えるとBackStackが消える
- タブ構成ではshow()/hide()でNavHostFragmentを保持する
- Navigationの構造を理解して使うことが大事

