【NowInAndroid架构拆解】系列文章
- 【NowInAndroid架构拆解】(1)分层设计与模块化
- 【NowInAndroid架构拆解】(2)数据层的设计和实现之model与database
- 【NowInAndroid架构拆解】(3)数据层的设计和实现之network
- 【NowInAndroid架构拆解】(4)数据层的设计和实现之data
- 【NowInAndroid架构拆解】(5)VM层的设计和实现之ForYouViewModel
- 【NowInAndroid架构拆解】(6)View层的设计和实现之Navigation路由
- 【NowInAndroid架构拆解】(7)UI层解析——MainActivity构建过程
- 【NowInAndroid架构拆解】(8)UI层解析——ForYou页面展示
- 【NowInAndroid架构拆解】(9)重新审视NowInAndroid架构设计
- 【NowInAndroid架构拆解】番外篇1之Jetpack Compose Navigation
- 【NowInAndroid架构拆解】番外篇2之Bottom Navigation底部导航
- 【NowInAndroid架构拆解】番外篇3之给xml布局者最佳的Jetpack Compose介绍文章
底部导航栏是当代APP中常用的布局手段,Navigation组件同样支持这种场景下的导航跳转。

相比于之前的LoginScreen->MainScreen页面跳转,导航栏的场景在此基础上增加了复杂度。如上图,不仅要完成Screen之间的跳转,同时还要在底部导航栏里选中第一个Tab。
这时就要引入Navigation组件中的第三员大将——Navigation Graph导航图。
Navigation Graph 导航图
Navigation Graph(以下简称NavGraph)用于维护Screen数据。用“余杭地图”来做比喻——
- 整个余杭区是NavHost
- 余杭区的地铁、公交、步行线路是NavGraph
- 建筑物的具体地址(如XX路XX号)是Route
正如余杭区也按层级细分为仓前板块-高教路-...等,NavGraph也具备层级关系。
嵌套Navigation Graph
针对底部Tabs,设计一个父类。
// AppRouter.kt
// Add another sealed class
sealed class TopLevelDestination(
val route: String,
val title: Int? = null,
val selectedIcon: ImageVector? = null,
val unselectedIcon: ImageVector? = null,
val navArguments: List<NamedNavArgument> = emptyList()
)

将业务逻辑拆分为登录、主页两个模块,如图所示。在代码上也进行相应设计,结构更加清晰明确。
- 登录(Auth)链条:包含Login、Register。
- 主页(Main)链条:包含Bottom Navigation Destinations和其它Screen。
// AppRouter.kt
private object Routes {
// First Graph Route
const val AUTH = "auth"
const val LOGIN = "login"
const val REGISTER = "signup"
// Second Graph Route
const val MAIN = "main"
const val HOME = "home"
const val PROFILE = "profile"
const val NOTIFICATION = "notification"
const val PRODUCT_DETAIL = "productDetail/{${ArgParams.PRODUCT_ID}}"
}
// grouping AppScreen
sealed class AppScreen(val route: String) {
object Auth : AppScreen(Routes.AUTH) {
object Login : AppScreen(Routes.LOGIN)
object Register : AppScreen(Routes.REGISTER)
}
object Main : TopLevelDestination(Routes.MAIN) {
object Home : TopLevelDestination(
route = Routes.HOME,
title = R.string.home,
selectedIcon = AppIcons.HomeFilled,
unselectedIcon = AppIcons.HomeOutlined,
)
object Profile : TopLevelDestination(
route = Routes.PROFILE,
title = R.string.profile,
selectedIcon = AppIcons.ProfileFilled,
unselectedIcon = AppIcons.ProfileOutlined,
)
object Notification : TopLevelDestination(
route = Routes.NOTIFICATION,
title = R.string.notification,
selectedIcon = AppIcons.NotificationFilled,
unselectedIcon = AppIcons.NotificationOutlined,
)
}
}
1. Auth Navigation Graph
Auth NavGraph包含注册、登录。
// AuthNavGraph.kt
fun NavGraphBuilder.authNavGraph(
navController: NavHostController
) {
navigation(
route = AppScreen.Auth.route
startDestination = AppScreen.Auth.Login.route,
) {
composable( // 注册到NavGraph里
route = AppScreen.Auth.Login.route
) {
// route to main navigation graph
LoginScreen(
navigateToHome = {
navController.navigate(AppScreen.Main.route) {
popUpTo(AppScreen.Auth.route) {
inclusive = true
}
}
},
navigateToSignUp = {
navController.navigate(AppScreen.Auth.Register.route)
},
)
}
composable(
route = AppScreen.Auth.Register.route
) {
SignUpScreen(onNavigateBack = {
navController.navigateUp()
})
}
}
}
2. Main Navigation Graph
包含多个Tab,默认展示HomeTab。
// MainNavGraph.kt
fun NavGraphBuilder.mainNavGraph(
navController: NavHostController
) {
navigation(
startDestination = AppScreen.Main.Home.route,
route = AppScreen.Main.route
) {
composable(
route = AppScreen.Main.Home.route
) {
HomeScreen(onProductClick = {
val route = AppScreen.Main.ProductDetail.createRoute(productId = it)
navController.navigate(route)
})
}
composable(
route = AppScreen.Main.Notification.route,
) {
NotificationScreen()
}
// route back to auth graph
composable(
route = AppScreen.Main.Profile.route
) {
ProfileScreen(navigateToLogin = {
navController.navigate(AppScreen.Auth.route) {
popUpTo(AppScreen.Main.route) {
inclusive = true
}
}
})
}
dialog(
route = AppScreen.Main.ProductDetail.route
) {
// val productId = backStackEntry.arguments?.getString("productId")
// value also can be retrieve directly from responsible view-model
ProductDetail() {
navController.navigateUp()
}
}
}
}
然后再根NavGraph中注册上述2个NavGraph,同样要声明startDestination是登录Screen。
@Composable
fun RootNavGraph(navHostController: NavHostController) {
RootNavHost(
navController = navHostController,
startDestination = AppScreen.Auth.route
) {
authNavGraph(navHostController)
mainNavGraph(navHostController)
}
}
3. 增加底部导航
@Composable
fun BottomBar(
navController: NavHostController,
) {
val navigationScreen = listOf(
AppScreen.Main.Home, AppScreen.Main.Notification, AppScreen.Main.Profile
)
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
navigationScreen.forEach { item ->
NavigationBarItem(
selected = currentRoute == item.route,
label = {
Text(text = stringResource(id = item.title!!), style = MaterialTheme.typography.displaySmall)
},
icon = {
BadgedBox(badge = { }) { }
Icon(
imageVector = (if (item.route == currentRoute) item.selectedIcon else item.unselectedIcon)!!,
contentDescription = stringResource(id = item.title!!)
)
},
onClick = {
navController.navigate(item.route) { // 一级页面之间跳转,不入栈
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
)
}
}
}
修改MainActivity,支持底部Tab。
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val TAG: String = AppLog.tagFor(this.javaClass)
private lateinit var navController: NavHostController
private val mainViewModel: MainViewModel by viewModels()
private var isAuthenticated = false
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
setContent {
FireflyComposeTheme {
navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
val bottomBarState = rememberSaveable { (mutableStateOf(true)) }
val topBarState = rememberSaveable { (mutableStateOf(true)) }
// Control TopBar and BottomBar
when (navBackStackEntry?.destination?.route) {
AppScreen.Main.Home.route -> {
bottomBarState.value = true
topBarState.value = true
}
AppScreen.Main.Profile.route -> {
bottomBarState.value = true
topBarState.value = true
}
AppScreen.Main.Notification.route -> {
bottomBarState.value = true
topBarState.value = true
}
else -> {
bottomBarState.value = false
topBarState.value = false
}
}
Scaffold( // 脚手架函数设置界面显示
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
bottomBar = {
if (bottomBarState.value) {
BottomBar(navController = navController)
}
}) { paddingValues ->
Box(
modifier = Modifier.padding(paddingValues)
) {
RootNavHost(navHostController = navController)
}
}
}
}
}
}
导航过程中的条件分支
在执行导航时,存在根据当前用户的不同状态,执行不同分支的场景。例如进程启动时,如果是首次使用,则跳转到登录页面;如果用户近期登录过,且token仍然处于有效期,则直接进入MainScreen。通过startDestination可以实现上述需求。
@Composable
fun RootNavGraph(isLoggedIn: Boolean, navHostController: NavHostController) {
NavHost(
navController = navHostController,
startDestination = if(isLoggedIn) AppScreen.Main.route else AppScreen.Auth.route
) {
AppLog.showLog("Nav Graph Setup")
authNavGraph(navHostController)
mainNavGraph(navHostController)
}
}
类型安全导航
可以使用单例对象来作为路由,代替String的使用。
@Serializable
object Login
@Serializable
data class Home(val userId: Int)
@Composable
fun RootNavHost() {
val navController = rememberNavController()
NavHost(navController, startDestination = Login) {
composable<Login> {
LoginScreen(
onLoginClick = { navController.navigate(Home(it.userId)) },
)
}
composable<Home> { backStackEntry ->
val home = backStackEntry.toRoute<Home>()
HomeScreen(
bookId = home.userId,
)
}
}
}