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

news2024/10/3 0:27:09

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()
                    }
                )
            }
        }
    }
}

为了实现上述所示的导航,我们必须从各个屏幕模块中公开每个屏幕的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 
}
  • viewModel包含了UI的状态,推荐使用一个独立的状态类作为屏幕Composable的参数,但这不是本篇文章的重点。
  • onNavigationBack是从ArticleScreen中省略的事件,将由更高级的NavGraph处理。

2.1 按屏幕拆分导航图

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

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

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

让我们以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
        )
    }
}

需要注意以下几点:

  • 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")
}

上述扩展方法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)
}

使用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)

        }
    }
}

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

//HomeNavigation.kt
const val homeRoute = "home"

fun NavGraphBuilder.homeScreen(
    onNavigateToArticle: () -> Unit
) {
    composable(homeRoute) {
        HomeScreen(
            onNavigateToArticle = onNavigateToArticle
        )
    }
}

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

//MainNavigation.kt
@Composable
fun MainNavigation(
    navController: NavHostController = rememberNavController(),
) {
    NavHost(navController = navController, startDestination = homeRoute) {

        homeScreen {
            navController.navigateToArticle("fakeArticleId")
        }

        articleScreen {
            navController.navigateUp()
        }
    }
}

结论

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

Github

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1071967.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何查看电脑IP历史记录:几种电脑操作系统的实用指南

在日常使用电脑时,了解电脑的IP历史记录对于网络故障排查、网络管理和安全监控非常重要。无论您是使用Windows、Mac还是Linux操作系统,本文将为您详细介绍如何查看电脑IP历史记录,帮助您更好地管理和维护您的网络环境。 第一部分:…

C语言分支与循环语句

这篇文章简单总结一下C语言的分支和循环,分支结构可以用if、switch语句实现,循环语句可以用while、for、以及do-while语句实现,重点解说这四种语句。 一、C语言分支 1.if语句 if语句的基本结构为: if (条件表达式)执行语句&am…

vue3 配置生产和开发 非vite

首先在根目录创建.env.development和.env.production文件env文件随意 development文件配置 production文件配置 注意:有些需要配置名称是有要求的! 通过process.env就能拿到我们的信息了,然后在pack配置启动

C++设计模式_07_Bridge 桥模式

文章目录 1. 动机(Motivation)2. 代码演示Bridge 桥模式2.1 基于继承的常规思维处理2.2 基于组合关系的重构优化2.3 采用Bridge 桥模式的实现 3. 模式定义4. 结构(Structure)5. 要点总结 与上篇介绍的Decorator 装饰模式一样&…

Go语言进阶------>init()函数

Init()包初始化 执行优先级 Init()函数的执行优先级比main()函数的执行优先级要高,也就是说程序会优先执行Init()函数之后再执行main()函数. 代码如下 package mainimport "fmt"func init() {fmt.Println("执行了Init()函数") }func main() {fmt.Println…

【OCR】合同上批量贴印章

一、需求 OCR算法在处理合同等文件时,会由于印章等遮挡导致文本误识别。因此在OCR预处理时,有一个很重要的步骤是“去除印章”。其中本文主要聚焦在“去除印章”任务中的数据构建步骤:“合同伪印章”的数据构建。下面直接放几张批量合成后效果…

存档&改造【04】二维码操作入口设置细节自动刷新设置后的交互式网格

因为数据库中没有数据无法查看设置效果,于是自己创建了个测试数据表,用来给demo测试 -- 二维码操作入口设置 create table JM_QR_CODE(QR_CODE_ID NUMBER generated as identity primary key,SYSTEM_ID NUMBER(20) not null,IS_ENAB…

PPT课件培训视频生成系统实现全自动化

前言 困扰全动自化的重要环节,AI语音合成功能,终于可以实现自动化流程,在此要感谢团队不懈的努力和韧性的精神! 实现原理 请参照我的文章《Craneoffice云PPT课件培训视频生成系统》 基本流程 演示视频 PPT全自动 总结 过去实…

解读大模型(LLM)的token

当人们谈论大型语言模型的大小时,参数会让我们了解神经网络的结构有多复杂,而token的大小会让我们知道有多少数据用于训练参数。 正像陆奇博士所说的那样,大型语言模型为从文本生成到问题回答的各种任务提供了令人印象深刻的能力,…

css 动态点击效果

效果图 daima <body><div></div> </body> <style>div {background-color: rgb(220, 74, 98);width: 160px;height: 60px;border-radius: 40px;transition: all 0.2s linear;}div:active {transform: scale3d(0.92, 0.92, 1);} </style>

Apache RocketMQ远程代码执行

漏洞概述 Apache RocketMQ是一个分布式消息中间件&#xff0c;专为万亿级超大规模的消息处理而设计&#xff0c;具有高吞吐量、低延迟、海量堆积、顺序收发等特点。RocketMQ 5.1.0及以下版本&#xff0c;在一定条件下&#xff0c;存在远程命令执行风险。RocketMQ的NameServer&…

如何使用 CloudQuery 对 100+ 套数据库进行安全管控?

业务挑战 A 集团信息部门发展将近 10 年&#xff0c;从最初的一个 MySQL 数据库到现在鱼龙混杂什么数据库都有&#xff0c;其中仓库部门用了 PostgreSQL 做数据底座&#xff0c;历史原因问题&#xff0c;生产系统却是使用 Oracle。基于信创的需求&#xff0c;上级领导要求试点…

CAMERALINK通信应用

简述&#xff1a; Cameralink是一个全面的视频接口&#xff0c;他可以满足以前所有的传输视频的功能&#xff0c;包括通信、配置、应答、同步、以及复位等等&#xff0c;在以前简单提过一下&#xff0c;但是没有深入研究&#xff0c;其实这个通信还是比较简单的&#xff0c;在这…

uniapp vue3 静态图片引入

方法一 从新定义路径 一定看好你图片的路径 代码 <template><div class"main">Main<img :src"getImg()" alt""></div> </template><!-- 方式一 // <script setup> // let imgName logo.png // cons…

材质、纹理、贴图的区别和关联

1、材质、纹理、贴图的概念 材质&#xff08;Material&#xff09;、纹理&#xff08;Texture&#xff09;、贴图&#xff08;Texture Map&#xff09;是计算机图形学中的三个概念&#xff0c;它们之间存在关系但也有一些区别。 材质&#xff08;Material&#xff09;是描述物…

计算机竞赛 题目:基于深度学习的中文汉字识别 - 深度学习 卷积神经网络 机器视觉 OCR

文章目录 0 简介1 数据集合2 网络构建3 模型训练4 模型性能评估5 文字预测6 最后 0 简介 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 基于深度学习的中文汉字识别 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff01; &a…

爬虫破解:解决CSRF-Token反爬问题 - 上海市发展和改革委员会

标题:爬虫破解:解决CSRF-Token反爬问题 - 上海市发展和改革委员会 网址:https://fgw.sh.gov.cn/fgw-interaction-front/biz/projectApproval/home MD5加密:ca7f5c978b1809d15a4b228198814253 需求文档 采集数据如下所示: 解决反爬思路 这里只提供解决思路,解决反爬,…

30 数据分析(上)(业务略写可跳)|jupyter|matplotlib

文章目录 数据科学领域概述数据如何驱动运营给企业带来价值岗位关键词说明业务的商业模式 数据指标数据指标定义及常用数据指标如何选取指标分析角度计数流量导向的工具内容导向的工具用户导向的工具业务导向的工具 数据分析方法对比分析多维分析漏斗分析留存分析总结 用户画像…

基于Java的医护人员排班系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

Python教程——配置环境,再探IDE

文章目录 一、Python安装下载安装验证 二、第一个Python程序常见问题 三、Python解释器四、PyCharm工具安装和配置安装使用PyCharm基本使用 一、Python安装 下载 如果我们想要使用Python语言编写程序&#xff0c;我们必须下载Python安装包并配置Python环境&#xff0c;我们现…