Jetpack Compose 底部导航实战教程(完整版)

Jetpack Compose 底部导航实战教程(完整版)

项目Gitee路径:https://gitee.com/developer_wind/BottomNavDemo


目录

  1. 最终效果
  2. 项目结构与技术栈
  3. 架构总览
  4. 准备工作:依赖与入口
  5. [基础篇:底部 5 Tab 导航](#基础篇:底部 5 Tab 导航)
  6. 扩展篇:五大进阶功能
  7. [数据层与 ViewModel](#数据层与 ViewModel)
  8. [Compose Preview](#Compose Preview)
  9. 验证方式
  10. 常见问题排查
  11. 构建与运行

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.ktnavigation/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 等。

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 时保留滚动位置等状态

核心流程:

复制代码
用户点击 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
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

需求

首页推荐列表点击卡片 → 详情页显示对应 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 闪动。


概念

点击 URL 直接打开 App 指定页面,例如:

  • https://bottomnavdemo.com/message → 消息 Tab
  • https://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 { ... }

LearnScreenTabRow 切换索引,uiState.selectedSubRoute 驱动内容与列表数据。

复制代码
底部 BottomBar
├── 首页   → home(嵌套 home_main / home_detail)
├── 发现   → discover
├── 学习   → learn
│            ├── [视频] Tab
│            ├── [文章] Tab
│            └── [课程] Tab
├── 消息   → message
└── 我的   → mine

嵌套导航下,currentDestinationhierarchy 仍包含 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 与单元测试
  • ViewModelinit 中从 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 导航

  1. 启动 App,默认在首页 Tab
  2. 依次点击 5 个 Tab,确认高亮与页面切换正确
  3. 反复切换 Tab,确认列表滚动位置等状态被保留

9.2 参数传递

  1. 首页点击「热门推荐」等卡片
  2. 进入详情页,显示 接收参数: {标题}
  3. 确认 BottomBar 已隐藏;TopAppBar 返回可回到列表
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

  1. 点击底部「学习」
  2. 切换「视频 / 文章 / 课程」
  3. 内容与列表变化,底部「学习」保持选中

9.5 Badge

  1. 启动后消息 Tab 显示数字角标(默认 5)
  2. 进入消息页并触发清除逻辑后,角标消失
  3. 发现页列表项可见 NEW 角标(若有新内容)

10. 常见问题排查

Q1:点击 Tab 没反应?

  1. onClick 是否调用 navigateSingleTop(item.route.path)
  2. composableroute 是否与 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 类加载时访问了 IconsRoute.Home.pathBottomNavItemslearnSubItems 应使用 by lazy

Q5:Preview 不显示?

  1. 是否有 @Preview 且包了 BottomNavDemoTheme
  2. 是否对 Preview 执行 Build & Refresh
  3. 是否使用 XxxScreenContent + 假 UiState,避免在 Preview 里启动 ViewModel

Q6:页面切换总是重建?

确认使用 navigateSingleTop(),不要裸调 navigate(route)


11. 构建与运行

  1. Android Studio 打开 D:\AsWorkSpace\BottomNavDemo
  2. Sync Project with Gradle Files
  3. 运行 app Debug 变体

命令行(若已配置 Gradle Wrapper):

bash 复制代码
./gradlew assembleDebug

路由速查表

路由 页面
home 首页嵌套图
discover 发现
learn 学习
message 消息
mine 我的

首页嵌套图(挂在 home 下)

路由 页面
home_main 首页列表
home_detail/{title} 详情(全屏,隐藏 BottomBar)
相关推荐
随遇丿而安5 小时前
第5周:XML 资源、样式和主题,真正解决的是“页面以后还改不改得动”
android
zh_xuan6 小时前
Android 获取系统内存页大小:sysconf(_SC_PAGESIZE) 与 JNI 实现
android·jni·ndk·内存页大小
fundroid7 小时前
Google I/O 2026 | Android 全面进化:从操作系统到“智能中枢”
android·jetpack compose·google i/o 2026
zh_xuan8 小时前
Android 复用 .so 库:通过 jniLibs 集成预编译二进制库(获取 Page Size )
android·jni·ndk·内存页大小
匆忙拥挤repeat9 小时前
Android Compose 约束布局
android
好安静9 小时前
Android ShellTransitions 机制完整分析(by DeepSeekV4Pro)
android
事后不诸葛9 小时前
安卓init.rc解析
android·framework
徒手猫9 小时前
myslq 中json 格式的数据如何获取某个属性
android·json
2401_827560209 小时前
【电脑和手机系统】解锁bl后刷LineageOS与Magisk各模块的安装(七)
android·linux·智能手机