Android Jetpack Navigation组件(四):DeepLink(深链接)

前言

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参数分类:

  1. 传入的Context是一个Activity
    Navigation组件会直接使用传入的Activity处理显式DeepLink
  2. 传入的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()方法里注释的第二行代码有着极其重要的三层含义

  1. 在单Activity架构模式下,intent[0]即指向应用的唯一主Activity,所以主Activity会收到这3个Flag
  2. 前两个Flag意味着在启动intent[0]之前会清空应用的任务栈,也就是销毁所有的Activity
  3. 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两个目的地,那么会执行以下步骤:

  1. 清空任务栈
  2. 启动Activity
  3. 依次导航到从根导航图到A目的地的所在导航图的所有startDestination,最后导航到A目的地
  4. 如果有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}&amp;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使用方法对应的默认FlagActivity所在任务栈相关结论:

隐式DeepLink方式Activity启动模式触发DeepLink前,Activity是否已运行Intent默认FlagActivity是否在触发DeepLink的应用任务栈
应用外链接standard与之无关0
singleTask0x10000000
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。

2.工程代码

代码地址

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值