22、导航与通知:点击通知跳转到指定页面、配置通知的DeepLink、处理通知中的参数

通知推送,几乎是每个App的标配。但通知点击后跳转到哪里,怎么带参数,这里面的坑其实不少。我见过太多项目,通知点进去直接崩了,或者参数丢了,用户一脸懵。

今天我们就来彻底搞定这件事。用Navigation组件配合DeepLink,让通知跳转变得清晰可控。

通知跳转的核心思路

说白了,通知就是一个“外部入口”。它跟用户从桌面图标点进来不一样,通知往往带着特定目标——比如跳到一个订单详情、一个聊天窗口。

在单Activity架构下,我们只有一个Activity。所有页面切换都靠NavController。那通知怎么告诉NavController该去哪?

答案就是:DeepLink

DeepLink本质上是一个URI。你可以把它理解成“页面的地址”。通知里塞一个DeepLink,点击后系统解析这个地址,Navigation组件自动找到对应的目的地。

核心流程:

  1. 用户收到通知
  2. 点击通知,触发PendingIntent
  3. PendingIntent携带DeepLink URI
  4. Activity启动,NavController解析URI
  5. 导航到目标页面,参数自动注入
收到通知 点击通知 PendingIntent 携带DeepLink URI 启动Activity NavController 解析DeepLink URI 匹配目的地 自动注入参数 跳转到指定页面

第一步:在NavGraph中配置DeepLink

先别急着写通知代码。我们得先在导航图里把DeepLink注册好。这样Navigation才知道哪个页面能接收什么格式的URI。

假设我们有一个订单详情页,需要接收订单ID。DeepLink的URI可以设计成:myapp://order/{orderId}

<!-- res/navigation/nav_graph.xml -->
<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/homeFragment">

    <fragment
        android:id="@+id/orderDetailFragment"
        android:name="com.example.OrderDetailFragment"
        android:label="订单详情">

        <argument
            android:name="orderId"
            app:argType="string" />

        <deepLink
            app:uri="myapp://order/{orderId}" />

    </fragment>

</navigation>

小技巧:URI里的占位符 {orderId} 会自动映射到同名的argument上。命名保持一致,省心省力。

第二步:构建通知的PendingIntent

通知的PendingIntent需要携带DeepLink。这里有个关键点:用NavDeepLinkBuilder。这是Navigation组件提供的工具类,专门用来构建通知的Intent。

我自己早期踩过坑,手动拼Intent,结果参数传过去总是null。后来才发现,直接用NavDeepLinkBuilder最稳。

// 构建DeepLink URI
val deepLinkUri = "myapp://order/${orderId}"

// 用NavDeepLinkBuilder创建PendingIntent
val pendingIntent = NavDeepLinkBuilder(context)
    .setGraph(R.navigation.nav_graph)
    .setDestination(R.id.orderDetailFragment)
    .setArguments(Bundle().apply {
        putString("orderId", orderId)
    })
    .createPendingIntent()

// 构建通知
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
    .setSmallIcon(R.drawable.ic_notification)
    .setContentTitle("订单更新")
    .setContentText("您的订单 $orderId 状态已变更")
    .setContentIntent(pendingIntent)
    .setAutoCancel(true)
    .build()

NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, notification)

注意:setGraph() 和 setDestination() 必须调用。否则NavDeepLinkBuilder不知道你的导航结构。我曾经漏掉setGraph,结果通知点进去直接崩溃,查了半天才发现。

第三步:处理通知中的参数

参数传过去了,目标Fragment怎么接收?其实跟普通导航传参一样。在Fragment的onCreate或onViewCreated里读取arguments即可。

class OrderDetailFragment : Fragment() {

    private val args: OrderDetailFragmentArgs by navArgs()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val orderId = args.orderId
        // 用orderId加载数据
        loadOrderDetail(orderId)
    }
}

这里用 navArgs() 委托,类型安全,不用手动解析Bundle。我推荐大家都用这个方式。

第四步:处理App已启动的情况

上面讲的流程,假设App被杀了,通知点进来重新创建Activity。但如果App已经在前台呢?

嗯,这里有个细节。如果Activity已经存在,系统不会重新创建,而是调用 onNewIntent()。我们需要在那里处理DeepLink。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 正常初始化NavController
        val navController = findNavController(R.id.nav_host_fragment)

        // 处理启动Intent中的DeepLink
        intent?.data?.let { uri ->
            navController.handleDeepLink(intent)
        }
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)

        // App已存在时,处理新的DeepLink
        intent.data?.let { uri ->
            val navController = findNavController(R.id.nav_host_fragment)
            navController.handleDeepLink(intent)
        }
    }
}

关键点:

  • onCreate里处理:App冷启动
  • onNewIntent里处理:App热启动(已在后台)
  • 两个地方都要调用 handleDeepLink()

第五步:AndroidManifest中的配置

别忘了在Manifest里声明DeepLink的scheme。否则系统不认识你的URI。

<activity
    android:name=".MainActivity"
    android:launchMode="singleTask">

    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <!-- DeepLink intent-filter -->
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data
            android:scheme="myapp"
            android:host="order" />
    </intent-filter>

</activity>

launchMode设为singleTask,这样通知点进来不会重复创建Activity实例。这个我建议固定下来。

常见坑与避坑指南

问题 原因 解决方案
通知点进去白屏 NavDeepLinkBuilder没设setGraph 加上setGraph(R.navigation.nav_graph)
参数传过去是null URI占位符和argument名称不一致 检查 {orderId} 和 argument name 是否匹配
App在前台时通知无效 没处理onNewIntent 在onNewIntent里调用handleDeepLink
通知点击后回退栈混乱 没设置launchMode Activity设为singleTask

避坑指南:我曾经在一个项目里,通知点进去总是跳到首页而不是目标页。排查了半天,发现是NavDeepLinkBuilder的setDestination写错了ID。嗯,这种低级错误,大家引以为戒。

总结一下

通知跳转这件事,说白了就是三步:

  • 在NavGraph里配好DeepLink
  • 用NavDeepLinkBuilder构建PendingIntent
  • 在Activity的onCreate和onNewIntent里处理DeepLink

参数传递、回退栈管理,Navigation组件都帮你搞定了。你只需要把URI设计好,剩下的交给框架。

我个人习惯把DeepLink的URI统一管理在一个常量类里,比如 const val ORDER_DEEPLINK = "myapp://order/{orderId}"。这样维护起来方便,不会到处散落字符串。

好了,通知跳转就讲到这里。下一章我们会聊更进阶的话题——导航与多模块架构。到时候见。


公众号:蓝海资料掘金营,微信deep3321