在Jetpack Compose逐渐进入人们视野的同时,Google同时推出了与之配套使用的路由框架,旨在提供一种更便捷、更安全的路由方式。
这篇文章将基于NowInAndroid项目,介绍其中对Navigation组件的使用。首先看一下之前介绍过的,NowInAndroid项目的组件化设计:
首先从navigation包开始,具体来说是NiaNavHost.kt
文件。根据Navigation框架的设计,NavHost文件 在Compose层次结构中提供独立导航 。如下所示,NiaNavHost
接收5个参数:
kotlin
@Composable
fun NiaNavHost(
navController: NavHostController,
onNavigateToDestination: (NiaNavigationDestination, String) -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
startDestination: String = ForYouDestination.route
)
- navController: 接收一个
NavHostController
对象,它继承自NavController
,用于与其他的页面进行交互、跳转。 - onNavigateToDestination: 包含一个类型为String的Key,指明接下来要跳转的目标页面,以及一个同样描述跳转目标页面的接口实现
NiaNavigationDestination
,其接口如下:
kotlin
interface NiaNavigationDestination {
/**
* 目标页面属于哪个特定路由
*/
val route: String
/**
* 目标页面的唯一ID
*/
val destination: String
}
- onBackClick: 从目标页面返回时的回调。
- modifier: compose属性,允许在composable中进行微小的变更,例如调整padding、width、定义点击回调等。
- startDestination: navigation将开始的第一个路由。
接下来看具体的函数:
kotlin
@Composable
fun NiaNavHost(
navController: NavHostController,
onNavigateToDestination: (NiaNavigationDestination, String) -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
startDestination: String = ForYouDestination.route
) {
NavHost(
navController = navController,
startDestination = startDestination,
modifier = modifier,
) {
forYouGraph()
bookmarksGraph()
interestsGraph(
navigateToTopic = {
onNavigateToDestination(
TopicDestination, TopicDestination.createNavigationRoute(it)
)
},
navigateToAuthor = {
onNavigateToDestination(
AuthorDestination, AuthorDestination.createNavigationRoute(it)
)
},
nestedGraphs = {
topicGraph(onBackClick)
authorGraph(onBackClick)
}
)
}
}
传入NavController、startDestination和modifier几个参数给父类的构造函数NavHost------它还有第四个继承自NavGraphBuilder
的lambda参数,叫做builder
kotlin
builder: NavGraphBuilder.() -> Unit
这个参数内部调用了forYouGraph
函数,它定义在feature包中的ForYou
模块,这也是我们将feature包作为被依赖模块的原因。
kotlin
object ForYouDestination : NiaNavigationDestination {
override val route = "for_you_route"
override val destination = "for_you_destination"
}
fun NavGraphBuilder.forYouGraph() {
composable(route = ForYouDestination.route) {
ForYouRoute()
}
}
上面是该函数的实现,它是NavGraphBuilder的扩展函数,内部调用composeable
函数,传入参数为route
,参数值是for_you_route
。
ForYouRoute()
函数定义在响应的feature包里,它将screen与ViewModel关联起来。
kotlin
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
fun ForYouRoute(
modifier: Modifier = Modifier,
viewModel: ForYouViewModel = hiltViewModel()
) {
val interestsSelectionState by viewModel.interestsSelectionState.collectAsStateWithLifecycle()
val feedState by viewModel.feedState.collectAsStateWithLifecycle()
ForYouScreen(
interestsSelectionState = interestsSelectionState,
feedState = feedState,
onTopicCheckedChanged = viewModel::updateTopicSelection,
onAuthorCheckedChanged = viewModel::updateAuthorSelection,
saveFollowedTopics = viewModel::saveFollowedInterests,
onNewsResourcesCheckedChanged = viewModel::updateNewsResourceSaved,
modifier = modifier
)
}
bookmarksGraph
同理,而interestsGraph
稍有不同,它包含一个嵌套的nav graph。
kotlin
object InterestsDestination : NiaNavigationDestination {
override val route = "interests_route"
override val destination = "interests_destination"
}
fun NavGraphBuilder.interestsGraph(
navigateToTopic: (String) -> Unit,
navigateToAuthor: (String) -> Unit,
nestedGraphs: NavGraphBuilder.() -> Unit
) {
navigation(
route = InterestsDestination.route,
startDestination = InterestsDestination.destination
) {
composable(route = InterestsDestination.destination) {
InterestsRoute(
navigateToTopic = navigateToTopic,
navigateToAuthor = navigateToAuthor,
)
}
nestedGraphs()
}
}
可以看到除了参数interests_route
和interests_destination
外,navigation()函数还包含一个composable参数,用来将Screen和ViewModel关联起来。区别在于它嵌套了navigationToTopic
函数和navigationToAuthor
函数,它们用来指向内部的两个NavGraph。
kotlin
[...]
interestsGraph(
navigateToTopic = {
onNavigateToDestination(
TopicDestination, TopicDestination.createNavigationRoute(it)
)
},
navigateToAuthor = {
onNavigateToDestination(
AuthorDestination, AuthorDestination.createNavigationRoute(it)
)
},
)
[...]
相似地,这两个函数接收NiaNavigationDestination、route这两个参数,其实现位于topic
模块的navigation
包。
kotlin
object TopicDestination : NiaNavigationDestination {
const val topicIdArg = "topicId"
override val route = "topic_route/{$topicIdArg}"
override val destination = "topic_destination"
/**
* 根据topicId创建Route
*/
fun createNavigationRoute(topicIdArg: String): String {
val encodedId = Uri.encode(topicIdArg)
return "topic_route/$encodedId"
}
/**
* 从NavBackStackEntry中读取topicId
*/
fun fromNavArgs(entry: NavBackStackEntry): String {
val encodedId = entry.arguments?.getString(topicIdArg)!!
return Uri.decode(encodedId)
}
}
在基本的route和destination基础上,这个函数还接收了自定义的topicId
参数。
整个控制流如下:
- 展示Topic页面
- 用户点击一个Topic
- Route框架调用navigateToTopic函数,传入topicId作为参数,完成跳转
在嵌套的NavGraph中,topicGraph接收topicIdArg作为String类型的参数:
kotlin
fun NavGraphBuilder.topicGraph(
onBackClick: () -> Unit
) {
composable(
route = TopicDestination.route,
arguments = listOf(
navArgument(TopicDestination.topicIdArg) { type = NavType.StringType }
)
) {
TopicRoute(onBackClick = onBackClick)
}
}
回到最初的原点,NiaNavHost
是何时创建(调用)的?------在NiaApp的脚手架里面,定义了页面(Screen)的结构,例如NiaBottomBar。
kotlin
@OptIn(
ExperimentalMaterial3Api::class,
ExperimentalLayoutApi::class,
ExperimentalComposeUiApi::class
)
@Composable
fun NiaApp(
windowSizeClass: WindowSizeClass,
appState: NiaAppState = rememberNiaAppState(windowSizeClass)
) {
NiaTheme {
NiaBackground {
Scaffold(
modifier = Modifier.semantics {
testTagsAsResourceId = true
},
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onBackground,
bottomBar = {
if (appState.shouldShowBottomBar) {
NiaBottomBar(
destinations = appState.topLevelDestinations,
onNavigateToDestination = appState::navigate,
currentDestination = appState.currentDestination
)
}
}
) { padding ->
Row(
Modifier
.fillMaxSize()
.windowInsetsPadding(
WindowInsets.safeDrawing.only(
WindowInsetsSides.Horizontal
)
)
) {
if (appState.shouldShowNavRail) {
NiaNavRail(
destinations = appState.topLevelDestinations,
onNavigateToDestination = appState::navigate,
currentDestination = appState.currentDestination,
modifier = Modifier.safeDrawingPadding()
)
}
NiaNavHost(
navController = appState.navController,
onBackClick = appState::onBackClick,
onNavigateToDestination = appState::navigate,
modifier = Modifier
.padding(padding)
.consumedWindowInsets(padding)
)
}
}
}
}
}
其中
NavRail
是用来在平板、电脑上面展示的导航栏,如下图:
最后是NiaNavHost,它从appState中接收参数。后者用于控制APP的状态,由NavHostController、WindowSizeClass两个参数构建。WindowSizeClass声明了窗口的宽高,以便于在不同尺寸的屏幕上创建自适应布局。
kotlin
@Stable
class NiaAppState(
val navController: NavHostController,
val windowSizeClass: WindowSizeClass
) {
[...]
}
NiaNavHost类在构建时,包含4个参数:
1、当前的destination:从navController中获取当前参数。
kotlin
val currentDestination: NavDestination?
@Composable get() = navController
.currentBackStackEntryAsState().value?.destination
2、ShouldShowBottomBar:适用于更紧凑的设备,符合响应式UI规范:
kotlin
val shouldShowBottomBar: Boolean
get () = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact ||
windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact
3、ShouldShowNavRail:用于适配Pad、电脑显示器等大型的设备,是上一个布尔参数的相反之:
kotlin
val shouldShowNavRail: Boolean
get() = !shouldShowBottomBar
4、底部导航所使用的topLevelDestinations列表:
kotlin
val topLevelDestinations: List<TopLevelDestination> = listOf(
TopLevelDestination(
route = ForYouDestination.route,
destination = ForYouDestination.destination,
selectedIcon = DrawableResourceIcon(NiaIcons.Upcoming),
unselectedIcon = DrawableResourceIcon(NiaIcons.UpcomingBorder),
iconTextId = forYouR.string.for_you
),
TopLevelDestination(
route = BookmarksDestination.route,
destination = BookmarksDestination.destination,
selectedIcon = DrawableResourceIcon(NiaIcons.Bookmarks),
unselectedIcon = DrawableResourceIcon(NiaIcons.BookmarksBorder),
iconTextId = bookmarksR.string.saved
),
TopLevelDestination(
route = InterestsDestination.route,
destination = InterestsDestination.destination,
selectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
unselectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
iconTextId = interestsR.string.interests
)
)
NiaAppState
类内部有2个成员函数,功能可见其注释:
kotlin
/**
* 管理页面之间跳转,区别一级、二级destination对象。
* 一级对象:先出栈后入栈,避免用户在不同Tab之间切换时不必要的入栈。
* 二级(其它)对象:直接入栈,用来在Back事件时返回上一级页面。
* @param destination:[NiaNavigationDestination] 类型,说明当前的Destination。
* @param route: 可选的用于补充Destination的所需参数。
*/
fun navigate(destination: NiaNavigationDestination, route: String? = null) {
trace("Navigation: $destination") {
if (destination is TopLevelDestination) {
navController.navigate(route ?: destination.route) {
// 出栈
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// 避免产生多个一级页面对象(Tab)
launchSingleTop = true
// 恢复上一次选中该Tab时的状态
restoreState = true
}
} else {
navController.navigate(route ?: destination.route)
}
}
}
kotlin
fun onBackClick() {
navController.popBackStack()
}
在NiaApp
函数里,使用rememberNiaAppState
来初始化APP状态,它包装了navController和windowSizeClass所产生的状态变更。
注意:NavigationTrackingSideEffect存储有关导航事件的信息,以便与JankStats 一起使用, JankStats 是一个帮助分析应用程序性能问题的库。
kotlin
@Composable
fun rememberNiaAppState(
windowSizeClass: WindowSizeClass,
navController: NavHostController = rememberNavController()
): NiaAppState {
NavigationTrackingSideEffect(navController)
return remember(navController, windowSizeClass) {
NiaAppState(navController, windowSizeClass)
}
}