Jetpack Compose Navigation 遇上类型安全

news2024/11/15 9:14:52

Jetpack Compose Navigation 遇上类型安全

引言

随着 Navigation 2.8.0-alpha08 版本的发布,Navigation 组件引入了基于 Kotlin Serialization 的完整类型安全系统,用于在使用 Kotlin DSL 时定义导航图。这一新特性旨在与 Navigation Compose 等集成最佳的方案。

什么是 Kotlin DSL?

Navigation 组件包含三个主要组件:

  • Host —— 在您的布局中显示当前“目标”(destination)的 UI 元素。
  • Graph —— 定义应用程序中所有可能的目标的数据结构。
  • Controller —— 管理目标之间导航并保存目标的后向堆栈的中央协调器。

Kotlin DSL 只是构建导航图的一种方式。自 Navigation 于 2018 年首次发布以来,一直提供了三种构建图的方式:

  1. 手动构造 NavGraph 实例并添加目标(例如 Fragment 目标)以构建图。
  2. 从导航 XML 文件中膨胀您的图,并手动编辑它或通过导航编辑器进行编辑。
  3. 使用 Kotlin DSL 直接在 Kotlin 代码中构建导航图。

Navigation Compose 是第一个真正拥抱 Kotlin DSL 作为构建图的方式的集成,有意地转向了更灵活的系统,并远离了静态 XML 文件。

Kotlin 代码,但代价是什么?

然而,从构建时的静态 XML 文件到在运行时生成图的转变意味着开发者可用的工具也发生了显著变化。Navigation 组件的 Safe Args Gradle 插件生成了类型安全的“Directions”类,您可以在代码中使用它们在目标之间进行导航,但它依赖于从那些导航 XML 文件中读取目标及其参数。这意味着如果没有导航 XML 文件,就没有生成的 Safe Args 代码。

虽然 Navigation 在运行时需要正确的类型和参数(如果您尝试向期望 Int 的地方传递 String 或忘记了必需的参数,它会立即崩溃),但编译时安全性留给了开发者自行解决。

编译时类型安全性

如果没有 Safe Args Gradle 插件,您会剩下什么呢?使用 Navigation Compose 的 Kotlin DSL 是基于每个目标都有一个唯一的“路由”这个想法的 —— 一个唯一标识该目标的 RESTful 路径。

例如,就像一个网站一样,您可能会有一个“home”目标,一个“products”目标,以及需要参数的页面 —— 特定产品的路由可能是“products/{productId}” —— 它将包含该产品的唯一 ID 的占位符。

这意味着:

  • 要跟踪这些字符串路由。
  • 管理它们的参数及其类型。
  • 最糟糕的是,执行字符串插值。

我们采取了一些措施来最小化这种繁琐工作 —— 将字符串与我们基础的 Kotlin DSL 上的类型安全扩展分离开来。虽然这提供了基础 Kotlin DSL 与您的代码库和不同模块中公开的 API 之间的强大隔离层,但显然这还不够。

Android 社区为我们提供了一些出色的解决方案,这些解决方案建立在 Navigation Compose 之上,旨在最小化或完全消除这些手动编写的代码,其中包括:

  • Rafael Costa 的 Compose Destinations 使用 KSP 处理附加到组合函数的注解,以生成整个导航图。
  • Kiwi.com 的 navigation-compose-typed 使用 Kotlin Serialization 直接从 @Serializable 对象或数据类生成路由。

如此多的代码生成方式

在研究在我们的 Kotlin DSL 中实现 Safe Args 时,我们探索了多种方法,基本上是研究了可以用来生成类型安全代码的许多技术。

我们探索的一种方法是将我们与 Safe Args Gradle 插件所做的事情进行转译 —— 而不是一个读取源的真理(您的导航 XML 文件)的 Gradle 插件,我们将使用您现有的 Kotlin 代码作为真理的来源。

这意味着如果您编写了一个类似于以下的 Kotlin DSL 片段的代码:

composable(
    route = "profile/{userId}/{name}",
    arguments = listOf(
        navArgument("userId") {
            type = NavType.Int,
            nullable = false
        },
        navArgument("name") {
            type = NavType.String,
            nullable = true
        }
    )
) {

我们将生成您需要导航到“profile”目标并提取这些参数的 ProfileDestination 和 ProfileArgs 类。但是,经过调查后发现,这并不像想象中那么容易。像 “profile/{userId}/{name}” 这样的字符串信息在技术上是可以提取出来的,但只能作为 Kotlin 编译器插件。即使这样,虽然我们可以找到传递给 Kotlin DSL 的路由字符串,但是如果它不是一个常量字符串,则很难解析该字符串。目前这种解决方案已经不再维护。

那么,如果 Kotlin DSL 代码不是一个可行的真实来源,什么是一个可行的真实来源呢?有什么工具可以读取这些信息?

事实证明,另外两个重要选项(KSP 和 Kotlin Serialization)也都是 Kotlin 编译器插件。但值得注意的是:它们是随着每个 Kotlin 版本一起发布的插件,这对于让开发者能够及时使用新版本的 Kotlin 是至关重要的。

为什么选择 Kotlin Serialization

我们在开发 Navigation 组件时遵循的一个指导原则是尽量减少 Navigation 代码的“传染性”:例如,轻松地将我们的库替换为其他库(没有判断!)。如果您的每个文件中都散布着 Navigation 代码和类,您永远无法摆脱它。

这就是为什么我们的测试指南明确建议在屏幕级别的组合方法中不要有任何对 NavController 的引用,也是为什么没有 NavController 组合本地:您层次结构深处的按钮,尽管它可能很方便,但不应与您特定的导航库绑定。

因此,当寻找一个完全独立于 Navigation 类的方式来定义图中的每个目标时,这种方法正是我们正在寻找的。

这意味着,如果您想要在导航图中定义一个新目标,您可以编写最简单的代码:

// 定义一个不带任何参数的 home 目标
@Serializable
object Home

// 定义一个带有 ID 的 profile 目标
@Serializable
data class Profile(val id: String)

您会注意到这些故意不需要实现任何 Navigation 提供的接口,甚至不需要在具有 Navigation 依赖的模块中定义它们。但是,它们足以封装目标的有意义名称(如果您将其命名为 object Object1,您可能会受到一些怀疑)和对该目标的标识至关重要的任何参数。这看起来像是一个可行的真实来源。

借助 Kotlin Serialization 作为可行的编译时安全性来源,我们继续对接收字符串路由的每个 API 添加了基于 Kotlin Serialization 的重载。

示例代码

因此,一旦定义了您的 Home 和 Profile Serializable 类,您的图现在看起来像是这样的:

NavHost(navController, startDestination = Home) {
    composable<Home> {
        HomeScreen(onNavigateToProfile = { id ->
            navController.navigate(Profile(id))
        })
     }
     composable<Profile> { backStackEntry ->
         val profile: Profile = backStackEntry.toRoute()
         ProfileScreen(profile)
     }
}

您应该立即注意到一件事:没有字符串!具体来说:

  • 在定义组合目标时没有路由字符串 —— 指定类型就足以为您生成路由以及参数(不再需要 navArgument)。
  • 导航到新目标时没有路由字符串。您向 NavController 传递与要导航到的目标关联的 Serializable 对象。
  • 在定义导航图的起始目标时没有路由字符串。

对于 Profile 屏幕,我们使用了 toRoute() 扩展方法从 NavBackStackEntry 和其参数中重新创建 Profile 对象。在 SavedStateHandle 上也有类似的扩展方法,因此在不需要引用特定参数键的情况下,从 ViewModel 中轻松获取类型安全的参数也是同样容易的。

这种新方法适用于单个目标,因此您可以逐步从当前方法迁移到这种新方法,逐个目标或逐个模块。

路由 vs 路由模式

我个人最喜欢这种类型安全方法的一个特性是它清楚地表明哪些 API 支持路由模式(例如,“profile/{id}”)以及哪些支持填充的路由(例如,“profile/42”)。例如,popBackStack() API 实际上支持两种,但当它的参数只是一个字符串时这一点并不明显。使用类型安全的 API,情况就清楚得多:

// 弹出到包括 Profile 屏幕在内的堆栈顶部实例
navController.popBackStack<Profile>(inclusive = true)

// 弹出到具有 ID 42 的 Profile 屏幕的确切实例
// 同时弹出任何其他位于其顶部的实例在后向堆栈中的目标
navController.popBackStack(Profile(42), inclusive = true)

因此,当您看到一个接受具体类的 API 时,您会知道它表示该类型的任何目标,而无论其参数如何。而当一个接受该类的实际实例时,它用于查找具有完全匹配参数的特定目标。

诸如getBackStackEntry()或甚至图的 startDestination 这样的 API 就是技术上已经支持两种情况一段时间的例子,您甚至可能都不知道它们!

自定义类型

如果您确实在原始类型之外进行了某些自定义(例如 Parcelable 类型),您甚至可以将其用作 Serializable 类的字段,方法是编写自己的自定义 NavType 并在构建图时传递它:

// Search 屏幕需要更复杂的参数
@Parcelable
data class SearchParameters(
   val searchQuery: String,
   val filters: List<String>
)

@Serializable
data class Search(
   val parameters: SearchParameters
)

val SearchParametersType = object : NavType<SearchParameters>(
    isNullableAllowed = false
) {
    // 请参阅上面链接的自定义 NavType 文档以了解如何实现此操作的示例
}

// 现在在您的目标中使用它
composable<Search>(
 typeMap = mapOf(typeOf<SearchParameters>() to SearchParametersType)
) { backStackEntry ->
    val searchParameters = backStackEntry.toRoute<Search>().parameters
}

注意:这应该是一个路障:认真考虑一下,是否不可变的、快照式的参数真的是这些数据的真实来源,或者是否这应该真的是从一个响应式源(例如从存储库公开的流)中检索的对象,如果您的数据发生更改,它将自动刷新。

放心使用

完整的类型安全 API 从 Navigation 2.8.0-alpha08 版本开始提供。除了支持我们支持的所有 Kotlin DSL 构建器(包括我们在这里讨论的 Navigation Compose 和带有 Fragment 的 Navigation)之外,它还包括其他一些可能会让您感兴趣的 API,例如 navDeepLink API,它接受一个 Serializable 类和一个前缀,可以让您轻松地将外部链接连接到相同的类型安全 API。

如果您发现任何问题或对我们遗漏的 API 有功能请求,请提交问题 —— 当这些 API 仍处于 alpha 阶段时,是请求更改的最佳时机。

结论

Navigation Compose 和类型安全系统的结合代表了 Android 开发中导航的一大飞跃。通过将 Kotlin Serialization 与 Navigation 整合,我们能够实现更轻松的编译时类型安全,减少手动编写代码的工作量,提高了代码的可维护性和健壮性。这一新特性让开发者能够更专注于构建高质量的 Android 应用程序,摆脱了手动管理字符串路由和参数的繁琐工作。随着 Navigation 组件的不断演进,我们可以期待更多功能和改进,以进一步改善 Android 应用程序的开发体验。在您的项目中尝试使用 Navigation Compose 和类型安全系统,体验其中带来的便利和优势,以便更高效地构建出色的 Android 应用程序!

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

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

相关文章

1.Anaconda-创建虚拟环境的手把手教程

文章目录 介绍&#xff08;必看&#xff09;正文版本信息模块安装流程1.创建虚拟环境2.激活环境3.退出虚拟环境4.安装python(激活虚拟环境)5.安装tensorflow(激活虚拟环境)6.安装matplotlib7.protobuf版本太高会有问题(激活虚拟环境) 常用的指令&#xff08;一定会用到&#xf…

【CSAPP导读】导论

目录 &#x1f308; 前言&#x1f308; &#x1f4c1; 书籍介绍 &#x1f4c1; 阅读路线 &#x1f4c1; 总结 &#x1f308; 前言&#x1f308; 《深入理解计算机系统》书籍是由布赖恩特(Bryant,R.E.)著的一本经典计算机科学教材&#xff0c;常被简称为"CSAPP"&a…

找不到msvcr110.dll怎么办,msvcr110.dll丢失的7个不同能修复的解决方法

msvcr110.dll是一个动态链接库&#xff08;DLL&#xff09;文件&#xff0c;由Microsoft Corporation开发&#xff0c;是Visual C Redistributable for Visual Studio 2012的一部分。这个文件包含了运行时库函数&#xff0c;这些函数对于基于Visual C 2012编译的应用程序来说是…

linux笔记8--安装软件

文章目录 1. PMS和软件安装的介绍2. 安装、更新、卸载安装更新ubuntu20.04更新镜像源&#xff1a; 卸载 3. 其他发行版4. 安装第三方软件5. 推荐 1. PMS和软件安装的介绍 PMS(package management system的简称)&#xff1a;包管理系统 作用&#xff1a;方便用户进行软件安装(也…

✔️Vue基础++

✔️Vue基础 组件通信 什么是组件通信&#xff1f; 组件通信就是指 组件与组件 之间的 数据传递 组件的数据是独立的&#xff0c;无法直接访问其他组件的数据想使用其他组件的数据&#xff0c;就需要组件通信 组件之间如何通信&#xff1f; 组件关系 父子关系非父子关系 …

leetcode -- 114.二叉树展开为链表

https://leetcode.cn/problems/flatten-binary-tree-to-linked-list/ 题目中要求把链表展开为单链表&#xff0c;并且展开后的链表要跟二叉树的前序遍历顺序相同。 /*** Definition for a binary tree node.* struct TreeNode {* int val;* struct TreeNode *left;* …

adb shell pm path packageName

在Android命令行中&#xff0c;如果你想要查询某个应用程序的安装位置&#xff0c;可以使用pm命令&#xff08;Package Manager的缩写&#xff09;。这个命令提供了很多关于软件包管理的操作&#xff0c;查询应用安装路径&#xff0c;可以使用path选项。 具体命令如下&#xf…

unity 简易异步socket

1.unity 同步socket 改异步 using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Net.Sockets; using UnityEngine.UI; using System.Threading; using System;public class Echo : MonoBehaviour {//定义套接字Socket socket;//UG…

基于STM32开发的智能温室控制系统

目录 引言环境准备智能温室控制系统基础代码实现&#xff1a;实现智能温室控制系统 4.1 温湿度传感器数据读取4.2 风扇与加热器控制4.3 灌溉系统控制4.4 用户界面与数据可视化应用场景&#xff1a;温室环境管理与优化问题解决方案与优化收尾与总结 1. 引言 随着农业技术的发…

插入删除单链表指定结点-偷天换日法

王道说下面的代码有BUG&#xff0c;比如当删除的结点p在最后一个元素时&#xff0c;p->nextNULL; So *q NULL; q->data就是错误的&#xff0c;我认为加个判断就行 加个判断即可 /*看着是删除q了&#xff0c;从结果上看就是把p删除了 偷天换日法*/ bool DeleteNode(LNod…

如何用Vue3打造一个酷炫的音乐播放器

本文由ScriptEcho平台提供技术支持 项目地址&#xff1a;传送门 Spotify音乐卡片组件 应用场景 本代码用于构建一个类似于Spotify音乐播放器中的音乐卡片组件&#xff0c;可展示歌曲信息、控制播放、调节音量等功能。 基本功能 该音乐卡片组件主要包含以下功能&#xff1…

万万没想到,一个不起眼的小功能,差点把我们项目搞崩溃!

大家好&#xff0c;我是程序员鱼皮。今天分享一个真实项目开发的小故事。 故事 最近我们团队一直在持续更新编程导航网站&#xff0c;优化了界面&#xff0c;也新增了不少功能。现在网站长下面这样&#xff0c;是不是看着比以前舒服多了&#xff1f; 编程导航&#xff1a;htt…

萌啦OZON数据分析工具:OZON电商卖家的得力助手

在当下电商领域&#xff0c;数据分析的重要性不言而喻。对于在OZON这一俄罗斯电商平台上耕耘的卖家而言&#xff0c;拥有一款高效、准确的数据分析工具&#xff0c;无疑是提升销售业绩、优化运营策略的关键。今天&#xff0c;我们就来聊聊“萌啦OZON数据分析工具”&#xff0c;…

模型的手工下载技巧-代码自动批量下载模型文件

之前分享过通过镜像网站手工下载模型文件的技巧&#xff08;见这里模型的手工下载技巧-镜像网站的使用&#xff09;。但有的时候&#xff0c;模型文件数量较多&#xff0c;一个个​手工下载非常不便。比如著名的“麦橘写实”模型。 有没有什么好办法可以把整个目录都下载下来呢…

泰国街头狂潮肖战王一博魅力引爆

泰国街头狂潮&#xff01;肖战王一博魅力引爆&#xff0c;中国明星影响力横扫东南亚&#xff01;当《怦然心动20岁》的镜头转向泰国的繁华街头&#xff0c;一场意想不到的明星风暴正在悄然酝酿。在这场青春的盛宴中&#xff0c;嘉宾们随机采访路人&#xff0c;试图探寻泰国人民…

rospkg.os_detect.OsNotDetected检测不到系统的解决办法

遇到上述报错时&#xff0c;可以参考博客进行解决

美容美发门店SaaS收银系统源码分享、连锁美业拓客系统预约系统源码

美业收银管理系统对于美容、美发、医美行业的门店来说至关重要&#xff0c;它不仅可以帮助提高管理效率和降低成本&#xff0c;还可以改善客户体验并促进业务增长。 &#xff08;私信获取更多方案/演示视频&#xff09; 以下是美业系统的一些作用和重要性&#xff1a; 1. 记录…

两台电脑之间如何互传文件?快学起来

电脑已经是现代社会不可或缺的综合性办公设备&#xff0c;无论是在学习还是工作中&#xff0c;我们很多时候都需要在两台电脑之间互传文件&#xff0c;以便实现文件共享和共同协作。 两台电脑之间如何互传文件&#xff1f;本文将探讨两台电脑互传文件的意义、方法和注意事项。…

远程医疗平台如何连接医生和患者?

远程医疗平台&#xff0c;以其创新的信息技术手段&#xff0c;构筑了一个无视地理界限的医疗服务新体系&#xff0c;实现了医患之间的实时互动和诊疗服务。例如欣九康诊疗系统&#xff0c;通过一系列功能模块&#xff0c;有效连接了医生与患者&#xff0c;为两者提供了一个全面…

2024/6/11随笔

端午买了很多地毯和一些氛围灯&#xff0c;地毯拼夕夕也好贵。地毯折合的一块70*70的8块钱了。。。真是造孽啊&#xff0c;看今天朋友圈怎么都去旅游了呢&#xff1f;不行&#xff0c;明年我也想去旅游了&#xff0c;先去广州再西安&#xff0c;然后再去上海。剩下的就随便去哪…