---
title: 'Deep Links and Shortcuts'
source: 'https://academia.sh/en/courses/platform-services-and-integrations/deep-links-and-shortcuts'
course: 'Platform Services and Integrations'
language: en
updated: '2026-10-21T05:02:41+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Separates the roles of the deep link, the verified app link and the shortcut'
    - 'Measures that the same deep link is processed twice while the screen is recreated'
    - 'Shows that when the handled marker is kept in saved state, the links arrive once'
    - 'Shows that if the marker is kept in memory, the link is duplicated again after the process ends'
---

# Deep Links and Shortcuts

A deep link ties an address that comes from outside to a specific screen of the app; while the screen is being recreated, the same link can be processed once more. The lesson measures this duplicated delivery and the effect of keeping the handled marker in saved state or in memory.

In the previous lesson readings were skipped when the receiver could not keep up. In a deep link the problem comes from the opposite direction: a link reaches the receiver once, but the receiver can process it one more time. The user taps an address in an email and the app opens the related product screen. When the screen is rotated it is recreated, and if the same link is processed again, the user sees the product screen opened a second time. In this lesson we ask the same question again: **how many copies of this sent event reached the receiver?**

```kotlin
// Arrival.kt — classifies how many copies of a sent event reached the receiver
class Signal(val key: String, val seq: Int)

class Arrival(val label: String, val sent: Signal, received: List<Signal>) {
    val copies: Int = received.count { it.seq == sent.seq }
    val verdict: String = when {
        copies > 1 -> "duplicated"
        copies == 1 -> "once"
        received.any { it.key == sent.key && it.seq > sent.seq } -> "superseded"
        else -> "lost"
    }

    fun line() {
        println("$label: key=${sent.key} | copies=$copies -> $verdict")
    }
}

fun countArrivals(arrivals: List<Arrival>) {
    for (name in listOf("once", "duplicated", "superseded", "lost")) {
        println("$name: ${arrivals.count { it.verdict == name }}")
    }
}
```

The classification is the same as in the previous lessons. The rule counts the copies in the receiver's list that carry the same sequence number; if there is no copy, it checks whether a later event with the same key arrived. It does not see the content or the cause.

## The Deep Link and the Shortcut

A deep link lets an address open a specific screen of the app. In the manifest the app defines, with an `intent` filter, which addresses it opens for. The verified app link goes one step further: the owner of the address verifies that the domain is tied to the app, and the platform hands the link to the app without asking the user. The shortcut is an entry that appears when the app icon is pressed and held and that leads to a specific screen; it also carries an `intent`.

```xml
<intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="https" android:host="shop.example" android:pathPrefix="/product" />
</intent-filter>
```

The link reaches the app as an `intent`, and the screen reads it on launch.

```kotlin
// Platform code: ProductActivity
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class ProductActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val link = intent?.data
        if (link != null) openProduct(link.lastPathSegment)
    }
}
```

`onCreate` runs every time the screen is recreated, and the `intent` stays the same. This snippet claims no output or timing. The model builds this rule of the platform; the model is not the platform itself.

## The Link That Is Processed Again

In the model each link is a `Signal` and its key is the product address. When the screen is recreated, the same link is processed again; this is the second arrival of the same sequence. In the model, processing again means that the given sequences are appended to the end of the list as a copy.

```kotlin
// Redeliver.kt — hands over the same link once more when the screen is recreated
fun redeliver(sent: List<Signal>, again: List<Int>): List<Signal> =
    sent + again.map { seq -> sent.first { it.seq == seq } }
```

```kotlin
// Main.kt — when the screen is recreated, two links are processed twice
fun main() {
    val sent = listOf(
        Signal("product.7", 1),
        Signal("product.9", 2),
        Signal("product.12", 3),
        Signal("product.15", 4),
    )
    val received = redeliver(sent, again = listOf(2, 3))
    val arrivals = sent.map { Arrival("event-${it.seq}", it, received) }
    arrivals.forEach { it.line() }
    countArrivals(arrivals)
}
```

```text
event-1: key=product.7 | copies=1 -> once
event-2: key=product.9 | copies=2 -> duplicated
event-3: key=product.12 | copies=2 -> duplicated
event-4: key=product.15 | copies=1 -> once
once: 2
duplicated: 2
superseded: 0
lost: 0
```

This lesson's delivery is **duplicated**. Links 2 and 3 arrived as two copies; links 1 and 4 stayed with a single copy and arrived once. The user sees the product page opened twice. The rule does not see whether the copy came from a rotation or from another cause.

**SC1.** The model builds processing again as a copy appended to the end of the list. On the platform, when the screen is recreated, the same `intent` is read again within the screen's own flow, and exactly when the copy comes depends on the lifecycle of the screen; this is not in the model.

## A New Link for an Open Screen

If the user taps a second link while the app is open, the platform chooses one of two ways: it recreates the screen, or it gives the new link to the existing screen with a separate call. The launch mode of the screen decides which one is chosen. Recreating starts the lifecycle of the screen from the beginning, and the link is read at launch during this. Giving it to the existing screen passes the new link through a separate callback and does not touch the link read at launch.

This duality is one source of duplication. If the launch mode produces a new screen every time, the old screen stays on the back stack, and when the back button is pressed the old screen appears again with the link it still holds. The user sees the same product opened a second time. Which mode is chosen is the preference of the app; the measurement counts only the result, that is, how many copies were processed.

## The Handled Marker

The way to prevent duplication is to keep a marker that the link has been handled. If the marker is kept in saved state, then even if the screen is recreated, it is known at the next read that the link has been handled. This is how deduplication works: if the same sequence comes a second time, it is not processed.

```kotlin
// Handled.kt — skips a link that was already processed if the handled marker is kept
fun dedupe(arrived: List<Signal>, kept: Boolean, restartAt: Int): List<Signal> {
    val handled = mutableSetOf<Int>()
    val processed = mutableListOf<Signal>()
    arrived.forEachIndexed { i, s ->
        if (i == restartAt && !kept) handled.clear()
        if (handled.add(s.seq)) processed.add(s)
    }
    return processed
}
```

`kept` shows whether the marker is preserved even if the process restarts, and `restartAt` shows before which arrival the process restarted.

```kotlin
// Main.kt — if the handled marker is kept in saved state, the links arrive once
fun main() {
    val sent = (5..8).map { Signal("product.${it * 2}", it) }
    val arrived = redeliver(sent, again = listOf(6, 7))
    val received = dedupe(arrived, kept = true, restartAt = 4)
    val arrivals = sent.map { Arrival("event-${it.seq}", it, received) }
    arrivals.forEach { it.line() }
    countArrivals(arrivals)
}
```

```text
event-5: key=product.10 | copies=1 -> once
event-6: key=product.12 | copies=1 -> once
event-7: key=product.14 | copies=1 -> once
event-8: key=product.16 | copies=1 -> once
once: 4
duplicated: 0
superseded: 0
lost: 0
```

The same two reprocessings went through deduplication, and all four links arrived once. The processed list carries a single copy.

**SC2.** The model accepts the cost of keeping the marker as zero. On the platform the data written to saved state is limited, and how long the marker is kept is a decision of the app; this is not in the model.

## Shortcuts

The shortcut comes in three forms. The static shortcut ships with the app and is defined in a resource file; it does not change until the app is updated. The dynamic shortcut is added, updated and deleted by the app itself while it runs; when the app data is cleared, the dynamic shortcuts are deleted too. The pinned shortcut is placed on the home screen by the user; the app cannot remove this shortcut, it can only disable it.

The delivery-related side of the shortcut is its target. A shortcut carries an `intent` like a link and opens the same screen in the same way. For this reason the deduplication built for the deep link also holds for the shortcut: when the screen is recreated, the target of the shortcut is read again. The difference is that the shortcut is defined by the app, not by the user. A user who taps a removed shortcut sees a different error from a user who taps a link: the link never opens, while the shortcut opens but the target screen cannot be found.

## Link Types and Verification

The addresses that open the app fall into three types. A custom scheme address carries only a prefix the app defines, and any app can define the same prefix. If more than one app recognizes an address, the platform asks the user which to open it with. With a web address the same question arises between the browser and the app. The verified app link removes this question: the owner of the address declares, with a file in its domain, that it ties the identity of the app to the domain, and the platform hands the link directly to that app.

If verification fails, the platform does not hand the link to the app. The app sees no error; the link opens in the browser or in the chooser screen. The user taps a product link and sees a web page instead of the product page of the app. This is an invisible loss of delivery: for the app it looks as if the event was never sent, while for the user a sent action has produced no result.

In the model, verification holds back from the app all the links that belong to a certain domain. The domain is the first part of the key, and the unverified domain is a made-up value of the model.

```kotlin
// Verify.kt — does not hand links of an unverified domain to the app
fun verifiedOnly(sent: List<Signal>, unverified: String): List<Signal> =
    sent.filter { !it.key.startsWith("$unverified.") }
```

```kotlin
// Main.kt — links of the unverified domain do not reach the app
fun main() {
    val sent = listOf(
        Signal("example.product7", 13),
        Signal("old.product3", 14),
        Signal("example.product9", 15),
        Signal("old.product3", 16),
    )
    val received = verifiedOnly(sent, unverified = "old")
    println("received seqs: ${received.map { it.seq }}")

    val arrivals = sent.map { Arrival("event-${it.seq}", it, received) }
    arrivals.forEach { it.line() }
    countArrivals(arrivals)
}
```

```text
received seqs: [13, 15]
event-13: key=example.product7 | copies=1 -> once
event-14: key=old.product3 | copies=0 -> lost
event-15: key=example.product9 | copies=1 -> once
event-16: key=old.product3 | copies=0 -> lost
once: 2
duplicated: 0
superseded: 0
lost: 2
```

The two links of the verified domain arrived once. The two links of the unverified domain did not arrive, and because no link with the same key came after them, they were lost. Although the same address was sent twice, this time it was not counted as duplicated: duplicated is the number of copies that arrived, not the number of copies sent.

**SC3.** The model builds verification as all or nothing per domain. On the platform verification depends on the download of the domain file, the signature of the app and the path prefix, and partial verification is also possible; these are not in the model.

## Cold Start and Link Data

The link can find the app in two separate states. If the app is not running at all, the process starts from scratch, the screen is created and the link is read during launch. If the app is in the background, the process is alive, and the screen is either recreated or the new link is given to the existing screen. The handled marker helps a lot in the second state; in the first state the marker does not exist yet and the link arrives as a single copy.

The data that comes from the link comes from the outside world and is not trustworthy. If the last part of the address is a product number, the app must check its format before converting it to a number, and for a missing or invalid product it must show an error screen. From the delivery point of view this check concerns the content, not the copies, and stays outside the measurement. The rule counts that the link arrived; it does not look at whether the link that arrived is valid. An invalid link can arrive once and still fail to show the user a product.

## The Back Button and the Screen Stack

When a link opens the app directly on an inner screen, where the back button takes the user is a design question. If the app was not running and only the product screen is on the stack, the back button takes the user out of the app. If the app also places the home screen beneath the product screen, the user stays inside the app. This choice does not change delivery, but it makes duplication visible: if there are two product screens on the stack, the back button shows the first, and the user sees the same product a second time. The rule still counts these two screens either as one copy or as two copies; which one it is depends on how many times the link was processed.

## Common Misconceptions

**"A verified link always opens the app."** It opens it if verification succeeds. When verification fails or is not yet complete, the link goes to the browser. The app does not see this as an error, because no event has been sent to it.

**"If the same link came twice, the platform made a mistake."** The platform hands the link over once. The second copy comes from the recreation of the screen and from reading the link again at launch. The source is the lifecycle of the screen, not the platform.

**"The handled marker can be kept only in memory."** A marker in memory protects against rotation, but it disappears when the process is ended. In the measurement the difference was only the value of `kept`, and its cost was two duplicated links. The marker should be kept in saved state or in a durable store.

**"Deduplication increases the risk of loss."** Deduplication only does not process the same sequence a second time. It does not put links with different sequences in place of one another. Deduplication with the wrong key, however, counts two different products as the same and discards the second after the first; this is a loss caused by an app error, not a superseded one.

**"A shortcut and a link are the same thing."** Both carry an `intent`, but their owners differ. Someone from outside produces the link and the app must check its content; the app itself defines the shortcut and can trust its content more. As far as duplication goes, the two behave the same.

**"A duplicated link is harmless."** Opening a product screen twice is only a visual flaw; but if the link triggers a payment or a confirmation, the second copy is a second operation and is hard to undo.

## The Bounding Measurement

If the marker is kept only in memory, the memory is erased when the process is ended. The system can end the app in the background under memory pressure, and when the user returns the screen is recreated. Because the marker in memory is gone, the same link is processed again.

```kotlin
// Main.kt — if the marker is kept in memory, the link is duplicated again after the process ends
fun main() {
    val sent = (9..12).map { Signal("product.${it * 2}", it) }
    val arrived = redeliver(sent, again = listOf(10, 11))
    val received = dedupe(arrived, kept = false, restartAt = 4)
    val arrivals = sent.map { Arrival("event-${it.seq}", it, received) }
    arrivals.forEach { it.line() }
    countArrivals(arrivals)
}
```

```text
event-9: key=product.18 | copies=1 -> once
event-10: key=product.20 | copies=2 -> duplicated
event-11: key=product.22 | copies=2 -> duplicated
event-12: key=product.24 | copies=1 -> once
once: 2
duplicated: 2
superseded: 0
lost: 0
```

The input is the same as in the previous measurement, but because the marker stayed in memory, links 10 and 11 were processed again after the restart and became duplicated. The difference is only in the value of `kept`. A marker kept in memory protects against rotation; it does not protect against the process ending.

**SC4.** The model builds the end of the process at a single point. On the platform the end can happen at any moment, and the restoring of saved state varies with the type of ending; these are not in the model.

## Summary

- This lesson's delivery: duplicated. When the screen is recreated, the same deep link is processed twice.
- Of four links, two arrived as two copies: duplicated 2, once 2.
- When the handled marker was kept in saved state, all four links arrived once.
- When the marker was kept in memory, two links became duplicated again after the process ended: duplicated 2.
- The two links of the unverified domain did not reach the app: once 2, lost 2. The app does not see this as an error.
- The verified app link confirms that the owner of the address ties the domain to the app; the shortcut carries an `intent` that leads to a screen.

## Next Step

In this lesson the event came from outside and the same event was processed twice. In a widget the event is the update of a small view on the home screen, and if the view does not exist at all, the update finds no receiver. The next lesson, `Widgets`, measures the last update that is drawn when the widget appears and a widget that has been removed from the home screen.
