一、使用官方 Navigation 组件(单 Activity,多 NavGraph)
比如,我们要做成底部导航栏去动态切换。
kt
// 1. 定义密封类或对象来存储目的地信息
sealed class BottomNavItem(
val route: String,
val icon: ImageVector,
val title: String
) {
object Home : BottomNavItem("home", Icons.Default.Home, "Home")
object Search : BottomNavItem("search", Icons.Default.Search, "Search")
object Profile : BottomNavItem("profile", Icons.Default.Person, "Profile")
}
// 2. 在主界面 Scaffold 中设置
@Composable
fun MainScreen() {
val navController = rememberNavController()
val currentBackStack by navController.currentBackStackEntryAsState()
val currentDestination = currentBackStack?.destination
Scaffold(
bottomBar = {
NavigationBar {
// 获取所有底部导航项
val navItems = listOf(
BottomNavItem.Home,
BottomNavItem.Search,
BottomNavItem.Profile
)
navItems.forEach { item ->
NavigationBarItem(
// 高亮选中当前目的地对应的Item
selected = currentDestination?.route == item.route,
onClick = {
// 导航到该目的地,并采用弹出到起始点的策略
// 避免在点击底部栏Item时在栈中堆积多个同一目的地的实例
navController.navigate(item.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// 如果点击的是当前已选中的Item,则避免重复导航
launchSingleTop = true
// 恢复之前保存的状态(如果存在)
restoreState = true
}
},
icon = { Icon(item.icon, contentDescription = item.title) },
label = { Text(item.title) }
)
}
}
}
) { innerPadding ->
// 3. 设置 NavHost
NavHost(
navController = navController,
startDestination = BottomNavItem.Home.route,//设置主页
modifier = Modifier.padding(innerPadding)
) {
// 为每个目的地定义可组合函数
composable(BottomNavItem.Home.route) { HomeScreen(navController) }
composable(BottomNavItem.Search.route) { SearchScreen(navController) }
composable(BottomNavItem.Profile.route) { ProfileScreen(navController) }
}
}
}
当然,它不一定需要导航栏,比如有一种情况就是,我们希望通过页面的一些按钮或逻辑来切换,那么我们可以去掉底部导航栏。直接这样就行。
kt
@Composable
fun MainScreen() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = BottomNavItem.Home.route,//设置主页
modifier = Modifier.padding(innerPadding)
) {
// 为每个目的地定义可组合函数
composable(BottomNavItem.Home.route) { HomeScreen(navController) }
composable(BottomNavItem.Search.route) { SearchScreen(navController) }
composable(BottomNavItem.Profile.route) { ProfileScreen(navController) }
}
}
还有一种页面切换的方式,NavigationBar+HorizontalPager
二、NavigationBar+HorizontalPager
kt
@Composable
fun MainScaffold(navHostController: NavHostController) {
// 创建页面状态(使用Accompanist Pager)
val pagerState = rememberPagerState(
pageCount = { MainScreen.values().size } // 页面数量
)
// 创建协程作用域
val scope = rememberCoroutineScope()
// 当前选中的页面索引(状态)
var selectedScreen by remember { mutableIntStateOf(0) }
// 监听页面变化并更新选中状态
LaunchedEffect(pagerState) {
// 使用snapshotFlow将页面状态转换为Flow
snapshotFlow { pagerState.currentPage }.collect { page ->
selectedScreen = page
}
}
// 使用Scaffold布局
Scaffold(
bottomBar = {
// 自定义底部导航栏
WanBottomBar(
selectedScreen = selectedScreen, // 当前选中项
onClick = { screenIndex ->
// 点击导航项时滚动到对应页面
scope.launch {
pagerState.scrollToPage(screenIndex)
}
}
)
}
) { innerPadding ->
// 水平分页器(实现页面滑动)
HorizontalPager(
state = pagerState,
userScrollEnabled = false, // 禁用用户滑动(仅通过底部导航切换)
contentPadding = innerPadding // 内边距
) { pageIndex ->
// 根据页面索引显示对应内容
when (MainScreen.values()[pageIndex]) {
MainScreen.Home -> HomeScreen(navHostController) // 首页
MainScreen.Project -> ProjectScreen() // 项目页
MainScreen.Navigator -> NavigatorScreen() // 导航页
MainScreen.Group -> GroupScreen() // 分组页
else -> ProfileScreen() // 个人中心页
}
}
}
}
三、这两种方式的区别在哪里呢?
我们可以类比一下原生的
-
方案一 (Navigation Component) 类似于使用 一个 Activity + 多个 Fragment ,并且用
NavHostFragment
+ 导航图 来管理它们。当你导航时,发生的是 Fragment 事务(Transaction),当前 Fragment 被替换,Fragment就会被销毁了。 -
方案二 (Pager) 类似于使用 一个 Activity + 一个 ViewPager2 ,并且使用
FragmentStateAdapter
或 FragmentPagerAdapter
。所有 Fragment 都已经被创建并添加到 ViewPager 中,滑动只是切换显示。
所以,切换会导致重新执行吗?
-
Navigation 组件 :会 。
composable { ... }
中的代码块会在目的地成为当前目的地时执行,离开时销毁。LaunchedEffect也会。 -
HorizontalPager :不会 。每个页面的
Composable
只在HorizontalPager
首次进入组合时执行一次,之后不会被重新执行,LaunchedEffect也不会,除非整个MainScaffold
重组。