Jetpack Compose 底部导航实战教程(完整版)
项目Gitee路径:
https://gitee.com/developer_wind/BottomNavDemo
目录
- 最终效果
- 项目结构与技术栈
- 架构总览
- 准备工作:依赖与入口
- [基础篇:底部 5 Tab 导航](#基础篇:底部 5 Tab 导航)
- 扩展篇:五大进阶功能
- [数据层与 ViewModel](#数据层与 ViewModel)
- [Compose Preview](#Compose Preview)
- 验证方式
- 常见问题排查
- 构建与运行
1. 最终效果
运行 App 后可体验:
| 能力 | 说明 |
|---|---|
| 5 个 Tab | 首页、发现、学习、消息、我的 |
| 图标切换 | 选中 Filled,未选中 Outlined |
| 高亮联动 | 点击 Tab,BottomBar 与内容区同步 |
| 返回栈控制 | launchSingleTop + popUpTo + saveState / restoreState |
| 首页 → 详情 | 列表项点击传递 title,详情全屏并隐藏 BottomBar |
| 嵌套导航 | 首页使用 navigation 嵌套图;学习 Tab 内 TabRow 子页 |
| Deep Link | URL 直达消息页或首页详情 |
| Badge | 消息 Tab 数字角标;发现列表 NEW 角标 |
| 主题 | Material 3 亮/暗色跟随系统 |
| Edge-to-Edge | enableEdgeToEdge() + 各页 statusBarsPadding |
2. 项目结构与技术栈
2.1 目录结构
app/src/main/java/com/example/bottomnav/
├── MainActivity.kt # 薄壳:仅 setContent
├── data/
│ ├── AppRepositories.kt # Repository 单例入口
│ ├── model/ # HomeItem、DiscoverCategory 等
│ └── repository/ # 接口 + Fake* 实现
├── navigation/
│ ├── route.kt # Route / HomeRoute / DeepLink / 扩展函数
│ ├── BottomNavItems.kt # Tab 配置(Icons 延迟加载)
│ ├── NavHostExtensions.kt # navigateSingleTop
│ ├── AppNavHost.kt # 根 NavHost
│ └── HomeNavGraph.kt # 首页嵌套 NavGraph
└── ui/
├── BottomNavApp.kt # Scaffold + NavigationBar + Badge
├── viewmodel/ # 各页 ViewModel + MainViewModel
├── screens/ # home / discover / learn / message / mine
└── theme/ # Color / Theme / Type
2.2 技术栈
| 类别 | 版本 |
|---|---|
| Kotlin | 1.9.24 |
| compileSdk / targetSdk | 35 |
| minSdk | 26 |
| Compose BOM | 2024.10.00 |
| Navigation Compose | 2.8.4 |
| Lifecycle | 2.8.7 |
| Coroutines | 1.7.3 |
2.3 依赖(app/build.gradle)
groovy
dependencies {
implementation platform('androidx.compose:compose-bom:2024.10.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.compose.material:material-icons-extended'
implementation 'androidx.navigation:navigation-compose:2.8.4'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7'
implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.8.7'
implementation 'androidx.activity:activity-compose:1.9.3'
debugImplementation 'androidx.compose.ui:ui-tooling'
}
3. 架构总览
MainActivity
└── BottomNavApp(Scaffold + NavigationBar)
├── showBottomBar:详情页 / 非主 Tab 时隐藏
└── AppNavHost(根 NavHost)
├── home(嵌套图 HomeNavGraph)
│ ├── home_main → HomeScreen
│ └── home_detail/{title} → HomeDetailScreen
├── discover → DiscoverScreen
├── learn → LearnScreen
├── message → MessageScreen(Deep Link)
└── mine → MineScreen
Screen → ViewModel → AppRepositories → Repository 接口 → Fake*Repository
| 层级 | 职责 |
|---|---|
| ui | Composable;只读 UiState;通过回调触发导航 |
| viewmodel | StateFlow<UiState>;调用 Repository |
| data | 数据模型 + 假数据源,便于替换真实 API |
| navigation | 路由、NavHost、嵌套图、Tab 配置 |
4. 准备工作:依赖与入口
4.1 MainActivity(薄壳)
kotlin
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
BottomNavDemoTheme {
BottomNavApp()
}
}
}
}
所有导航与 UI 组装在 ui/BottomNavApp.kt 与 navigation/AppNavHost.kt,便于测试与维护。
5. 基础篇:底部 5 Tab 导航
5.1 定义类型安全路由 --- route.kt
kotlin
sealed class Route(val path: String) {
data object Home : Route("home")
data object Discover : Route("discover")
data object Learn : Route("learn")
data object Message : Route("message")
data object Mine : Route("mine")
companion object {
val mainTabRoutes = listOf("home", "discover", "learn", "message", "mine")
}
}
集中管理 route 字符串,避免硬编码与拼写错误。
5.2 Tab 配置 --- BottomNavItems.kt
kotlin
data class BottomNavItem(
val route: Route,
val title: String,
val selectedIcon: ImageVector,
val unselectedIcon: ImageVector
)
object BottomNavItems {
val items: List<BottomNavItem> by lazy {
listOf(
BottomNavItem(Route.Home, "首页", Icons.Filled.Home, Icons.Outlined.Home),
// ... 发现 / 学习 / 消息 / 我的
)
}
}
注意 :items 使用 by lazy,避免在类加载阶段访问 Icons 导致 ExceptionInInitializerError。不要在 Route 的 <clinit> 里引用 Route.Home.path 等。
5.3 封装 Tab 切换 --- NavHostExtensions.kt
kotlin
fun NavHostController.navigateSingleTop(route: String) {
navigate(route) {
popUpTo(graph.findStartDestination().id) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
| 选项 | 作用 |
|---|---|
launchSingleTop |
栈顶已有同 route 则不重复创建 |
popUpTo(start) |
弹出到图起始点,防止返回栈无限加深 |
saveState / restoreState |
切换 Tab 时保留滚动位置等状态 |
5.4 组装 BottomBar + NavHost --- BottomNavApp.kt
核心流程:
用户点击 Tab「发现」
→ navigateSingleTop("discover")
→ NavHost 显示 DiscoverScreen
→ hierarchy 匹配,BottomBar 高亮「发现」
kotlin
@Composable
fun BottomNavApp(viewModel: MainViewModel = viewModel()) {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
val backStackEntries by navController.currentBackStack.collectAsState()
val hasOverlayPage = backStackEntries.hasHomeDetail()
val showBottomBar = !hasOverlayPage && currentDestination.isMainTabDestination()
Scaffold(
bottomBar = {
if (showBottomBar) {
NavigationBar {
BottomNavItems.items.forEach { item ->
val selected = currentDestination?.hierarchy?.any {
it.route == item.route.path
} == true
NavigationBarItem(
selected = selected,
onClick = { navController.navigateSingleTop(item.route.path) },
// icon / label ...
)
}
}
}
}
) { innerPadding ->
AppNavHost(navController, viewModel, innerPadding.calculateBottomPadding())
}
}
5.5 判断 Tab 是否选中
kotlin
// ✅ 正确:用 hierarchy(嵌套图下首页实际 route 是 home_main)
val selected = currentDestination?.hierarchy?.any {
it.route == item.route.path
} == true
// ❌ 错误:直接比 currentRoute
val selected = currentDestination?.route == item.route.path
5.6 根 NavHost --- AppNavHost.kt
kotlin
NavHost(
navController = navController,
startDestination = Route.Home.path,
modifier = Modifier.fillMaxSize().padding(bottom = bottomPadding)
) {
homeNavGraph(navController)
composable(Route.Discover.path) { DiscoverScreen() }
composable(Route.Learn.path) { LearnScreen() }
composable(Route.Message.path) { MessageScreen(onClearBadge = { ... }) }
composable(Route.Mine.path) { MineScreen() }
}
5.7 返回栈示意
初始:[home → home_main]
点击「发现」并 popUpTo 后:[home..., discover]
按系统返回:依次 pop,不会无限堆叠 Tab
6. 扩展篇:五大进阶功能
6.1 扩展功能总览
| 扩展 | 功能 | 关键 API / 文件 |
|---|---|---|
| 1 | 首页 → 详情,传递 title |
navArgument + HomeRoute.Detail + HomeNavGraph.kt |
| 2 | Deep Link 打开指定页 | navDeepLink + AndroidManifest.xml |
| 3 | 学习 Tab 内视频/文章/课程 | TabRow + LearnViewModel |
| 4 | 消息 Tab 未读角标 | BadgedBox + MainViewModel |
| 5 | 自定义 BottomBar 样式 | NavigationBar + Modifier / colors |
6.2 扩展 1:页面间传递参数(嵌套 NavGraph)
需求
首页推荐列表点击卡片 → 详情页显示对应 title。
路由定义
kotlin
sealed class HomeRoute(val path: String) {
data object Main : HomeRoute("home_main")
data object Detail : HomeRoute("home_detail/{title}") {
const val argumentName = "title"
fun create(title: String): String = "home_detail/$title"
}
}
嵌套图注册 --- HomeNavGraph.kt
kotlin
fun NavGraphBuilder.homeNavGraph(navController: NavHostController) {
navigation(route = Route.Home.path, startDestination = HomeRoute.Main.path) {
composable(HomeRoute.Main.path) {
HomeScreen(
onNavigateToDetail = { title ->
navController.navigate(HomeRoute.Detail.create(title))
}
)
}
composable(
route = HomeRoute.Detail.path,
arguments = listOf(
navArgument(HomeRoute.Detail.argumentName) {
type = NavType.StringType
}
),
deepLinks = listOf(
navDeepLink {
uriPattern = "https://bottomnavdemo.com/home_detail/{${HomeRoute.Detail.argumentName}}"
}
)
) {
HomeDetailScreen(onNavigateBack = { navController.popBackStack() })
}
}
}
首页触发跳转
kotlin
HomeCard(item = item, onClick = { onNavigateToDetail(item.title) })
详情页接收参数 --- HomeDetailViewModel
通过 SavedStateHandle 读取导航参数(推荐做法,无需在 Composable 里手动 getString):
kotlin
class HomeDetailViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
private val _uiState = MutableStateFlow(
HomeDetailUiState(
title = savedStateHandle.get<String>(HomeRoute.Detail.argumentName) ?: "未知"
)
)
val uiState: StateFlow<HomeDetailUiState> = _uiState.asStateFlow()
}
参数传递流程
HomeScreen 点击「技术文章」
→ navigate("home_detail/技术文章")
→ NavHost 匹配 home_detail/{title}
→ SavedStateHandle["title"] = "技术文章"
→ HomeDetailScreen 显示「接收参数: 技术文章」
详情页隐藏 BottomBar
kotlin
val hasOverlayPage = backStackEntries.hasHomeDetail()
val showBottomBar = !hasOverlayPage && currentDestination.isMainTabDestination()
hasHomeDetail() 检查返回栈是否仍含 home_detail/...,避免 pop 动画期间 BottomBar 闪动。
6.3 扩展 2:深层链接 Deep Link
概念
点击 URL 直接打开 App 指定页面,例如:
https://bottomnavdemo.com/message→ 消息 Tabhttps://bottomnavdemo.com/home_detail/公告→ 首页详情(title=公告)
代码配置
route.kt:
kotlin
object DeepLinkDestinations {
const val messageDeepLink = "https://bottomnavdemo.com/message"
fun createHomeDetailDeepLink(title: String) =
"https://bottomnavdemo.com/home_detail/$title"
}
AppNavHost.kt(消息页)与 HomeNavGraph.kt(详情页)中分别配置 navDeepLink。
AndroidManifest.xml
xml
<!-- 消息页 -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="bottomnavdemo.com" android:path="/message" />
</intent-filter>
<!-- 首页详情(pathPrefix 匹配带参数路径) -->
<intent-filter>
...
<data android:scheme="https" android:host="bottomnavdemo.com" android:pathPrefix="/home_detail" />
</intent-filter>
ADB 测试
bash
adb shell am start -a android.intent.action.VIEW -d "https://bottomnavdemo.com/message"
adb shell am start -a android.intent.action.VIEW -d "https://bottomnavdemo.com/home_detail/技术文章"
| URL | 目标页面 |
|---|---|
.../message |
消息 Tab |
.../home_detail/{title} |
首页详情 |
6.4 扩展 3:学习 Tab 内子页面(TabRow)
学习 Tab 不 再单独建 NavGraph 子路由,而是用 TabRow + LearnViewModel 管理子 Tab 状态(BottomBar 始终停在「学习」)。
route.kt:
kotlin
sealed class LearnSubRoute(val route: String, val title: String) {
data object Video : LearnSubRoute("video", "视频")
data object Article : LearnSubRoute("article", "文章")
data object Course : LearnSubRoute("course", "课程")
}
val learnSubItems: List<LearnSubRoute> by lazy { ... }
LearnScreen 中 TabRow 切换索引,uiState.selectedSubRoute 驱动内容与列表数据。
底部 BottomBar
├── 首页 → home(嵌套 home_main / home_detail)
├── 发现 → discover
├── 学习 → learn
│ ├── [视频] Tab
│ ├── [文章] Tab
│ └── [课程] Tab
├── 消息 → message
└── 我的 → mine
嵌套导航下,currentDestination 的 hierarchy 仍包含 learn,故 BottomBar 选中态不变。
6.5 扩展 4:Badge 角标
消息 Tab 数字角标
状态由 MainViewModel 管理(非 Composable 内 remember):
kotlin
data class MainUiState(val messageBadgeCount: Int = 5)
class MainViewModel : ViewModel() {
fun clearMessageBadge() {
_uiState.update { it.copy(messageBadgeCount = 0) }
}
}
BottomNavApp.kt:
kotlin
BadgedBox(
badge = {
if (item.route == Route.Message && uiState.messageBadgeCount > 0) {
Badge {
Text(if (uiState.messageBadgeCount > 99) "99+" else uiState.messageBadgeCount.toString())
}
}
}
) { Icon(...) }
MessageScreen 通过 onClearBadge = { mainViewModel.clearMessageBadge() } 在适当时机清除角标。
发现页列表 NEW 角标
DiscoverScreen 列表项在 newCount > 0 时显示 Badge { Text("NEW") },数据来自 DiscoverViewModel / FakeDiscoverRepository。
6.6 扩展 5:自定义 BottomBar 样式
当前项目使用默认 Material 3 NavigationBar。可按需在 BottomNavApp.kt 调整,例如:
kotlin
NavigationBar(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp),
containerColor = MaterialTheme.colorScheme.surface,
tonalElevation = 2.dp
) { ... }
NavigationBarItem(
colors = NavigationBarItemDefaults.colors(
selectedIconColor = Color(0xFF4F46E5),
unselectedIconColor = Color(0xFF9CA3AF),
indicatorColor = Color(0xFFE0E7FF)
),
// ...
)
圆角示例:Modifier.clip(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp))。
7. 数据层与 ViewModel
7.1 Repository 模式
kotlin
// AppRepositories.kt --- 统一入口,替换实现只需改此处
object AppRepositories {
val home: HomeRepository = FakeHomeRepository()
val discover: DiscoverRepository = FakeDiscoverRepository()
// ...
}
7.2 典型页面结构
kotlin
@Composable
fun HomeScreen(
onNavigateToDetail: (String) -> Unit,
viewModel: HomeViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsState()
HomeScreenContent(uiState = uiState, onNavigateToDetail = onNavigateToDetail)
}
- Screen:注入 ViewModel,收集状态
- ScreenContent:纯 UI,便于 Preview 与单元测试
- ViewModel :
init中从 Repository 加载数据
8. Compose Preview
各页面采用 Screen / ScreenContent 分离:
| 组件 | 职责 |
|---|---|
HomeScreen |
ViewModel + collectAsState |
HomeScreenContent |
接收 UiState,无 ViewModel |
HomeScreenPreview |
使用 previewHomeUiState 预览 Content |
kotlin
@Preview(name = "亮色", showBackground = true)
@Preview(name = "暗色", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun HomeScreenPreview() {
BottomNavDemoTheme {
HomeScreenContent(uiState = previewHomeUiState, onNavigateToDetail = {})
}
}
| Preview 函数 | 文件 |
|---|---|
HomeScreenPreview |
ui/screens/home/HomeScreen.kt |
HomeDetailScreenPreview |
ui/screens/home/detail/HomeDetailScreen.kt |
DiscoverScreenPreview |
ui/screens/discover/DiscoverScreen.kt |
LearnScreenPreview |
ui/screens/learn/LearnScreen.kt |
MessageScreenPreview |
ui/screens/message/MessageScreen.kt |
MineScreenPreview |
ui/screens/mine/MineScreen.kt |
预览不显示时:对 Preview 函数执行 Build & Refresh ;确认已添加 debugImplementation 'androidx.compose.ui:ui-tooling'。
9. 验证方式
9.1 基础 Tab 导航
- 启动 App,默认在首页 Tab
- 依次点击 5 个 Tab,确认高亮与页面切换正确
- 反复切换 Tab,确认列表滚动位置等状态被保留
9.2 参数传递
- 首页点击「热门推荐」等卡片
- 进入详情页,显示
接收参数: {标题} - 确认 BottomBar 已隐藏;TopAppBar 返回可回到列表
9.3 Deep Link
bash
adb shell am start -a android.intent.action.VIEW -d "https://bottomnavdemo.com/message"
adb shell am start -a android.intent.action.VIEW -d "https://bottomnavdemo.com/home_detail/公告"
9.4 学习子 Tab
- 点击底部「学习」
- 切换「视频 / 文章 / 课程」
- 内容与列表变化,底部「学习」保持选中
9.5 Badge
- 启动后消息 Tab 显示数字角标(默认 5)
- 进入消息页并触发清除逻辑后,角标消失
- 发现页列表项可见
NEW角标(若有新内容)
10. 常见问题排查
Q1:点击 Tab 没反应?
onClick是否调用navigateSingleTop(item.route.path)composable的route是否与Route.xxx.path完全一致(区分大小写)
Q2:Tab 高亮不对?
必须使用 currentDestination?.hierarchy?.any { it.route == item.route.path }。首页嵌套后当前 route 为 home_main,不是 home。
Q3:详情页 BottomBar 闪一下?
使用 backStackEntries.hasHomeDetail() 判断,不要仅用 currentDestination?.route。
Q4:启动崩溃 ExceptionInInitializerError?
检查是否在 Route 类加载时访问了 Icons 或 Route.Home.path;BottomNavItems 与 learnSubItems 应使用 by lazy。
Q5:Preview 不显示?
- 是否有
@Preview且包了BottomNavDemoTheme - 是否对 Preview 执行 Build & Refresh
- 是否使用
XxxScreenContent+ 假UiState,避免在 Preview 里启动 ViewModel
Q6:页面切换总是重建?
确认使用 navigateSingleTop(),不要裸调 navigate(route)。
11. 构建与运行
- 用 Android Studio 打开
D:\AsWorkSpace\BottomNavDemo - Sync Project with Gradle Files
- 运行
appDebug 变体
命令行(若已配置 Gradle Wrapper):
bash
./gradlew assembleDebug
路由速查表
根 NavHost
| 路由 | 页面 |
|---|---|
home |
首页嵌套图 |
discover |
发现 |
learn |
学习 |
message |
消息 |
mine |
我的 |
首页嵌套图(挂在 home 下)
| 路由 | 页面 |
|---|---|
home_main |
首页列表 |
home_detail/{title} |
详情(全屏,隐藏 BottomBar) |