1. 准备工作
深层链接、网页链接和 Android App Links 快速回顾
用户访问深层链接的主要目标是获取他们想要看到的内容。深层链接具有帮助用户实现这一目标的所有功能。Android 会处理以下类型的链接:
- 深层链接:能够让用户进入应用中的特定部分的 URI,可采用任何架构。
- 网页链接:采用 HTTP 和 HTTPS 架构的深层链接。
- Android App Links:采用 HTTP 和 HTTPS 架构且包含
android:autoVerify
属性的网页链接。
如需详细了解深层链接、网页链接和 Android App Links,请参阅 Android 文档以及 YouTube 和 Medium 上的速成课程。
已熟悉 Android App Links?
如果您了解所有技术细节,请参阅随附的博文中的快速实现方法,只需几步即可完成设置。
Codelab 目标
此 Codelab 会逐步引导您完成内含 Android App Links 的应用,包括配置、实现和验证流程的最佳实践。
Android App Links 的优势之一是安全,也就是说任何未经授权的应用都无法处理您的链接。Android OS 必须验证您所拥有网站的链接,确认是否可将其视为 Android App Links。此过程称为网站关联。
此 Codelab 侧重于拥有网站和 Android 应用的开发者。Android App Links 可实现应用与网站的无缝集成,从而提供更好的用户体验。
前提条件
- 了解有关 adb activity 管理器和 adb 软件包管理系统的基础知识。
- 了解有关使用 Jetpack Compose 进行 Android 开发和导航的基础知识。
学习内容
- 了解为 Android App Links 设计网址的最佳实践。
- 在 Android 应用中配置所有类型的深层链接。
- 了解路径通配符(
path
、pathPrefix
、pathPattern
、pathAdvancePattern
)。 - 了解 Android App Links 验证流程,包括上传 Google Digital Asset Links (DAL) 文件、Android App Links 手动验证流程,以及 Play 管理中心内的“深层链接”信息中心。
- 构建 Android 应用,其中包含不同地点多家餐馆的相关信息。
所需条件
- Android Studio Dolphin (2021.3.1) 或更高版本。
- 用于托管 Google Digital Asset Links (DAL) 文件的网域。(可选:阅读这篇博文,以便您快速做好准备。)
- 可选:Google Play 管理中心开发者账号,可让您使用另一种方法来调试 Android App Links 配置。
2. 设置代码
创建空白的 Compose 应用
如需启动 Compose 项目,请按以下步骤操作:
- 在 Android Studio 中,依次选择 File > New > New Project。
- 从可用模板中选择 Empty Compose Activity。
- 点击 Next,然后配置您的项目,并将其命名为“Deep Links Basics”。请确保您选择的 Minimum SDK 版本至少为 API 级别 21,这是 Compose 支持的最低 API。
- 点击 Finish 并等待项目生成。
- 启动应用,确保应用处于运行状态。您应该会看到一个显示“Hello Android!”消息的空白屏幕。
本 Codelab 的解决方案
您可以从 GitHub 获取本 Codelab 的解决方案代码:
git clone https://github.com/android/deep-links
或者,您可以下载代码库 Zip 文件:
首先,进入 deep-links-introduction 目录。您会在 solution 目录中找到该应用。建议您按照自己的节奏逐步完成 Codelab,必要时再查看解决方案。在此 Codelab 的学习过程中,我们会为您提供需要添加到项目的代码段。
3. 查看面向深层链接的网址设计
RESTful API 设计
链接是网页开发的重要部分,链接设计则是经过无数次迭代产生的各种标准。建议您查看并采用网页开发链接设计标准,这样能让链接更易于使用和维护。
REST(表征状态转移)就是其中一项标准,一种通常用于构建网络服务 API 的架构。Open API 是一项对 REST API 进行标准化的计划。此外,您还可以使用 REST 为深层链接设计网址。
请注意,您不是在构建网络服务。本部分只会着重介绍网址设计。
设计网址
首先,查看网站中生成的各个网址,了解这些网址在 Android 应用中代表的意义:
/restaurants
:列出您管理的所有餐馆。/restaurants/:restaurantName
:显示某一家餐馆的详细信息。/restaurants/:restaurantName/orders
:显示餐馆的订单。/restaurants/:restaurantName/orders/:orderNumber
:显示餐馆中的特定订单。/restaurants/:restaurantName/orders/latest
:显示餐馆的最新订单。
网址设计的重要性
Android 的 intent 过滤器会处理其他应用组件中的操作,还会用于捕获网址。在定义 intent 过滤器以捕获网址时,您必须采用依赖于路径前缀和简单通配符的结构。以下示例展示了餐馆网站中现有网址的组成结构:
https://example.com/pawtato-3140-Skinner-Hollow-Road
尽管此网址指定了餐馆及其位置,但在为 Android 定义 intent 过滤器来捕获网址时,该路径可能仍会带来问题,因为应用是以不同的餐馆网址为基础,如下所示:
https://example.com/rawrbucha-2064-carriage-lane
https://example.com/pizzabus-1447-davis-avenue
使用路径和通配符定义 intent 过滤器来捕获这些网址时,您可以使用类似 https://example.com/*
的路径,这基本上是可行的。尽管如此,您并没有真正解决这个问题,因为网站的不同版块还有其他现有路径,例如:
交付日期:https://example.com/deliveries
管理员:https://example.com/admin
您可能不希望 Android 捕获这些网址,因为其中某些网址可能是内部网址,但定义的 intent 过滤器 https://example.com/*
会捕获它们,包括不存在的网址。当用户点击其中某个网址后,系统会在浏览器上打开该网址(Android 12 以上版本),或者可能会显示消除歧义对话框(Android 12 以下版本)。在此设计中,这并非预期行为。
现在,Android 提供的路径前缀可以解决这个问题,但必须重新设计网址,从:
https://example.com/*
更改为:
https://example.com/restaurants/*
添加分层嵌套结构可让 intent 过滤器明确定义,而 Android 会捕获您指定的网址。
网址设计最佳实践
以下是从 Open API 收集的一些最佳实践,适用于深层链接:
- 将网址设计的重点放在链接显示的业务实体上。例如,对于电子商务,可以是“customers”和“orders”;对于旅行,可以是“tickets”和“flights”。在餐馆应用和网站中,您将使用“restaurants”和“orders”。
- 大多数 HTTP 方法(GET、POST、DELETE、PUT)都是动词,用于描述所发出的请求,但在网址端点中使用动词会让人感到困惑。
- 如需描述集合,请使用实体的复数形式,例如
/restaurants/:restaurantName
。这能让网址更易于阅读和维护。以下是每个 HTTP 方法的示例:
GET /restaurants/pawtato
POST /restaurants
DELETE /restaurants
PUT /restaurants/pawtato
每个网址都更易于阅读和理解其作用。请注意,此 Codelab 不会说明网络服务 API 的设计,以及每个方法的用途。
- 使用逻辑嵌套将包含相关信息的网址进行分组。例如,其中一家餐馆的网址可以包含正在处理的订单:
/restaurants/1/orders
4. 查看数据元素
AndroidManifest.xml
文件是 Android 的重要组成部分,会将应用信息提供给 Android 构建工具、Android OS 和 Google Play。
对于深层链接,您必须使用 3 个主要标记来定义 intent 过滤器:<action>
、<category>
和 <data>
。本部分的主要重点是 <data>
标记。
<data>
元素会在用户点击链接后告知 Android OS 该链接的网址结构。您可以在 intent 过滤器中使用的网址格式和结构如下:
<scheme>://<host>:<port>[<path>|<pathPrefix>|<pathPattern>|<pathAdvancedPattern>|<pathSuffix>]
Android 会读取、解析和合并 intent 过滤器中的所有 <data>
元素,以反映属性的所有变体。例如:
AndroidManifest.xml
<intent-filter>
...
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="example.com" />
<data android:path="/restaurants" />
<data android:pathPrefix="/restaurants/orders" />
</intent-filter>
Android 会捕获以下网址:
http://example.com/restaurants
https://example.com/restaurants
http://example.com/restaurants/orders/*
https://example.com/restaurants/orders/*
路径属性
path
(适用于 API 1)
此属性会指定以 /
开头且与 intent 中“完整路径”匹配的完整路径。例如,android:path="/restaurants/pawtato"
只会匹配 /restaurants/pawtato
网站路径;如果路径为 /restaurant/pawtato
,则因为缺少 s
,系统会将该路径视为不匹配。
pathPrefix
(适用于 API 1)
此属性会指定只与 intent 中路径的“初始部分”匹配的部分路径。例如,
android:pathPrefix="/restaurants"
将匹配餐馆路径 /restaurants/pawtato
和 /restaurants/pizzabus
等。
pathSuffix
(适用于 API 31)
此属性会指定与 intent 中路径的“末尾部分”完全匹配的路径。例如,
android:pathSuffix="tato"
将匹配以“tato”结尾的所有餐馆路径,例如 /restaurants/pawtato
和 /restaurants/corgtato
。
pathPattern
(适用于 API 1)
此属性会指定与 intent 中“包含通配符的完整路径”匹配的完整路径:
- 星号 (
*
) 会匹配前一个字符出现 0 次到多次的序列。 - 英文句点后跟星号 (
.*
) 匹配由零到多个字符构成的任意序列。
示例:
/restaurants/piz*abus
:此模式会匹配“pizzabus”餐馆,但也会匹配名称中含有 0 个或多个z
字符的餐馆,例如/restaurants/pizzabus
、/restaurants/pizzzabus
和/restaurants/pizabus
。/restaurants/.*
:此模式会匹配任何包含/restaurants
路径的餐馆名称(例如/restaurants/pizzabus
和/restaurants/pawtato
),以及应用不知道的餐馆名称(例如/restaurants/wateriehall
)。
pathAdvancePattern
(适用于 API 31)
此属性会指定与“具有类似正则表达式模式的完整路径”匹配的完整路径:
- 句点 (
.
) 匹配任何字符。 - 一组方括号 (
[...]
) 会匹配一系列字符。这个组合也支持非 (^
) 修饰符。 - 星号
*
会与前一个模式匹配 0 次或多次。 - 加号 (
+
) 会与前一个模式匹配 1 次或多次。 - 大括号 (
{...}
) 代表模式可以匹配的次数。
此属性可视为 pathPattern
的扩展,能让系统更灵活地选择要匹配的网址,例如:
/restaurants/[a-zA-Z]*/orders/[0-9]{3}
会匹配任何长度不超过 3 位数的餐馆订单。/restaurants/[a-zA-Z]*/orders/latest
会匹配应用中任何餐馆的最新订单
5. 创建深层链接和网页链接
使用自定义架构的深层链接
使用自定义架构的深层链接是最常见的深层链接类型,也是最容易实现的,但存在缺点。网站无法打开这类链接。不过,任何在清单中声明支持该架构的应用都可以打开该链接。
您可以对 <data>
元素使用任何架构。例如,此 Codelab 使用 food://restaurants/keybabs
网址。
- 在 Android Studio 中,向清单文件添加以下 intent 过滤器:
AndroidManifest.xml
<activity ... >
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="food"/>
<data android:path="/restaurants/keybabs"/>
</intent-filter>
</activity>
- 若要验证应用能否打开设有自定义架构的链接,请向主 activity 添加以下内容,在主屏幕上显示输出内容:
MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Receive the intent action and data
val action: String? = intent?.action;
val data: Uri? = intent?.data;
setContent {
DeepLinksBasicsTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
// Add a Column to print a message per line
Column {
// Print it on the home screen
Greeting("Android")
Text(text = "Action: $action")
Text(text = "Data: $data")
}
}
}
}
}
}
- 若要测试是否收到了 intent,请搭配以下命令使用 Android 调试桥 (adb):
adb shell am start -W -a android.intent.action.VIEW -d "food://restaurants/keybabs"
此命令会启动包含 VIEW 操作的 intent,并将提供的网址用作数据。当您运行此命令后,应用会启动并接收 intent。请注意主屏幕中文本部分的变化。第一行显示“Hello Android!”消息,第二行显示 intent 调用的操作,第三行显示 intent 调用的网址。
在下图中,请注意 Android Studio 的底部,提到的 adb
命令已运行。在屏幕右侧,应用主屏幕显示 intent 信息,表示已收到该 intent。
网页链接
网页链接是使用 http
和 https
(而非自定义架构)的深层链接。
对于网页链接实现,请使用 /restaurants/keybabs/order/latest.html
路径(表示餐馆收到的最新订单)。
- 使用现有的 intent 过滤器调整清单文件。
AndroidManifest.xml
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="food"/>
<data android:path="/restaurants/keybabs"/>
<!-- Web link configuration -->
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:host="sabs-deeplinks-test.web.app"/>
<data android:path="/restaurants/keybabs/order/latest.html"/>
</intent-filter>
由于这两个路径都已共享 (/restaurants/keybabs
),因此最好将它们放在同一个 intent 过滤器下,这样实现起来更加简单,清单文件也更易于阅读。
- 在测试网页链接之前,请重启应用以应用新的更改。
- 使用相同的 adb 命令启动 intent,但在本例中,我们会更新网址。
adb shell am start -W -a android.intent.action.VIEW -d "https://sabs-deeplinks-test.web.app/restaurants/keybabs/orders/latest.html"
请注意,屏幕截图显示,系统已收到 intent,且网络浏览器已打开并显示网站,这是 Android 12 之后版本中的功能。
6. 配置 Android App Links
这类链接能提供最顺畅的用户体验,因为链接在获得用户点击后,一定会将其直接引导至相关应用,而不会显示消除歧义对话框。Android App Links 是在 Android 6.0 中实现的,同时也是最具体的深层链接类型。它们都是使用 http/https
架构和 android:autoVerify
属性的网页链接,使应用成为所有匹配链接的默认处理程序。实现 Android App Links 有两个主要步骤:
- 使用适当的 intent 过滤器更新清单文件。
- 添加网站关联以进行验证。
更新清单文件
- 若要支持 Android App Links,请在清单文件中使用以下代码替换旧配置:
AndroidManifest.xml
<!-- Replace deep link and web link configuration with this -->
<!-- Please update the host with your own domain -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="https"/>
<data android:host="example.com"/>
<data android:pathPrefix="/restaurants"/>
</intent-filter>
此 intent 过滤器会添加 android:autoVerify
属性,并将其设置为 true。这样一来,Android OS 便可在安装应用和每次更新时验证网域。
网站关联
若要验证 Android App Link,请在应用和网站之间建立关联。您必须在网站上发布 Google Digital Asset Links (DAL) JSON 文件,才能进行验证。
Google DAL 是一种协议和 API,定义了有关其他应用和网站的可验证语句。在此 Codelab 中,您将在 assetlinks.json
文件中创建有关 Android 应用的语句。示例如下:
assetlinks.json
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.devrel.deeplinksbasics",
"sha256_cert_fingerprints":
["B0:4E:29:05:4E:AB:44:C6:9A:CB:D5:89:A3:A8:1C:FF:09:6B:45:00:C5:FD:D1:3E:3E:12:C5:F3:FB:BD:BA:D3"]
}
}]
此文件可存储语句列表,但该示例仅显示了一项内容。每个语句都必须包含以下字段:
- 关系。描述声明的且与目标相关的一个或多个关系。
- 目标。此语句适用的资产。可能是以下两个可用目标之一:
web
或android_app
。
Android 语句的 target
属性包含以下字段:
namespace
:适用于所有 Android 应用的android_app
。package_name
:完全限定软件包名称 (com.devrel.deeplinksbasics
)。sha256_cert_fingerprints
:应用证书的指纹。您将在下一部分中了解如何生成此证书。
证书指纹
有多种方法可以获取证书指纹。此 Codelab 会使用两种方法,一种用于应用调试 build,另一种用于帮助将应用发布到 Google Play 商店。
调试配置
Android Studio 首次运行您的项目时,会自动使用调试证书为应用签名。此证书的位置为 $HOME/.android/debug.keystore
。您可以使用 Gradle 命令获取此 SHA-256 证书指纹;具体步骤如下:
- 按
Control
两次,系统应该会显示 Run anything 菜单。如果没有显示,您可以在右侧边栏的 Gradle 菜单中找到该菜单,然后点击 Gradle 图标。
- 输入
gradle signingReport
,然后按Enter
。该命令会在控制台中执行,并显示调试应用变体的指纹信息。
- 要完成网站关联,请复制 SHA-256 证书指纹,更新 JSON 文件,然后将其上传到您网站的
https://<domain>/.well-know/assetlinks.json
位置。请参阅此 Android App Links 博文,了解如何进行设置。 - 如果您的应用仍在运行,请按 Stop 以停止该应用。
- 如需重新启动验证流程,请从模拟器中移除该应用。在模拟器上,点击并按住 DeepLinksBasics 应用图标,然后选择 App Info。在模态窗口中,依次点击 Uninstall 和 Confirm。然后,运行该应用,以便 Android Studio 可以验证关联。
- 务必选择 app 运行配置。否则,Gradle 签名报告将再次运行。
- 重启应用,然后启动含有 Android 应用链接网址的 intent:
adb shell am start -W -a android.intent.action.VIEW -d "https://sabs-deeplinks-test.web.app/restaurants/"
- 请注意,应用会启动,且 intent 会显示在主屏幕上。
恭喜,您刚刚创建了您的首个 Android 应用链接!
版本配置
现在,为了能够将包含 Android App Links 的应用上传到 Play 商店,您必须使用具有正确证书指纹的发布 build。要生成并上传该 build,请按以下步骤操作:
- 在 Android Studio 主菜单中,依次点击 Build > Generate Signed Bundle/APK。
- 在接下来出现的对话框中,选择 Android App Bundle(针对 Play 应用签名)或 APK(如果您要直接部署到设备)。
- 在接下来出现的对话框中,点击 Key store path 下的 Create new。系统会显示一个新窗口。
- 为您的密钥库选择路径,并将其命名为
basics-keystore.jks
。 - 为密钥库创建并确认密码。
- 让“Key”部分的 Alias 字段保留默认值。
- 确保密码和确认密码与密钥库中的密码一样。二者必须一致。
- 填写 Certificate 信息,然后点击 OK。
- 确保已针对 Play 应用签名功能勾选导出加密密钥的选项,然后点击 Next。
- 在该对话框中,选择发布 build 变体,然后点击 Finish。现在,您可以将应用上传到 Google Play 商店并使用 Play 应用签名功能。
Play 应用签名
借助 Play 应用签名功能,Google 可帮助您管理和保护应用的签名密钥。您只需上传在上一步中完成的已签名 app bundle 即可。
如需检索 assetlinks.json
文件的证书指纹,并在发布变体 build 中提供 Android App Links,请按以下步骤操作:
- 在 Google Play 管理中心内,点击创建应用。
- 输入 Deep Links Basics 作为应用名称。
- 在接下来的两个选项中,分别选择应用和免费。
- 接受声明,然后点击创建应用。
- 如需上传 bundle 并能够测试 Android App Links,请在左侧菜单中依次选择测试 > 内部测试。
- 点击创建新的发布版本。
- 在接下来出现的屏幕中,点击上传,然后选择在上一部分中生成的 bundle。您可以在 DeepLinksBascis > app > release 下找到
app-release.aab
文件。点击打开,然后等待 bundle 上传完毕。 - 上传后,让其余字段先保留默认设置。点击保存。
- 点击检查发布版本,然后在接下来出现的屏幕上点击开始发布到内部测试,为下一部分做好准备。请忽略显示的警告,因为发布到 Play 商店不在本 Codelab 的讨论范围内。
- 点击模态窗口上的发布。
- 如需获取 Play 应用签名创建的 SHA-256 证书指纹,请点击左侧菜单中的深层链接标签页,然后查看“深层链接”信息中心。
- 在网域部分下,点击网站的网域。请注意,Google Play 管理中心会提及您尚未验证应用的网域(网站关联)。
- 在修复域名问题部分下,点击展开箭头。
- 在该屏幕中,Google Play 管理中心会展示如何使用证书指纹更新
assetlinks.json
文件。复制相应代码段并更新assetlinks.json
文件。
assetlinks.json
文件更新后,点击重新检查验证状态。如果验证尚未通过,验证服务最多需要等待 5 分钟的时间,才会检测到新的更改。- 如果重新加载深层链接信息中心页面,您不会再看到验证错误。
对已上传的应用进行验证
您已经了解如何验证位于模拟器上的应用。现在,您需要验证上传到 Play 商店的应用。
如需在模拟器上安装应用并确保 Android 应用链接已通过验证,请按以下步骤操作:
- 在左侧边栏中,点击发布版本概览,然后选择您刚刚上传的最新版本,它应该是 1 (1.0) 版。
- 点击发布版本详情(右侧蓝色箭头)以查看版本详情。
- 点击同一蓝色箭头按钮可获取 app bundle 信息。
- 在此模态窗口中,选择下载标签页,然后点击已签名的通用 APK 资源对应的下载。
- 在将 bundle 安装到模拟器之前,先删除 Android Studio 安装的上一个应用。
- 在模拟器上,点击并按住 DeepLinksBasics 应用图标,然后选择 App Info。在模态窗口中,依次点击 Uninstall 和 Confirm。
- 如需安装下载的 bundle,请将下载的
1.apk
文件拖放到模拟器屏幕上,然后等待安装。
- 如需测试验证,请在 Android Studio 中打开终端,并使用以下两个命令运行验证流程:
adb shell pm verify-app-links --re-verify com.devrel.deeplinksbasics
adb shell pm get-app-links com.devrel.deeplinksbasics
- 运行
get-app-links
命令之后,您应该会在控制台上看到verified
消息。如果您看到legacy_failure
消息,请确保证书指纹与您为网站上传的证书指纹相符。如果相符,但您仍然没有看到验证消息,请尝试重新执行第 6、7 和 8 步。
7. 实现 Android App Links
现在,您已经完成所有配置,接下来可以实现该应用了。
Jetpack Compose 将用于实现。如需详细了解 Jetpack Compose,请参阅使用 Jetpack Compose 更快地打造更出色的应用。
代码依赖项
如需添加和更新此项目所需的一些依赖项,请按以下步骤操作:
- 将以下代码添加到
Module
和Project
Gradle 文件中:
build.gradle(项目)
buildscript {
...
dependencies {
classpath "com.google.dagger:hilt-android-gradle-plugin:2.43"
}
}
build.gradle(模块)
plugins {
...
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
...
dependencies {
...
implementation 'androidx.compose.material:material:1.2.1'
...
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"
implementation "androidx.hilt:hilt-navigation-compose:1.0.0"
implementation "com.google.dagger:hilt-android:2.43"
kapt "com.google.dagger:hilt-compiler:2.43"
}
项目 zip 文件中含有一个图片目录,其中有 10 张免版税的图片可用于各个餐馆。您可以随意使用这些图片,也可以添加您自己的图片。
如需为 HiltAndroidApp
添加主入口点,请按以下步骤操作:
- 新建一个名为
DeepLinksBasicsApplication.kt
的 Kotlin 类/文件,然后使用新的应用名称更新清单文件。
DeepLinksBasicsApplication.kt
package com.devrel.deeplinksbasics
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class DeepLinksBasicsApplication : Application() {}
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Update name property -->
<application
android:name=".DeepLinksBasicsApplication"
...
数据
您需要为餐馆创建一个带有 Restaurant
类、库和本地数据源的数据层。所有内容都位于您需要创建的 data
软件包下。为此,请按以下步骤操作:
- 在
Restaurant.kt
文件中,使用以下代码段创建一个Restaurant
类:
Restaurant.kt
package com.devrel.deeplinksbasics.data
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
@Immutable
data class Restaurant(
val id: Int = -1,
val name: String = "",
val address: String = "",
val type: String = "",
val website: String = "",
@DrawableRes val drawable: Int = -1
)
- 在
RestaurantLocalDataSource.kt
文件的数据源类中添加一些餐馆。别忘了使用自己的域名更新数据。您可以参考下面的代码段:
RestaurantLocalDataSource.kt
package com.devrel.deeplinksbasics.data
import com.devrel.deeplinksbasics.R
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RestaurantLocalDataSource @Inject constructor() {
val restaurantList = listOf(
Restaurant(
id = 1,
name = "Pawtato",
address = "3140 Skinner Hollow Road, Medford, Oregon 97501",
type = "Potato and gnochi",
// TODO: Update with your own domain
website = "https://your.own.domain/restaurants/pawtato/",
drawable = R.drawable.restaurant1,
),
Restaurant(
id = 2,
name = "Rawrbucha",
address = "2064 Carriage Lane, Mansfield, Ohio 44907",
type = "Kombucha",
// TODO: Update with your own domain
website = "https://your.own.domain/restaurants/rawrbucha/",
drawable = R.drawable.restaurant2,
),
Restaurant(
id = 3,
name = "Pizzabus",
address = "1447 Davis Avenue, Petaluma, California 94952",
type = "Pizza",
// TODO: Update with your own domain
website = "https://your.own.domain/restaurants/pizzabus/",
drawable = R.drawable.restaurant3,
),
Restaurant(
id = 4,
name = "Keybabs",
address = "3708 Pinnickinnick Street, Perth Amboy, New Jersey 08861",
type = "Kebabs",
// TODO: Update with your own domain
website = "https://your.own.domain/restaurants/keybabs/",
drawable = R.drawable.restaurant4,
),
Restaurant(
id = 5,
name = "BBQ",
address = "998 Newton Street, Saint Cloud, Minnesota 56301",
type = "BBQ",
// TODO: Update with your own domain
website = "https://your.own.domain/restaurants/bbq/",
drawable = R.drawable.restaurant5,
),
Restaurant(
id = 6,
name = "Salades",
address = "4522 Rockford Mountain Lane, Oshkosh, Wisconsin 54901",
type = "salads",
// TODO: Update with your own domain
website = "https://your.own.domain/restaurants/salades/",
drawable = R.drawable.restaurant6,
),
Restaurant(
id = 7,
name = "Gyros and moar",
address = "1993 Bird Spring Lane, Houston, Texas 77077",
type = "Gyro",
// TODO: Update with your own domain
website = "https://your.own.domain/restaurants/gyrosAndMoar/",
drawable = R.drawable.restaurant7,
),
Restaurant(
id = 8,
name = "Peruvian ceviche",
address = "2125 Deer Ridge Drive, Newark, New Jersey 07102",
type = "seafood",
// TODO: Update with your own domain
website = "https://your.own.domain/restaurants/peruvianCeviche/",
drawable = R.drawable.restaurant8,
),
Restaurant(
id = 9,
name = "Vegan burgers",
address = "594 Warner Street, Casper, Wyoming 82601",
type = "vegan",
// TODO: Update with your own domain
website = "https://your.own.domain/restaurants/veganBurgers/",
drawable = R.drawable.restaurant9,
),
Restaurant(
id = 10,
name = "Taquitos",
address = "1654 Hart Country Lane, Blue Ridge, Georgia 30513",
type = "mexican",
// TODO: Update with your own domain
website = "https://your.own.domain/restaurants/taquitos/",
drawable = R.drawable.restaurant10,
),
)
}
- 记得将图片导入您的项目。
- 接下来,在
RestaurantRepository.kt
文件中添加Restaurant
库,其中包含按名称获取餐馆的函数,如以下代码段所示:
RestaurantRepository.kt
package com.devrel.deeplinksbasics.data
import javax.inject.Inject
class RestaurantRepository @Inject constructor(
private val restaurantLocalDataSource: RestaurantLocalDataSource
){
val restaurants: List<Restaurant> = restaurantLocalDataSource.restaurantList
// Method to obtain a restaurant object by its name
fun getRestaurantByName(name: String): Restaurant ? {
return restaurantLocalDataSource.restaurantList.find {
val processedName = it.name.filterNot { it.isWhitespace() }.lowercase()
val nameToTest = name.filterNot { it.isWhitespace() }.lowercase()
nameToTest == processedName
}
}
}
ViewModel
为了能够通过应用和 Android 应用链接选择一家餐馆,您需要创建一个 ViewModel
来更改所选餐馆的值。请按照以下步骤操作:
- 在
RestaurantViewModel.kt
文件中,添加以下代码段:
RestaurantViewModel.kt
package com.devrel.deeplinksbasics.ui.restaurant
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.devrel.deeplinksbasics.data.Restaurant
import com.devrel.deeplinksbasics.data.RestaurantRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class RestaurantViewModel @Inject constructor(
private val restaurantRepository: RestaurantRepository,
) : ViewModel() {
// restaurants and selected restaurant could be used as one UIState stream
// which will scale better when exposing more data.
// Since there are only these two, it is okay to expose them as separate streams
val restaurants: List<Restaurant> = restaurantRepository.restaurants
private val _selectedRestaurant = MutableStateFlow<Restaurant?>(value = null)
val selectedRestaurant: StateFlow<Restaurant?>
get() = _selectedRestaurant
// Method to update the current restaurant selection
fun updateSelectedRestaurantByName(name: String) {
viewModelScope.launch {
val selectedRestaurant: Restaurant? = restaurantRepository.getRestaurantByName(name)
if (selectedRestaurant != null) {
_selectedRestaurant.value = selectedRestaurant
}
}
}
}
Compose
现在您已经有了 ViewModel 和数据层的逻辑,是时候添加界面层了。得益于 Jetpack Compose 库,您只需几个步骤就能完成。就此应用而言,您希望以卡片网格的形式呈现餐馆。用户只需点击每张卡片,便可查看各个餐馆的详细信息。您需要三个主要的可组合函数,以及一个会路由到对应餐馆的导航组件。
如需添加界面层,请按以下步骤操作:
- 从可呈现各个餐馆详细信息的可组合函数着手。在
RestaurantCardDetails.kt
文件中,添加以下代码段:
RestaurantCardDetails.kt
package com.devrel.deeplinksbasics.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.devrel.deeplinksbasics.data.Restaurant
@Composable
fun RestaurantCardDetails (
restaurant: Restaurant,
onBack: () -> Unit,
) {
BackHandler() {
onBack()
}
Scaffold(
topBar = {
TopAppBar(
backgroundColor = Color.Transparent,
elevation = 0.dp,
) {
Row(
horizontalArrangement = Arrangement.Start,
modifier = Modifier.padding(start = 8.dp)
) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "Arrow Back",
modifier = Modifier.clickable {
onBack()
}
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = restaurant.name)
}
}
}
) { paddingValues ->
Card(
modifier = Modifier
.padding(paddingValues)
.fillMaxWidth(),
elevation = 2.dp,
shape = RoundedCornerShape(corner = CornerSize(8.dp))
) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Text(text = restaurant.name, style = MaterialTheme.typography.h6)
Text(text = restaurant.type, style = MaterialTheme.typography.caption)
Text(text = restaurant.address, style = MaterialTheme.typography.caption)
SelectionContainer {
Text(text = restaurant.website, style = MaterialTheme.typography.caption)
}
Image(painter = painterResource(id = restaurant.drawable), contentDescription = "${restaurant.name}")
}
}
}
}
- 接下来,实现网格单元和网格本身。在
RastaurantCell.kt
文件中,添加以下代码段:
RestaurantCell.kt
package com.devrel.deeplinksbasics.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.devrel.deeplinksbasics.data.Restaurant
@Composable
fun RestaurantCell(
restaurant: Restaurant
){
Card(
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 8.dp)
.fillMaxWidth(),
elevation = 2.dp,
shape = RoundedCornerShape(corner = CornerSize(8.dp))
) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Text(text = restaurant.name, style = MaterialTheme.typography.h6)
Text(text = restaurant.address, style = MaterialTheme.typography.caption)
Image(painter = painterResource(id = restaurant.drawable), contentDescription = "${restaurant.name}")
}
}
}
- 在
RestaurantGrid.kt
文件中,添加以下代码段:
RestaurantGrid.kt
package com.devrel.deeplinksbasics.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.devrel.deeplinksbasics.data.Restaurant
@Composable
fun RestaurantGrid(
restaurants: List<Restaurant>,
onRestaurantSelected: (String) -> Unit,
navigateToRestaurant: (String) -> Unit,
) {
Scaffold(topBar = {
TopAppBar(
backgroundColor = Color.Transparent,
elevation = 0.dp,
) {
Text(text = "Restaurants", fontWeight = FontWeight.Bold)
}
}) { paddingValues ->
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 200.dp),
modifier = Modifier.padding(paddingValues)
) {
items(items = restaurants) { restaurant ->
Column(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = {
onRestaurantSelected(restaurant.name)
navigateToRestaurant(restaurant.name)
})
) {
RestaurantCell(restaurant)
}
}
}
}
}
- 接下来,您需要实现应用状态和导航逻辑,并更新
MainActivity.kt
。用户只要点击餐馆卡片,便会被定向到特定餐馆。在RestaurantAppState.kt
文件中,添加以下代码段:
RestaurantAppState.kt
package com.devrel.deeplinksbasics.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
sealed class Screen(val route: String) {
object Grid : Screen("restaurants")
object Name : Screen("restaurants/{name}") {
fun createRoute(name: String) = "restaurants/$name"
}
}
@Composable
fun rememberRestaurantAppState(
navController: NavHostController = rememberNavController(),
) = remember(navController) {
RestaurantAppState(navController)
}
class RestaurantAppState(
val navController: NavHostController,
) {
fun navigateToRestaurant(restaurantName: String) {
navController.navigate(Screen.Name.createRoute(restaurantName))
}
fun navigateBack() {
navController.popBackStack()
}
}
- 对于导航,您需要创建
NavHost
,并使用可组合路由定向到各个餐馆。在RestaurantApp.kt
文件中,添加以下代码段:
RestaurantApp.kt
package com.devrel.deeplinksbasics.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import com.devrel.deeplinksbasics.ui.restaurant.RestaurantViewModel
@Composable
fun RestaurantApp(
viewModel: RestaurantViewModel = viewModel(),
appState: RestaurantAppState = rememberRestaurantAppState(),
) {
val selectedRestaurant by viewModel.selectedRestaurant.collectAsState()
val onRestaurantSelected: (String) -> Unit = { viewModel.updateSelectedRestaurantByName(it) }
NavHost(
navController = appState.navController,
startDestination = Screen.Grid.route,
) {
// Default route that points to the restaurant grid
composable(Screen.Grid.route) {
RestaurantGrid(
restaurants = viewModel.restaurants,
onRestaurantSelected = onRestaurantSelected,
navigateToRestaurant = { restaurantName ->
appState.navigateToRestaurant(restaurantName)
},
)
}
// Route for the navigation to a particular restaurant when a user clicks on it
composable(Screen.Name.route) {
RestaurantCardDetails(restaurant = selectedRestaurant!!, onBack = appState::navigateBack)
}
}
}
- 您现在可以使用应用实例更新
MainActivity.kt
了。请将文件替换为以下代码:
MainActivity .kt
package com.devrel.deeplinksbasics
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
import com.devrel.deeplinksbasics.ui.RestaurantApp
import com.devrel.deeplinksbasics.ui.theme.DeepLinksBasicsTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
DeepLinksBasicsTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
RestaurantApp()
}
}
}
}
}
- 运行应用以浏览网格,然后选择特定餐馆。当您选择一家餐馆后,应用会展示该餐馆及其详细信息。
Android App Links
现在,将您的 Android App Links 添加到网格和每个餐馆。您已经为 /restaurants
下的网格设置 AndroidManifest.xml
部分。真正巧妙的是,您可以对每个餐馆使用相同的设置;只需向逻辑添加新的路由配置即可。为此,请按以下步骤操作:
- 使用 intent 过滤器更新清单文件,以接收
/restaurants
作为路径,同时别忘了将您的网域添加为主机。在AndroidManifest.xml
文件中,添加以下代码段:
AndroidManifest.xml
...
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:host="your.own.domain"/>
<data android:pathPrefix="/restaurants"/>
</intent-filter>
- 在
RestaurantApp.kt
文件中,添加以下代码段:
RestaurantApp.kt
...
import androidx.navigation.NavType
import androidx.navigation.navArgument
import androidx.navigation.navDeepLink
fun RestaurantApp(...){
NavHost(...){
...
// Route for the navigation to a particular restaurant when a user clicks on it
// and for an incoming deep link
// Update with your own domain
composable(Screen.Name.route,
deepLinks = listOf(
navDeepLink { uriPattern = "https://your.own.domain/restaurants/{name}" }
),
arguments = listOf(
navArgument("name") {
type = NavType.StringType
}
)
) { entry ->
val restaurantName = entry.arguments?.getString("name")
if (restaurantName != null) {
LaunchedEffect(restaurantName) {
viewModel.updateSelectedRestaurantByName(restaurantName)
}
}
selectedRestaurant?.let {
RestaurantCardDetails(
restaurant = it,
onBack = appState::navigateBack
)
}
}
}
}
在后台,NavHost
会将 Android intent Uri
数据与可组合路由进行匹配。如果路由匹配,系统会呈现 composable
。
composable
组件可以接受 deepLinks
参数,其中包含从 intent 过滤器接收的 URI 列表。在此 Codelab 中,您将添加所创建网站的网址,并定义 ID 参数,以接收用户并将其引导至特定餐馆。
- 为了确保应用逻辑能在用户点击 Android 应用链接后将其引导至对应餐馆,请使用
adb
:
adb shell am start -W -a android.intent.action.VIEW -d "https://sabs-deeplinks-test.web.app/restaurants/gyrosAndMoar"
请注意,该应用显示的是对应餐馆。
8. 查看 Play 管理中心内的信息中心
您已经看过深层链接的信息中心。此信息中心会提供所有必要信息,以确保深层链接正常运行。您甚至可以按应用版本查看!此处会显示您在清单文件中添加的网域、链接和自定义链接;如果 assetlinks.json
文件出现问题,此信息中心甚至会显示更新该文件的位置。
9. 总结
恭喜,您已成功构建您的第一个 Android App Links 应用!
您已了解设计、配置、创建和测试 Android App Links 的流程。这个过程有许多不同的部分,因此,此 Codelab 汇总了所有相关详情,以帮助您在 Android OS 开发中取得成功。
现在,您已了解让 Android App Links 正常运行的关键步骤。