目录
前言
Android官方文档关于DeepLink的介绍很有限,通过本篇文章可以较为深入地理解DeepLink。我认为DeepLink是有一定难度的,想要灵活运用需要多花心思研究。
一、DeepLink定义
引用Android官方文档的定义:
In Android, a deep link is a link that takes you directly to a specific destination within an app.
翻译:深链接是一个能够直接到达app中特定目的地的链接。
二、显式DeepLink
1.简介
- 显式DeepLink必须通过PendingIntent实例导航到特定目的地。
- 使用场景为
Notification
(常用)、app widget
2.创建显式DeepLink
通过NavDeepLinkBuilder构造PendingIntent
PendingIntent pendingIntent = new NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph) // 设置导航图
.setDestination(R.id.aFragment) // 设置目的地,可以通过addDestination()方法添加多个目的地
.setArguments(args) // 传递Bundle参数
.createPendingIntent(); // 构造PendingIntent
然后把PendingIntent实例塞到Notification里即可实现点击Notification导航到AFragment。
3.NavDeepLinkBuilder接口说明
(1).NavDeepLinkBuilder(Context context) [必选]
构造方法; Context参数指定了处理显式DeepLink的Activity。
Context参数分类:
- 传入的Context是一个Activity
Navigation组件会直接使用传入的Activity处理显式DeepLink - 传入的Context不是Activity
Navigation组件会通过PackageManager.getLaunchIntentForPackage()方法获取应用的默认主Activity处理显式DeepLink。
对于单Activity架构模式下的应用,对Context参数不作要求。
(2).setGraph(int navGraphId) [必选]
设置目的地所在的导航图。
该导航图必须在处理显式DeepLink的Activity的NavHost里。如果不在,本次深链接会导航到Activity,但不会导航到导航图,导致显示空白界面。
(3).setDestination(int destId)/addDestination(int destId) [必选其一]
设置/添加目的地。
setDestination()方法会清空之前设置/添加的目的地。
(4).setArguments(Bundle args) [可选]
给目的地传递参数。
显式DeepLink的每个目的地都会收到这些参数,所以Bundle
应该要包含所有目的地需要的参数。
(5).setComponentName(Class activityClass) [可选]
指定处理显式DeepLink的Activity。
与构造方法的Context参数的作用一样,但是本方法的优先级比构造方法更高。
对于单Activity架构模式下的应用,不需要调用本方法。
(6).createPendingIntent() [必选]
作用:通过TaskStackBuilder构造PendingIntent。
说明:如果不想理解原理,可以跳过下面关于本方法的源码解析。
我们看一下涉及该方法的关键源码:
public class NavDeepLinkBuilder(...) {
public fun createPendingIntent(): PendingIntent {
......
// 通过TaskStackBuilder构造PendingIntent
return createTaskStackBuilder().getPendingIntent(
requestCode, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
// 创建TaskStackBuilder实例
public fun createTaskStackBuilder(): TaskStackBuilder {
......
// 构造TaskStackBuilder实例并放入包含显式DeepLink的Intent,该Intent指向用于处理显式DeepLink的Activity
val taskStackBuilder = TaskStackBuilder.create(context)
.addNextIntentWithParentStack(Intent(intent))
......
return taskStackBuilder
}
}
再看一下TaskStackBuilder.getPendingIntent()方法的关键源码:
public final class TaskStackBuilder implements Iterable<Intent> {
public PendingIntent getPendingIntent(int requestCode, int flags, @Nullable Bundle options) {
......
// 第一行代码
Intent[] intents = mIntents.toArray(new Intent[mIntents.size()]);
// 第二行代码
intents[0] = new Intent(intents[0]).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
......
return PendingIntent.getActivities(...);
}
}
对于上面在TaskStackBuilder.getPendingIntent()方法里注释的第二行代码
有着极其重要的三层含义:
- 在单Activity架构模式下,intent[0]即指向应用的唯一主Activity,所以主Activity会收到这3个Flag
- 前两个Flag意味着在启动intent[0]之前会清空应用的任务栈,也就是销毁所有的Activity
- Navigation组件会在主Activity的onCreate()方法里通过Intent中的Flag(这3个Flag为0x1000c000)去处理DeepLink
至于Navigation组件是怎么处理DeepLink的,参见处理DeepLink。
4.构造NavDeepLinkBuilder的其他方法
当你可以获取到NavController实例时,使用NavController.createDeepLink()方法构造NavDeepLinkBuilder
源码简析(Kotlin):
public open class NavController(...){
public open fun createDeepLink(): NavDeepLinkBuilder {
return NavDeepLinkBuilder(this)
}
}
public class NavDeepLinkBuilder(...) {
// 通过NavController构造NavDeepLinkBuilder实例
internal constructor(navController: NavController) : this(navController.context) {
// 设置显式DeepLink的导航图
graph = navController.graph
}
}
通过源码可以看到,这个方法其实就等价于上面介绍的
NavDeepLinkBuilder navDeepLinkBuilder = new NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph)
所以,还需要设置目的地才能构造PendingIntent。
5.显式DeepLink的导航逻辑
说完了如何创建显式DeepLink,我们还必须要理解显式DeepLink的导航逻辑。由于创建和使用显示DeepLink的方法是固定的,所以它的导航逻辑也是固定的。
假设我们依次给显式DeepLink添加了A、B两个目的地,那么会执行以下步骤:
- 清空任务栈
- 启动Activity
- 依次导航到从根导航图到A目的地的所在导航图的所有startDestination,最后导航到A目的地
- 如果有B目的地的祖先导航图的startDestination还未导航到过,则依次导航到从该祖先导航图到B目的地的所在导航图的所有startDestination,最后导航到B目的地。如果已经导航到过B目的地的所有祖先导航图的startDestination,则直接导航到B目的地。
假设A目的地就是一个startDestination,那么不会重复创建A目的地的实例。
这样用文字表述可能比较抽象,请接着看下面的举例说明。
6.举例说明导航逻辑
因为清空任务栈和启动Activity这两个步骤是固定的,所以只举例显式DeepLink在导航图的导航逻辑。
假设有下面这样一个导航图,根导航图是nav_graph
,嵌套导航图为nav_graph_nested
,共有AFragment、BFragment、CFragment、DFragment四个目的地。
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph"
app:startDestination="@id/aFragment">
<fragment
android:id="@+id/aFragment"
android:name="com.scx.navigation.deeplink.AFragment"/>
<fragment
android:id="@+id/bFragment"
android:name="com.scx.navigation.deeplink.BFragment"/>
<!-- 嵌套导航图 -->
<navigation
android:id="@+id/nav_graph_nested"
app:startDestination="@id/cFragment">
<fragment
android:id="@+id/cFragment"
android:name="com.scx.navigation.deeplink.CFragment"/>
<fragment
android:id="@+id/dFragment"
android:name="com.scx.navigation.deeplink.DFragment"/>
</navigation>
</navigation>
(1).设置显式DeepLink的目的地为DFragment
.setDestination(R.id.dFragment)
结果:依次导航到AFragment、CFragment、DFragment
(2).给显式DeepLink依次添加BFragment、DFragment两个目的地
.addDestination(R.id.bFragment)
.addDestination(R.id.dFragment)
结果:依次导航到AFragment、BFragment、CFragment、DFragment
(3).给显式DeepLink依次添加DFragment、BFragment两个目的地
.addDestination(R.id.dFragment)
.addDestination(R.id.bFragment)
结果:依次导航到AFragment、CFragment、DFragment、BFragment
。(注意与(2)的区别)
(4).设置显式DeepLink的目的地为CFragment
.setDestination(R.id.cFragment)
结果:依次导航到AFragment、CFragment
(5).给显式DeepLink依次添加DFragment、CFragment两个目的地
.addDestination(R.id.dFragment)
.addDestination(R.id.cFragment)
结果:依次导航到AFragment、CFragment、DFragment、CFragment
(注意会创建两个CFragment实例)
三、隐式DeepLink
1.创建隐式DeepLink
(1)通过<deepLink>元素给目的地声明隐式DeepLink
假设给DFragment声明隐式DeepLink,DFragment需要四个参数
<fragment
android:id="@+id/dFragment"
android:name="com.scx.navigation.deeplink.DFragment" >
<!-- id、name作为路径参数,phone、time作为查询参数 -->
<!-- 路径参数可以为路径的子集,例如scx://example.com/id{id}/{name} 这里的id也是合法的 -->
<deepLink app:uri="scx://example.com/{id}/{name}?phone={phone}&time={time}"
app:action="android.intent.action.MY_ACTION"
app:mimeType="type/video1"/>
<argument
android:name="id"
app:argType="integer" />
<argument
android:name="name"
app:argType="string" />
<argument
android:name="phone"
app:argType="string"
android:defaultValue="123"/>
<argument
android:name="time"
app:argType="string"
android:defaultValue="00:00"/>
</fragment>
注意以下几点:
从声明的角度:
- uri、action、mimeType三种方式中只有uri能传递参数
- uri中参数格式为{声明的参数名},注意路径参数和查询参数的语法区别
- 如果声明的uri没有设置协议,则默认可以匹配
http:
或者https:
- 如果声明的uri缺少参数,则使用该uri会导航到应用,但不会导航到任何目的地
从使用/匹配的角度:
-
uri、action、mimeType三选一即可匹配
-
必须完全匹配路径参数,可以不提供有默认值的查询参数
-
提供了多余的查询参数,也可以匹配uri
-
路径参数不全的uri视为无效uri
-
查询参数(没有默认值)不全的uri会导航到应用,但不会导航到任何目的地
-
如果两个目的地有相似的uri,则遵循最全匹配原则。
例如:A目的地uri为scx://example.com/{id}?name={name},B目的地uri为scx://example.com/{id}?name={name}&phone={phone}(其中phone参数有默认值),给出的uri为scx://example.com/1?name=Jack,那么给出的uri最全匹配于A目的地。
(2)在处理隐式DeepLink的Activity内添加导航图
在AndroidManifest.xml文件的Activity内添加<nav-graph>
元素:
<activity name=".MainActivity" ...>
...
<nav-graph android:value="@navigation/nav_graph" />
...
</activity>
假设nav_graph内部有一个嵌套导航图nav_graph_nested,那么MainActivity也会处理nav_graph_nested中的隐式DeepLink。
2.使用隐式DeepLink
(1)应用内使用
通过NavController的navigate(NavDeepLinkRequest request)方法
// 三个参数分别是Uri uri, String action, String mimeType。如果不需要可以传null
NavDeepLinkRequest request = new NavDeepLinkRequest(Uri.parse("scx://example.com/1/Jack"), "android.intent.action.MY_ACTION", "type/video1");
NavHostFragment.findNavController(this).navigate(request);
NavController.navigate()方法也可以直接接收Uri作为参数实现导航,但根据源码可知其本质上还是使用了NavDeepLinkRequest
。
注意:
- 通过NavDeepLinkRequest只能导航到NavHost导航图以内的目的地。跨模块使用的前提是,主模块的导航图
include
子模块的导航图。 - 通过NavDeepLinkRequest会直接导航到指定目的地,不会先导航到startDestination。也不会销毁当前应用的状态。
(2)应用外使用
假设我们想点击链接导航到上面创建的DFragment。
- 通过应用外链接
网页的核心代码:
<html>
<a href="scx://example.com/1/Jack?time=11:11">Deep Link To DFragment</a>
</html>
- 通过shortcut
shortcuts.xml:
<shortcuts xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:shortcutId="DFragment"
android:enabled="true"
android:shortcutShortLabel="@string/open_deep_link"
android:shortcutLongLabel="@string/open_deep_link"
android:shortcutDisabledMessage="@string/disabled"
tools:targetApi="n_mr1">
<intent
android:action="android.intent.action.VIEW"
android:data="scx://example.com/1/Jack" />
</shortcut>
</shortcuts>
3.处理DeepLink
使用NavController.handleDeepLink(Intent intent)方法处理DeepLink
- Activity的启动模式为
standard
无需手动调用NavController.handleDeepLink(Intent intent)方法。
- Activity的启动模式为
singleTask/singleTop
需要在onNewIntent()方法内手动调用NavController.handleDeepLink(Intent intent)方法:
@Override
protected void onNewIntent(Intent intent) {
......
navController.handleDeepLink(intent);
}
有几个基本要点:
(1)从应用外使用隐式DeepLink
会通过隐式Intent跳转到应用内处理该隐式DeepLink的Activity。
(2)只要走了Activity的onCreate()方法,就一定会走NavController的handleDeepLink(Intent intent)方法。
因为在Activity的onCreate()方法里,Navigation组件一定会通过NavController.handleDeepLink(Intent intent)处理DeepLink。
(3)不管Activity启动模式是什么,shortcut方式不会走onNewIntent()方法,会直接将之前的Activity销毁再重建,然后走Activity的onCreate()方法。
4.Intent的Flag解析
(1)怎么修改Intent的Flag?
- onCreate方法里
protected void onCreate(Bundle savedInstanceState) {
Intent intent = getIntent();
// 重新设置Intent的Flag
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
super.onCreate(savedInstanceState);
......
}
- onNewIntent方法里
protected void onNewIntent(Intent intent) {
......
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
navController.handleDeepLink(intent);
}
(2)不同Flag的导航逻辑
NavController的handleDeepLink(Intent intent)方法根据Intent的Flag去决定导航逻辑。
下面进行分类讨论:
- Ⅰ、只包含
FLAG_ACTIVITY_NEW_TASK
,不包含FLAG_ACTIVITY_CLEAR_TASK
(例如 0x10000000)
通过TaskStackBuilder重建任务栈,重建Activity,然后走Ⅱ中的步骤。
- Ⅱ、既包含
FLAG_ACTIVITY_NEW_TASK
,又包含FLAG_ACTIVITY_CLEAR_TASK
(例如 0x1000c000)
弹出整个导航图,再逐级导航到DeepLink中的目的地(与显式DeepLink在导航图的导航逻辑一致)。不会重建任务栈,不会重建Activity。
- Ⅲ、不包含
FLAG_ACTIVITY_NEW_TASK
(例如 0)
只导航到最终目的地,不会像上面两种情况一样逐级导航。不会重建任务栈,不会重建Activity。
(3)Flag相关总结
以下是两种隐式DeepLink使用方法对应的默认Flag
和Activity所在任务栈
相关结论:
隐式DeepLink方式 | Activity启动模式 | 触发DeepLink前,Activity是否已运行 | Intent默认Flag | Activity是否在触发DeepLink的应用任务栈 |
---|---|---|---|---|
应用外链接 | standard | 与之无关 | 0 | 是 |
singleTask | 0x10000000 | 否 | ||
shortcut | 与之无关 | 是 | 0x1040c000 | - |
否 | 0x1000c000 |
针对这个表格的解释:
Intent默认Falg
表示我们不对Intent进行任何修改的情况下,Intent所携带的Flag- 由于shortcut属于桌面快捷方式,因此不存在“Activity是否在触发DeepLink的应用任务栈”的说法
特殊情况:
假设通过应用外链接触发隐式DeepLink,启动了standard启动模式的Activity,然后在onCreate()方法里将Flag由0改为0x10000000,那么Activity将不在触发DeepLink的应用任务栈。
除了上面所说的特殊情况,不论你如何修改Flag的值,都不会影响Activity启动后所在的任务栈。
四、route
route是隐式DeepLink的一种特殊形式。
1、创建route
通过在目的地里添加<route>元素创建route。
<fragment
android:id="@+id/cFragment"
app:route="cFragment"
android:name="com.scx.navigation.deeplink.CFragment"
android:label="CFragment" >
</fragment>
可以看到route就是一个字符串。它携带参数的方式与uri相同,相当于uri去除了协议和服务器地址。
2、使用route
通过NavController.navigate()方法使用route
NavHostFragment.findNavController(this).navigate("cFragment");
只用一行代码就搞定了,通过源码知道它本质上用的还是NavDeepLinkRequest
,所以才说route是隐式DeepLink的一种特殊形式。
五、补充知识点
1.PackageManager.getLaunchIntentForPackage(String packageName)方法
定义:
public abstract class PackageManager {
/**
* Returns a "good" intent to launch a front-door activity in a package.
* This is used, for example, to implement an "open" button when browsing
* through packages. The current implementation looks first for a main
* activity in the category {@link Intent#CATEGORY_INFO}, and next for a
* main activity in the category {@link Intent#CATEGORY_LAUNCHER}. Returns
* <code>null</code> if neither are found.
*
* @param packageName The name of the package to inspect.
*
* @return A fully-qualified {@link Intent} that can be used to launch the
* main activity in the package. Returns <code>null</code> if the package
* does not contain such an activity, or if <em>packageName</em> is not
* recognized.
*/
public abstract @Nullable Intent getLaunchIntentForPackage(@NonNull String packageName);
}
作用:找到指定的package的主Activity,并返回为Intent。
寻找规则:优先寻找声明了Intent.CATEGORY_INFO的主Activity,其次是声明了Intent.CATEGORY_LAUNCHER的主Activity
主Activity:声明了<action android:name="android.intent.action.MAIN" />
的Activity是主Activity,也可以称为主入口Activity
例如:
<activity
android:name="com.scx.navigation.deeplink.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.scx.navigation.deeplink.OtherActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.INFO" />
</intent-filter>
</activity>
此时通过PackageManager.getLaunchIntentForPackage()方法获得的Intent中的Activity是OtherActivity
ps:如果一个应用没有Launcher Activity,它将不会显示在设备应用列表里
六、最终效果和工程代码
1.最终效果
通过长按应用图标可以使用shortcut。