• Compose Navigation用于Android多module项目最佳实践


    Compose Navigation用于Android多module项目最佳实践

    在本文中,我们将采取同一个项目并扩展它以实现最佳实践。该项目具有文章、设置和关于屏幕的抽屉导航。项目的输出如下所示:

    当你有一个多屏幕的项目时,每个屏幕至少必须有自己单独的模块。在我们的例子中,我为每个屏幕创建了三个单独的模块,并在应用程序模块中使用它们来实现基本的导航。

    1. 准备工作

    1.1 起始项目

    我在Github链接上创建了一个起始项目,你可以从仓库中下载代码并从starter文件夹获取项目。Github repo还包含final文件夹,这是实现Navigation Compose最佳实践后的最终项目,本文将逐步介绍如何实现。

    1.2 项目结构

    为了对起始项目模块结构有一个概述:它每个屏幕有一个单独的模块,并将所有这些模块添加到app模块的依赖项中。下面的图表说明了它。

    feature_articles是用于显示文章列表的模块,feature_article是用于显示关于单篇文章的详细信息的模块。

    图表中的箭头表示依赖关系的使用,这意味着app模块添加了对feature_settings、feature_articles、feature_about和feature_article模块的依赖项,以实现导航。

    1.3 基本的Compose导航

    让我们看一下起始项目中Compose导航的实现。

    //BasicComposeNavigation.kt
    @Composable
    fun MainNavigation(
        navController: NavHostController = rememberNavController(),
        coroutineScope: CoroutineScope = rememberCoroutineScope(),
        drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
    ) {
        ModalNavigationDrawer(
            drawerState = drawerState,
            drawerContent = {
                ModalDrawerSheet {
                    DrawerContent(menus) { route ->
                        coroutineScope.launch {
                            drawerState.close()
                        }
    
                        navController.navigate(route)
                    }
                }
            }
        ) {
            NavHost(navController = navController, startDestination = MainRoute.Articles.name) {
                composable(MainRoute.Articles.name) {
                    val viewModel: ArticlesViewModel = hiltViewModel()
                    ArticlesScreen(drawerState, viewModel) {
                        navController.navigate("article")
                    }
                }
                composable(MainRoute.About.name) {
                    val viewModel: AboutViewModel = hiltViewModel()
                    AboutScreen(drawerState, viewModel)
                }
                composable(MainRoute.Settings.name) {
                    val viewModel: SettingsViewModel = hiltViewModel()
                    SettingsScreen(drawerState, viewModel)
                }
                composable(MainRoute.Article.name) {
                    val viewModel: ArticleViewModel = hiltViewModel()
                    ArticleScreen(
                        viewModel = viewModel,
                        onBackNavigation = {
                            navController.navigateUp()
                        }
                    )
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    为了实现上述所示的导航,我们必须从各个屏幕模块中公开每个屏幕的Composable和ViewModel,以便在app模块中使用它们。例如,对于feature_articles模块,ArticlesScreenArticlesViewModel必须对app模块可访问,以构建上述的NavGraph,其他模块也是类似。

    2. 最佳实践

    我们将逐步详细说明并使用上述起始项目作为基线来实现最佳实践。

    2.1 屏幕Composables接收状态和事件传入。

    每个屏幕的Composable必须接收一个状态对象和事件作为参数。事件可以在模块内部的屏幕Composable中进行处理,而那些无法在屏幕内部处理的事件必须在屏幕Composable之外传递给外部/更高级的NavGraph来处理。

    下面是ArticleScreen的一个示例。

    //ArticleScreen.kt
    fun ArticleScreen(
        viewModel: ArticleViewModel,
        onNavigateBack: () -> Unit,
        ) {
      // code 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • viewModel包含了UI的状态,推荐使用一个独立的状态类作为屏幕Composable的参数,但这不是本篇文章的重点。
    • onNavigationBack是从ArticleScreen中省略的事件,将由更高级的NavGraph处理。

    2.1 按屏幕拆分导航图

    为每个屏幕创建一个导航图。我们可以通过在NavGraphBuilder上创建一个扩展方法来实现。

    要在我们的屏幕模块中使用NavGraphBuilder,我们需要添加以下Gradle依赖项。

    implementation("androidx.navigation:navigation-compose:2.5.3")
    
    • 1

    让我们以feature_article模块为例,在feature_article模块中创建ArticleNavigation.kt文件,该文件在NavGraphBuilder上添加了一个扩展方法,如下所示。

    //ArticleNavigation.kt
    
    private const val articleIdArg = "articleId"
    
    fun NavGraphBuilder.articleScreen(onNavigateBack: () -> Unit) {
        composable("article/{$articleIdArg}") {
            val viewModel: ArticleViewModel = hiltViewModel()
            ArticleScreen(
                viewModel = viewModel,
                onNavigateBack = onNavigateBack
            )
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    需要注意以下几点:

    • ArticleNavigation.kt文件和扩展方法将导航逻辑与屏幕逻辑分离。
    • 它封装了与导航相关的代码,不需要将其暴露给其他部分的代码。
    • ViewModel实例和UI状态实例将在此扩展方法中创建。
    • 无法在ViewModel内部处理的事件将传递给NavGraphBuilder的上层,例如在这种情况下的onNavigateBack事件。
    • NavGraphBuilder上的扩展方法将在模块之外暴露,并且不再需要将ArticleViewModelArticleScreen暴露给模块之外。
    • 我们必须为ArticleScreenArticleViewModel指定internal访问修饰符,因为它们不再需要在模块之外被访问。

    2.2 为每个屏幕目标提供扩展方法

    每个屏幕都必须暴露NavController扩展方法,以便其他目标可以安全地导航到它。
    为传递的参数提供类型安全性。
    封装特定于导航的代码。
    ArticleNavigation.kt文件中为ArticleScreen创建NavController扩展方法。

    // ArticleNavigation.kt
    
    fun NavController.navigateToArticle(articleId: String) {
        this.navigate("article/$articleId")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    上述扩展方法navigateToArticleArticleNavigation.kt文件中,该文件封装了如何指定导航路线和所需参数的方式。

    2.3 创建类型安全的参数包装器

    为了确保从ViewModel内的SavedStateInstance正确提取参数类型,我们应该创建一个参数包装器。

    以下是用于ArticleScreenarticleId的包装器ArticleArgs

    // ArticleNavigation.kt
    //TypeSafeArgs.kt
    private const val articleIdArg = "articleId"
    
    internal class ArticleArgs(articleId: String) {
        constructor(savedStateHandle: SavedStateHandle) :
                this(checkNotNull(savedStateHandle[articleIdArg]) as String)
    }
    
    // ArticleViewModel.kt
    
    @HiltViewModel
    internal class ArticleViewModel @Inject constructor(
        savedStateHandle: SavedStateHandle
    ): ViewModel() {
        val articleArgs = ArticleArgs(savedStateHandle)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    使用internal访问修饰符来确保ArticleArgsArticleViewModel不会被模块之外的代码访问。

    Navigation Compose无法提供编译时类型安全的代码,但通过这样做,我们可以确保运行时类型安全的代码。

    2.4 仅暴露所需的公共API

    如前所述和实施,我们必须确保只有所需的API被模块之外的代码访问,此处可以使用internal访问修饰符。

    请确保为屏幕组件、viewModel和Args(例如ArticleViewModelArticlesScreenArticleArgs)提供internal修饰符。
    NavGraphBuilderNavController上的扩展方法只能在模块之外暴露。
    只将第一个目标路由暴露给功能模块之外,以便NavHost可以指定起始目标。
    为想要导航到的每个目标在NavController上添加扩展方法。
    以下图示概述了每个模块,显示了模块之外暴露的内容以及模块内部的内容。

    2.5 模块结构指导Graph结构

    使您的模块结构指导Graph结构。

    我们首先需要创建一个主页模块,它将封装抽屉式导航逻辑,例如在我们的情况下,ArticlesSettingsAbout屏幕显示在抽屉中,因此让我们创建一个feature_home模块,其中包含此类导航逻辑和屏幕/模块依赖项。

    创建feature_home模块后,项目结构应如下图所示。

    feature_home模块使用feature_settingsfeature_articlesfeature_about模块,并封装了抽屉式导航逻辑,仅公开模块外所需的导航API。

    app模块使用feature_homefeature_article模块,从feature_articles模块导航到feature_article模块,因此feature_home将特定的事件传递回app模块,最终将用户导航到特定的文章。

    以下是feature_home模块中HomeScreen代码的示例。

    //HomeScreen.kt 
    @Composable
    internal fun HomeScreen(
        navController: NavHostController = rememberNavController(),
        coroutineScope: CoroutineScope = rememberCoroutineScope(),
        drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Closed),
        onNavigateToArticle: () -> Unit
    ) {
        ModalNavigationDrawer(
            drawerState = drawerState,
            drawerContent = {
                ModalDrawerSheet {
                    DrawerContent(menus) { route ->
                        coroutineScope.launch {
                            drawerState.close()
                        }
    
                        navController.navigate(route)
                    }
                }
            }
        ) {
            NavHost(navController = navController, startDestination = articlesRoute) {
    
                articlesScreen(drawerState, onNavigateToArticle)
    
                settingsScreen(drawerState)
    
                aboutScreen(drawerState)
    
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    HomeScreen被指定为internal,因为我们不需要将其暴露给模块之外;相反,我们将为NavGraphBuilder创建一个扩展方法。

    //HomeNavigation.kt
    const val homeRoute = "home"
    
    fun NavGraphBuilder.homeScreen(
        onNavigateToArticle: () -> Unit
    ) {
        composable(homeRoute) {
            HomeScreen(
                onNavigateToArticle = onNavigateToArticle
            )
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    现在看看app模块项目中使用homeScreen扩展方法的MainNavigation代码。

    //MainNavigation.kt
    @Composable
    fun MainNavigation(
        navController: NavHostController = rememberNavController(),
    ) {
        NavHost(navController = navController, startDestination = homeRoute) {
    
            homeScreen {
                navController.navigateToArticle("fakeArticleId")
            }
    
            articleScreen {
                navController.navigateUp()
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    结论

    • 每个屏幕目标必须提供一个带有state/viewModelevents参数的可组合项。
    • 每个目标模块必须在NavGraphBuilder上提供一个扩展方法,将导航逻辑与代码逻辑分离开来。
    • NavGraphBuilder上的扩展方法应解析状态并传递该特定屏幕的事件。
    • 每个目标屏幕还必须在NavController上提供一个扩展方法,指定如何导航到该特定目标,封装导航特定的代码。
    • 使用args包装器确保传递给目标的参数类型正确,确保运行时类型安全。
    • 在模块内部,仅公开所需的API,将viewModel和屏幕可组合项保持为internal

    Github

    https://github.com/saqib-github-commits/ComposeNavigationBestPractices

  • 相关阅读:
    i7 13700k怎么样 i7 13700k核显相当于什么显卡
    [Java反序列化]—C3P0反序列化
    基于Yolov8的工业小目标缺陷检测(3):多检测头提升小目标检测精度
    针对 DNS 监控的 Grafana Dashboard面板DeepFlow
    面试遇到并发编程,爽还是酸爽?
    Mybatis学习笔记8 查询返回专题
    【Three.js】知识梳理二十二:相机视角的平滑过渡与点击模型视角切换
    matlab新手快速上手5(蚁群算法)
    IIC基础知识
    删除数据库表中重复数据的方法
  • 原文地址:https://blog.csdn.net/u011897062/article/details/133695560