
公式サンプルは複雑すぎない?
と思ったので。
🤔 実際のアプリはボトムナビゲーションをどう扱っているか
実装に入る前に、主要アプリの挙動を見てみましょう。
ほとんどのアプリは以下のどれかです:
- タブごとに独立した BackStack(最も一般的)
- 1つの共有 BackStack(最もシンプル)
- タブ切り替え時にリセット(常にルートに戻る)
🤔 バックボタンの挙動
タブ内の履歴
↓
タブのルート
↓
アプリ終了
- タブの切り替えは手動(バックボタンでは行わない)
- 各タブは独自の履歴を持つ
- 履歴が空になったらアプリを終了
- 同じタブを再度タップ → そのタブをルートにリセット
- タブ数は固定
🤔 考え方
タブをキーとした BackStack の Map を使う
これだけです。複雑な状態ホルダーは一切不要です。
🤔 主要コンポーネント
- NavKey
- NavBackStack
- NavigationBar / NavigationBarItem
- NavDisplay
🤔 ナビゲーションモデル
NavKey
└── TabRoot (エントリポイント)
├── Home
├── Search
│ ├── Result (検索結果画面)
│ └── Detail (検索詳細画面)
└── Profile
@Serializable
sealed interface TabRoot : NavKey {
val label: String
val selectedIcon: ImageVector
val unselectedIcon: ImageVector
companion object {
val entries = listOf(Home, Search, Profile)
}
}
@Serializable
data object Home : TabRoot {
override val label = "Home"
override val selectedIcon = Icons.Filled.Home
override val unselectedIcon = Icons.Outlined.Home
}
@Serializable
data object Search : TabRoot { ... }
@Serializable
data object Profile : TabRoot { ... }
@Serializable
data class Result(val keyword: String) : NavKey
@Serializable
data class Detail(val id: String) : NavKey
なぜこれがうまくいくか
- 単一の型安全なナビゲーションモデル
- タブと画面が同じシステムを共有
- @Serializable で状態復元が可能
- sealed + object でコンパイル時安全性
🤔 状態管理
1. 選択中のタブ
var currentTab by rememberSerializable {
mutableStateOf<TabRoot>(Home)
}
var currentTab by rememberSerializable {
mutableStateOf<TabRoot>(Home)
}
2. 複数の NavBackStack
val stacks = TabRoot.entries.associateWith { root ->
rememberNavBackStack(root)
}
重要なポイント
- Map は再コンポーズごとに再作成されが生成コストは小さい
- しかし各 NavBackStack 維持されて再作成されない
これで安全かつシンプルに実現できます。
3. 現在の NavBackStack
val currentStack = stacks[currentTab]!!
Compose では currentTab が変わると自動的に更新されます。
4. タブ切り替え
onClick = {
currentTab = root
}
これだけです。
5. 画面遷移(Push)kotlin
onClick = {
currentStack.add(Result(keyword))
}
6. 戻る操作(Pop)
NavDisplay(
onBack = {
currentStack.removeAt(currentStack.lastIndex)
}
)
- ルートを削除しようとしたらアプリ終了(Android 標準挙動)
7. 同じタブを再度タップしたときのリセット
if (selected) {
currentStack.clear()
currentStack.add(root)
}
- Instagram や Twitter(X)と同じ挙動になります。
🧑🏻💻 最終的な最小パターン
var currentTab by rememberSerializable {
mutableStateOf<TabRoot>(Home)
}
val stacks = TabRoot.entries.associateWith { root ->
rememberNavBackStack(root)
}
val currentStack = stacks[currentTab]!!
🚀 このアプローチが優れている理由
- コード量が最小
- 実際のアプリ挙動にマッチ
- 完全に Compose ネイティブ
- 将来的に拡張しやすい
- Process Death 後も完全な状態復元が可能
というかんじでどうでしょうか。
🚗💨 参考
Related Categories : Android・Developmemt・JetpackCompose・Kotlin・Recommended・Trending