|
| 1 | +# Deep Link Guide |
| 2 | + |
| 3 | +This guide explains how to handle deep links with Navigation 3. It covers parsing |
| 4 | +intents into navigation keys and managing back stacks for proper "Back" and "Up" |
| 5 | +navigation behavior. |
| 6 | + |
| 7 | +## 1. Parsing an intent into a navigation key |
| 8 | + |
| 9 | +When your app receives a deep link, you need to convert the incoming `Intent` |
| 10 | +(specifically the data URI) into a `NavKey` that your navigation system can |
| 11 | +understand. |
| 12 | + |
| 13 | +This process involves four main steps: |
| 14 | + |
| 15 | +1. **Define Supported Deep Links**: Create `DeepLinkPattern` objects that |
| 16 | + describe the URLs your app supports. These patterns link a URI structure to |
| 17 | + a specific `NavKey` class. |
| 18 | +2. **Parse the Request**: Convert the incoming `Intent`'s data URI into a |
| 19 | + readable format, such as a `DeepLinkRequest`. |
| 20 | +3. **Match Request to Pattern**: Compare the incoming request against your list |
| 21 | + of supported patterns to find a match. |
| 22 | +4. **Decode to Key**: Use the match result to extract arguments and create an |
| 23 | + instance of the corresponding `NavKey`. |
| 24 | + |
| 25 | +### Example Implementation |
| 26 | + |
| 27 | +The `com.example.nav3recipes.deeplink.basic` package provides an example of |
| 28 | +this flow. |
| 29 | + |
| 30 | +**Step 1: Define Patterns** |
| 31 | + |
| 32 | +In a file (e.g., `MainActivity`), define a list of supported deeplink patterns. |
| 33 | +You can leverage the `kotlinx.serialization` library by annotating your Navigation keys with |
| 34 | +`@Serializable`, and then use the generated `Serializer` to map deep link arguments to a |
| 35 | +key argument. |
| 36 | + |
| 37 | +For example: |
| 38 | + |
| 39 | +```kotlin |
| 40 | +internal val deepLinkPatterns: List<DeepLinkPattern<out NavKey>> = listOf( |
| 41 | + // URL pattern with exact match: "https://www.nav3recipes.com/home" |
| 42 | + DeepLinkPattern(HomeKey.serializer(), (URL_HOME_EXACT).toUri()), |
| 43 | + |
| 44 | + // URL pattern with Path arguments: "https://www.nav3recipes.com/users/with/{filter}" |
| 45 | + DeepLinkPattern(UsersKey.serializer(), (URL_USERS_WITH_FILTER).toUri()), |
| 46 | + |
| 47 | + // URL pattern with Query arguments: "https://www.nav3recipes.com/users/search?{firstName}&{age}..." |
| 48 | + DeepLinkPattern(SearchKey.serializer(), (URL_SEARCH.toUri())), |
| 49 | +) |
| 50 | +``` |
| 51 | +The sample `DeepLinkPattern` class takes the defined URL pattern and serializer for the associated |
| 52 | +key, then maps each argument ("{...}") to a field in the key. Using the serializer, |
| 53 | +`DeepLinkPattern` stores the metadata for each argument such as its KType and its argument name. |
| 54 | + |
| 55 | + |
| 56 | +**Step 2: Parse the Request** |
| 57 | + |
| 58 | +In your Activity's `onCreate` method, you will first need to retrieve the data URI from the |
| 59 | +intent. Then you should parse that URI into a readable format. In this recipe, we use a |
| 60 | +`DeepLinkRequest` class which parses the URI's path segments and query parameters into a map. This |
| 61 | +map will make it easier to compare against the patterns defined in Step 1. |
| 62 | + |
| 63 | +**Step 3: Match Request to Pattern** |
| 64 | + |
| 65 | +Once the request is parsed, iterate through your list of `deepLinkPatterns`. For each pattern, |
| 66 | +use a matcher (e.g., `DeepLinkMatcher`) to check if the request's path and arguments align with |
| 67 | +the pattern's structure. The matcher should return a result object containing the matched arguments |
| 68 | +if successful, or null if not. |
| 69 | + |
| 70 | +**Step 4: Decode to Key** |
| 71 | + |
| 72 | +If a match is found, use the matched arguments to instantiate the corresponding `NavKey`. |
| 73 | +Since we used `kotlinx.serialization` in Step 1, we can leverage a custom Decoder (`KeyDecoder`) |
| 74 | +to decode the map of arguments directly into the strongly-typed key object. |
| 75 | + |
| 76 | +```kotlin |
| 77 | +override fun onCreate(savedInstanceState: Bundle?) { |
| 78 | + super.onCreate(savedInstanceState) |
| 79 | + |
| 80 | + val uri: Uri? = intent.data |
| 81 | + |
| 82 | + val key: NavKey = uri?.let { |
| 83 | + // Step 2: Parse request |
| 84 | + val request = DeepLinkRequest(uri) |
| 85 | + |
| 86 | + // Step 3: Find match |
| 87 | + val match = deepLinkPatterns.firstNotNullOfOrNull { pattern -> |
| 88 | + DeepLinkMatcher(request, pattern).match() |
| 89 | + } |
| 90 | + |
| 91 | + // Step 4: Decode to NavKey |
| 92 | + match?.let { |
| 93 | + KeyDecoder(match.args).decodeSerializableValue(match.serializer) |
| 94 | + } |
| 95 | + } ?: HomeKey // Fallback to home if no match or no URI |
| 96 | + |
| 97 | + setContent { |
| 98 | + val backStack = rememberNavBackStack(key) |
| 99 | + // ... setup NavDisplay |
| 100 | + } |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +For more details, refer to the [Basic Deep Link Recipe](../app/src/main/java/com/example/nav3recipes/deeplink/basic/BDEEPLINKREADME.md) |
| 105 | +and the `MainActivity` in `com.example.nav3recipes.deeplink.basic`. |
| 106 | + |
| 107 | +## 2. Building a synthetic backstack & managing a Task stack |
| 108 | + |
| 109 | +Handling deep links isn't just about showing the correct screen; it's also about |
| 110 | +ensuring the user has a natural navigation experience when they press "Back" or |
| 111 | +"Up". This often requires building a synthetic back stack. |
| 112 | + |
| 113 | +### The Problem: New Task vs. Existing Task |
| 114 | + |
| 115 | +When a user clicks a deep link, your activity might launch in a new Task or in |
| 116 | +an existing one. |
| 117 | + |
| 118 | +* **Existing Task**: If an app is already open, the deep link might just |
| 119 | + push a new screen onto the current stack. The "Back" button should return |
| 120 | + the user to where they were before the deep link. |
| 121 | +* **New Task**: If your app wasn't already open (or if the intent flags forced a new |
| 122 | + task), there is no existing backStack. Yet the "Back" button should conceptually go "up" to |
| 123 | + the parent of the current screen, rather than exiting the app. |
| 124 | + |
| 125 | +### Solution: Synthetic Back Stack |
| 126 | + |
| 127 | +A synthetic back stack is a manually constructed list of keys that represents |
| 128 | +the path the user *would* have taken from the root screen to the current screen. |
| 129 | + |
| 130 | +**Strategy** |
| 131 | + |
| 132 | +1. **Identify the Context**: Determine if you are in a new task or an existing |
| 133 | + one. Intent flags like `FLAG_ACTIVITY_NEW_TASK` would signal a new task. |
| 134 | +2. **Construct the Stack**: |
| 135 | + * **Up Button**: |
| 136 | + * In an **Existing Task**, you will need to restart the Activity in a new Task |
| 137 | + and build a backStack to ensure this parent exists |
| 138 | + * In a **New Task**, "Back" should behave like "Up" (go to the |
| 139 | + parent). You will need to build a backStack to ensure this parent exists. |
| 140 | + * **Back Button**: |
| 141 | + * In an **Existing Task**, "Back" goes to the previous screen (the one |
| 142 | + that triggered the deep link). |
| 143 | + * In a **New Task**, "Back" should behave like "Up" (go to the |
| 144 | + parent). You will need to build a backStack to ensure this parent exists. |
| 145 | + |
| 146 | +### Task & backStack illustration |
| 147 | + |
| 148 | +**Existing Task** |
| 149 | + |
| 150 | +| Task | Target | Synthetic backStack | |
| 151 | +|-------------|-----------------------------|-------------------------------------------------| |
| 152 | +| Up Button | Deep linked Screen's Parent | Restart Activity on new Task & build backStack | |
| 153 | +| Back Button | Screen before deep linking | None | |
| 154 | + |
| 155 | +**New Task** |
| 156 | + |
| 157 | +| Task | Target | Synthetic backStack | |
| 158 | +|-------------|-----------------------------|------------------------------------------------| |
| 159 | +| Up Button | Deep linked Screen's Parent | Build backStack on Activity creation | |
| 160 | +| Back Button | Deep linked Screen's Parent | Build backStack on Activity creation | |
| 161 | + |
| 162 | +**Implementation Tips** |
| 163 | + |
| 164 | +* **Restarting for consistency**: If you want to enforce a specific back stack |
| 165 | + structure (like always having the app's home screen at the bottom), you |
| 166 | + might need to restart the Activity with `Intent.FLAG_ACTIVITY_NEW_TASK` and |
| 167 | + `Intent.FLAG_ACTIVITY_CLEAR_TASK` when handling a deep link, then build the |
| 168 | + full stack manually. |
| 169 | + |
| 170 | +For a comprehensive demonstration of simulating "App A" deep linking into "App |
| 171 | +B" and managing these stacks, see the [Advanced Deep Link Recipe](../app/src/main/java/com/example/nav3recipes/deeplink/advanced/ADEEPLINKREADME.md) |
| 172 | +and the module `com.example.nav3recipes.deeplink.app`. |
0 commit comments