【Android架构底层逻辑拆解】Google官方项目NowInAndroid研究(6)View层的设计和实现之Navigation路由

在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_routeinterests_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参数。

整个控制流如下:

  1. 展示Topic页面
  2. 用户点击一个Topic
  3. 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)
    }
}

参考资料

相关推荐
小天努力学java21 分钟前
【软考-架构】11.3、设计模式-新
设计模式·架构
jason_yang1 小时前
Clean Code与代码重构
设计模式·架构·代码规范
雷渊5 小时前
java版本管理工具-jenv
后端·架构
我爱鸿蒙开发5 小时前
一文带你深入了解Stage模型
前端·架构·harmonyos
一个处女座的程序猿O(∩_∩)O5 小时前
DeepSeek与人工智能:技术演进、架构解析与未来展望
人工智能·架构
JinSo5 小时前
国际化探索:提升开发体验与灵活性
前端·javascript·架构
奔袭的算法工程师5 小时前
TI的Doppler-Azimuth架构(TI文档)
人工智能·深度学习·目标检测·架构·自动驾驶
LTPP6 小时前
Hyperlane:基于Rust的高性能Web框架,QPS突破32万!🚀
后端·面试·架构
非晓为骁6 小时前
【Agent】OpenManus-Agent-Memory详细设计
ai·架构·agent·agi·manus·openmanus