0% found this document useful (0 votes)
2 views28 pages

Deep Link Basic Recipe

This document outlines a recipe for parsing deep link URLs in an Android application using two activities: `CreateDeepLinkActivity` for constructing deep links and `MainActivity` for handling them. It demonstrates various deep link types, including exact URLs, path arguments, and query arguments, and provides a structured approach to parsing and matching these links using Kotlin. The recipe is organized into three main packages for activities, UI code, and utility functions for deep link processing.

Uploaded by

mona02
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views28 pages

Deep Link Basic Recipe

This document outlines a recipe for parsing deep link URLs in an Android application using two activities: `CreateDeepLinkActivity` for constructing deep links and `MainActivity` for handling them. It demonstrates various deep link types, including exact URLs, path arguments, and query arguments, and provides a structured approach to parsing and matching these links using Kotlin. The recipe is organized into three main packages for activities, UI code, and utility functions for deep link processing.

Uploaded by

mona02
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

# Deep Link Basic Recipe

This recipe demonstrates how to parse a deep link URL from an Android Intent into a
Navigation key.

## How it works

It consists of two activities - `CreateDeepLinkActivity` to construct and trigger the deeplink


request, and the `MainActivity` to show how an app can handle that request.

## Demonstrated forms of deeplink

The `MainActivity` has several backStack keys to demonstrate different types of supported
deeplinks:

1. `HomeKey` - deeplink with an exact url (no deeplink arguments)

2. `UsersKey` - deeplink with path arguments

3. `SearchKey` - deeplink with query arguments

See `[Link]` for the actual url pattern of each.

## Recipe structure

This recipe consists of three main packages:

1. `[Link]` - Contains the two activities

2. `[Link]` - Contains the activity UI code, i.e. global string variables, deeplink
URLs etc
3. `[Link]` - Contains the classes and helper methods to parse and match the
deeplinks

[![]([Link] Explore View the full


recipe on GitHub.]([Link]
recipes/tree/main/app/src/main/java/com/example/nav3recipes/deeplink/basic)

```

package [Link]

import [Link]

import [Link].STRING_LITERAL_FILTER

import [Link].STRING_LITERAL_HOME

import [Link].STRING_LITERAL_SEARCH

import [Link].STRING_LITERAL_USERS

import [Link]

internal interface NavRecipeKey: NavKey {

val name: String

@Serializable

internal object HomeKey: NavRecipeKey {

override val name: String = STRING_LITERAL_HOME

@Serializable

internal data class UsersKey(

val filter: String,


): NavRecipeKey {

override val name: String = STRING_LITERAL_USERS

companion object {

const val FILTER_KEY = STRING_LITERAL_FILTER

const val FILTER_OPTION_RECENTLY_ADDED = "recentlyAdded"

const val FILTER_OPTION_ALL = "all"

@Serializable

internal data class SearchKey(

val firstName: String? = null,

val ageMin: Int? = null,

val ageMax: Int? = null,

val location: String? = null,

): NavRecipeKey {

override val name: String = STRING_LITERAL_SEARCH

```

```

package [Link]

import [Link]

import [Link]

import [Link]

import [Link]

import [Link]
import [Link]

import [Link]

import [Link]

import [Link]

import [Link]

import [Link]

import [Link]

import [Link]

import [Link]

import [Link]

import [Link]

import [Link].URL_HOME_EXACT

import [Link].URL_SEARCH

import [Link].URL_USERS_WITH_FILTER

import [Link]

import [Link]

import [Link].LIST_USERS

/**

* Parses a target deeplink into a NavKey. There are several crucial steps involved:

* STEP [Link] supported deeplinks (URLs that can be deeplinked into) into a readily
readable

* format (see [DeepLinkPattern])

* STEP 2. Parse the requested deeplink into a readily readable, format (see
[DeepLinkRequest])

* **note** the parsed requested deeplink and parsed supported deeplinks should be
cohesive with each

* other to facilitate comparison and finding a match


* STEP 3. Compare the requested deeplink target with supported deeplinks in order to find
a match

* (see [DeepLinkMatchResult]). The match result's format should enable conversion from
result

* to backstack key, regardless of what the conversion method may be.

* STEP 4. Associate the match results with the correct backstack key

* This recipes provides an example for each of the above steps by way of
[Link].

* **This recipe is designed to focus on parsing an intent into a key, and therefore these
additional

* deeplink considerations are not included in this scope**

* - Create synthetic backStack

* - Multi-modular setup

* - DI

* - Managing TaskStack

* - Up button ves Back Button

*/

class MainActivity : ComponentActivity() {

/** STEP 1. Parse supported deeplinks */

// internal so that landing activity can link to this in the kdocs

internal val deepLinkPatterns: List<DeepLinkPattern<out NavKey>> = listOf(

// "[Link]

DeepLinkPattern([Link](), (URL_HOME_EXACT).toUri()),

// "[Link]

DeepLinkPattern([Link](), (URL_USERS_WITH_FILTER).toUri()),

// "[Link]
DeepLinkPattern([Link](), (URL_SEARCH.toUri())),

override fun onCreate(savedInstanceState: Bundle?) {

[Link](savedInstanceState)

// retrieve the target Uri

val uri: Uri? = [Link]

// associate the target with the correct backstack key

val key: NavKey = uri?.let {

/** STEP 2. Parse requested deeplink */

val request = DeepLinkRequest(uri)

/** STEP 3. Compared requested with supported deeplink to find match*/

val match = [Link] { pattern ->

DeepLinkMatcher(request, pattern).match()

/** STEP 4. If match is found, associate match to the correct key*/

match?.let {

//leverage [Link]'s Decoder to decode

// match result into a backstack key

KeyDecoder([Link])

.decodeSerializableValue([Link])

} ?: HomeKey // fallback if [Link] is null or match is not found

/**

* Then pass starting key to backstack

*/
setContent {

val backStack: NavBackStack<NavKey> = rememberNavBackStack(key)

NavDisplay(

backStack = backStack,

onBack = { [Link]() },

entryProvider = entryProvider {

entry<HomeKey> { key ->

EntryScreen([Link]) {

TextContent("<matches exact url>")

entry<UsersKey> { key ->

EntryScreen("${[Link]} : ${[Link]}") {

TextContent("<matches path argument>")

val list = when {

[Link]() -> LIST_USERS

[Link] == UsersKey.FILTER_OPTION_ALL -> LIST_USERS

else -> LIST_USERS.take(5)

FriendsList(list)

entry<SearchKey> { search ->

EntryScreen([Link]) {

TextContent("<matches query parameters, if any>")

val matchingUsers = LIST_USERS.filter { user ->

([Link] == null || [Link] == [Link]) &&

([Link] == null || [Link] == [Link]) &&


([Link] == null || [Link] >= [Link]) &&

([Link] == null || [Link] <= [Link])

FriendsList(matchingUsers)

```

```

package [Link]

import [Link]

import [Link]

import [Link]

import [Link]

import [Link]

import [Link]

import [Link]

import [Link]

import [Link]

import [Link]

import [Link]

import [Link]
import [Link]

import [Link]

import [Link]

import [Link].FIRST_NAME_JOHN

import [Link].FIRST_NAME_JULIE

import [Link].FIRST_NAME_MARY

import [Link].FIRST_NAME_TOM

import [Link].LOCATION_BC

import [Link].LOCATION_BR

import [Link].LOCATION_CA

import [Link].LOCATION_US

import [Link]

import [Link]

import [Link].PATH_BASE

import [Link].PATH_INCLUDE

import [Link].PATH_SEARCH

import [Link].STRING_LITERAL_HOME

import [Link]

/**

* This activity allows the user to create a deep link and make a request with it.

* **HOW THIS RECIPE WORKS** it consists of two activities - [CreateDeepLinkActivity] to


construct

* and trigger the deeplink request, and the [MainActivity] to show how an app can handle

* that request.

* **DEMONSTRATED FORMS OF DEEPLINK** The [MainActivity] has a several backStack


keys to
* demonstrate different types of supported deeplinks:

* 1. [HomeKey] - deeplink with an exact url (no deeplink arguments)

* 2. [UsersKey] - deeplink with path arguments

* 3. [SearchKey] - deeplink with query arguments

* See [[Link]] for the actual url pattern of each.

* **RECIPE STRUCTURE** This recipe consists of three main packages:

* 1. [Link] - Contains the two activities

* 2. [Link] - Contains the activity UI code, i.e. global string variables, deeplink
URLs etc

* 3. [Link] - Contains the classes and helper methods to parse and match

* the deeplinks

* See [MainActivity] for how the requested deeplink is handled.

*/

class CreateDeepLinkActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {

[Link](savedInstanceState)

setContent {

/**

* UI for deeplink sandbox

*/

EntryScreen("Sandbox - Build Your Deeplink") {

TextContent("Base url:\n${PATH_BASE}/")

var showFilterOptions by remember { mutableStateOf(false) }

val selectedPath = remember {


mutableStateOf(MENU_OPTIONS_PATH[KEY_PATH]?.first()) }
var showQueryOptions by remember { mutableStateOf(false) }

var selectedFilter by remember { mutableStateOf("") }

val selectedSearchQuery = remember { mutableStateMapOf<String, String>() }

// manage path options

MenuDropDown(

menuOptions = MENU_OPTIONS_PATH,

) { _, selection ->

[Link] = selection

when (selection) {

PATH_SEARCH -> {

showQueryOptions = true

showFilterOptions = false

PATH_INCLUDE -> {

showQueryOptions = false

showFilterOptions = true

else -> {

showQueryOptions = false

showFilterOptions = false

// manage path filter options, reset state if menu is closed


LaunchedEffect(showFilterOptions) {

selectedFilter = if (showFilterOptions) {

MENU_OPTIONS_FILTER.[Link]().first()

} else {

""

if (showFilterOptions) {

MenuDropDown(

menuOptions = MENU_OPTIONS_FILTER,

) { _, selected ->

selectedFilter = selected

// manage query options, reset state if menu is closed

LaunchedEffect(showQueryOptions) {

if (showQueryOptions) {

val initEntry = MENU_OPTIONS_SEARCH.[Link]()

selectedSearchQuery[[Link]] = [Link]()

} else {

[Link]()

if (showQueryOptions) {

MenuTextInput(

menuLabels = MENU_LABELS_SEARCH,

) { label, selected ->


selectedSearchQuery[label] = selected

MenuDropDown(

menuOptions = MENU_OPTIONS_SEARCH,

) { label, selected ->

selectedSearchQuery[label] = selected

// form final deeplink url

val arguments = when ([Link]) {

PATH_INCLUDE -> "/${selectedFilter}"

PATH_SEARCH -> {

buildString {

[Link] { entry ->

if ([Link]()) {

val prefix = if (isEmpty()) "?" else "&"

append("$prefix${[Link]}=${[Link]}")

else -> ""

val finalUrl = "${PATH_BASE}/${[Link]}$arguments"

TextContent("Final url:\n$finalUrl")

// deeplink to target
PaddedButton("Deeplink Away!", onClick = dropUnlessResumed {

val intent = Intent(

this@CreateDeepLinkActivity,

MainActivity::[Link]

// start activity with the url

[Link] = [Link]()

startActivity(intent)

})

private const val KEY_PATH = "path"

private val MENU_OPTIONS_PATH = mapOf(

KEY_PATH to listOf(

STRING_LITERAL_HOME,

PATH_INCLUDE,

PATH_SEARCH,

),

private val MENU_OPTIONS_FILTER = mapOf(

UsersKey.FILTER_KEY to listOf(UsersKey.FILTER_OPTION_RECENTLY_ADDED,
UsersKey.FILTER_OPTION_ALL),

)
private val MENU_OPTIONS_SEARCH = mapOf(

SearchKey::[Link] to listOf(

EMPTY,

FIRST_NAME_JOHN,

FIRST_NAME_TOM,

FIRST_NAME_MARY,

FIRST_NAME_JULIE

),

SearchKey::[Link] to listOf(EMPTY, LOCATION_CA, LOCATION_BC, LOCATION_BR,


LOCATION_US)

private val MENU_LABELS_SEARCH = listOf(SearchKey::[Link],


SearchKey::[Link])

```

```

package [Link]

import [Link]

import [Link]

import [Link]

import [Link]

import [Link]

import [Link]

/**

* Decodes the list of arguments into a a back stack key


*

* **IMPORTANT** This decoder assumes that all argument types are Primitives.

*/

@OptIn(ExperimentalSerializationApi::class)

internal class KeyDecoder(

private val arguments: Map<String, Any>,

) : AbstractDecoder() {

override val serializersModule: SerializersModule = EmptySerializersModule()

private var elementIndex: Int = -1

private var elementName: String = ""

/**

* Decodes the index of the next element to be decoded. Index represents a position of
the

* current element in the [descriptor] that can be found with


[descriptor].getElementIndex.

* The returned index will trigger deserializer to call [decodeValue] on the argument at
that

* index.

* The decoder continually calls this method to process the next available argument until
this

* method returns [CompositeDecoder.DECODE_DONE], which indicates that there are no


more

* arguments to decode.

* This method should sequentially return the element index for every element that has its
value
* available within [arguments].

*/

override fun decodeElementIndex(descriptor: SerialDescriptor): Int {

var currentIndex = elementIndex

while (true) {

// proceed to next element

currentIndex++

// if we have reached the end, let decoder know there are not more arguments to
decode

if (currentIndex >= [Link]) return


CompositeDecoder.DECODE_DONE

val currentName = [Link](currentIndex)

// Check if bundle has argument value. If so, we tell decoder to process

// currentIndex. Otherwise, we skip this index and proceed to next index.

if ([Link](currentName)) {

elementIndex = currentIndex

elementName = currentName

return elementIndex

/**

* Returns argument value from the [arguments] for the argument at the index returned
by

* [decodeElementIndex]

*/

override fun decodeValue(): Any {

val arg = arguments[elementName]


checkNotNull(arg) { "Unexpected null value for non-nullable argument $elementName"
}

return arg

override fun decodeNull(): Nothing? = null

// we want to know if it is not null, so its !isNull

override fun decodeNotNullMark(): Boolean = arguments[elementName] != null

```

```

package [Link]

import [Link]

/**

* Parse the requested Uri and store it in a easily readable format

* @param uri the target deeplink uri to link to

*/

internal class DeepLinkRequest(

val uri: Uri

){

/**

* A list of path segments

*/
val pathSegments: List<String> = [Link]

/**

* A map of query name to query value

*/

val queries = buildMap {

[Link] { argName ->

this[argName] = [Link](argName)!!

// TODO add parsing for other Uri components, i.e. fragments, mimeType, action

```

````

package [Link]

import [Link]

import [Link]

import [Link]

import [Link]

import [Link]

import [Link]

import [Link]

/**

* Parse a supported deeplink and stores its metadata as a easily readable format
*

* The following notes applies specifically to this particular sample implementation:

* The supported deeplink is expected to be built from a serializable backstack key [T] that

* supports deeplink. This means that if this deeplink contains any arguments (path or
query),

* the argument name must match any of [T] member field name.

* One [DeepLinkPattern] should be created for each supported deeplink. This means if [T]

* supports two deeplink patterns:

* ```

* val deeplink1 = [Link]/home

* val deeplink2 = [Link]/profile/{userId}

* ```

* Then two [DeepLinkPattern] should be created

* ```

* val parsedDeeplink1 = DeepLinkPattern([Link](), deeplink1)

* val parsedDeeplink2 = DeepLinkPattern([Link](), deeplink2)

* ```

* This implementation assumes a few things:

* 1. all path arguments are required/non-nullable - partial path matches will be considered
a non-match

* 2. all query arguments are optional by way of nullable/has default value

* @param T the backstack key type that supports the deeplinking of [uriPattern]

* @param serializer the serializer of [T]

* @param uriPattern the supported deeplink's uri pattern, i.e. "[Link]/home/{pathArg}"

*/
internal class DeepLinkPattern<T : NavKey>(

val serializer: KSerializer<T>,

val uriPattern: Uri

){

/**

* Help differentiate if a path segment is an argument or a static value

*/

private val regexPatternFillIn = Regex("\\{(.+?)\\}")

// TODO make these lazy

/**

* parse the path into a list of [PathSegment]

* order matters here - path segments need to match in value and order when matching

* requested deeplink to supported deeplink

*/

val pathSegments: List<PathSegment> = buildList {

[Link] { segment ->

// first, check if it is a path arg

var result = [Link](segment)

if (result != null) {

// if so, extract the path arg name (the string value within the curly braces)

val argName = [Link][1]!!.value

// from [T], read the primitive type of this argument to get the correct type parser

val elementIndex = [Link](argName)

if (elementIndex == CompositeDecoder.UNKNOWN_NAME) {

throw IllegalArgumentException(
"Path parameter '{$argName}' defined in the DeepLink $uriPattern does not
exist in the Serializable class '${[Link]}'."

val elementDescriptor = [Link](elementIndex)

// finally, add the arg name and its respective type parser to the map

add(PathSegment(argName, true, getTypeParser([Link])))

} else {

// if its not a path arg, then its just a static string path segment

add(PathSegment(segment, false, getTypeParser([Link])))

/**

* Parse supported queries into a map of queryParameterNames to [TypeParser]

* This will be used later on to parse a provided query value into the correct KType

*/

val queryValueParsers: Map<String, TypeParser> = buildMap {

[Link] { paramName ->

val elementIndex = [Link](paramName)

// Ignore static query parameters that are not in the Serializable class

if (elementIndex != CompositeDecoder.UNKNOWN_NAME) {

val elementDescriptor = [Link](elementIndex)

this[paramName] = getTypeParser([Link])

}
}

/**

* Metadata about a supported path segment

*/

class PathSegment(

val stringValue: String,

val isParamArg: Boolean,

val typeParser: TypeParser

/**

* Parses a String into a Serializable Primitive

*/

private typealias TypeParser = (String) -> Serializable

private fun getTypeParser(kind: SerialKind): TypeParser {

return when (kind) {

[Link] -> Any::toString

[Link] -> String::toInt

[Link] -> String::toBoolean

[Link] -> String::toByte

[Link] -> String::toCharArray

[Link] -> String::toDouble

[Link] -> String::toFloat

[Link] -> String::toLong


[Link] -> String::toShort

else -> throw IllegalArgumentException(

"Unsupported argument type of SerialKind:$kind. The argument type must be a


Primitive."

````

```

package [Link]

import [Link]

import [Link]

import [Link]

internal class DeepLinkMatcher<T : NavKey>(

val request: DeepLinkRequest,

val deepLinkPattern: DeepLinkPattern<T>

){

/**

* Match a [DeepLinkRequest] to a [DeepLinkPattern].

* Returns a [DeepLinkMatchResult] if this matches the pattern, returns null otherwise

*/

fun match(): DeepLinkMatchResult<T>? {

if ([Link] != [Link]) return null

if (![Link]([Link], ignoreCase =
true)) return null
if ([Link] != [Link]) return null

// exact match (url does not contain any arguments)

if ([Link] == [Link])

return DeepLinkMatchResult([Link], mapOf())

val args = mutableMapOf<String, Any>()

// match the path

[Link]

.asSequence()

// zip to compare the two objects side by side, order matters here so we

// need to make sure the compared segments are at the same position within the url

.zip([Link]())

.forEach { it ->

// retrieve the two path segments to compare

val requestedSegment = [Link]

val candidateSegment = [Link]

// if the potential match expects a path arg for this segment, try to parse the

// requested segment into the expected type

if ([Link]) {

val parsedValue = try {

[Link](requestedSegment)

} catch (e: IllegalArgumentException) {

Log.e(TAG_LOG_ERROR, "Failed to parse path value:[$requestedSegment].", e)

return null

args[[Link]] = parsedValue

} else if(requestedSegment != [Link]){

// if it's path arg is not the expected type, its not a match
return null

// match queries (if any)

[Link] { query ->

val name = [Link]

// If the pattern does not define this query parameter, ignore it.

// This prevents a NullPointerException.

val queryStringParser = [Link][name]?: return@forEach

val queryParsedValue = try {

[Link]([Link])

} catch (e: IllegalArgumentException) {

Log.e(TAG_LOG_ERROR, "Failed to parse query name:[$name]


value:[${[Link]}].", e)

return null

args[name] = queryParsedValue

// provide the serializer of the matching key and map of arg names to parsed arg values

return DeepLinkMatchResult([Link], args)

/**

* Created when a requested deeplink matches with a supported deeplink

*
* @param [T] the backstack key associated with the deeplink that matched with the
requested deeplink

* @param serializer serializer for [T]

* @param args The map of argument name to argument value. The value is expected to
have already

* been parsed from the raw url string back into its proper KType as declared in [T].

* Includes arguments for all parts of the uri - path, query, etc.

* */

internal data class DeepLinkMatchResult<T : NavKey>(

val serializer: KSerializer<T>,

val args: Map<String, Any>

const val TAG_LOG_ERROR = "Nav3RecipesDeepLink"

```

```

package [Link]

import [Link]

/**

* String resources

*/

internal const val STRING_LITERAL_FILTER = "filter"

internal const val STRING_LITERAL_HOME = "home"

internal const val STRING_LITERAL_USERS = "users"

internal const val STRING_LITERAL_SEARCH = "search"

internal const val STRING_LITERAL_INCLUDE = "include"


internal const val PATH_BASE = "[Link]

internal const val PATH_INCLUDE = "$STRING_LITERAL_USERS/$STRING_LITERAL_INCLUDE"

internal const val PATH_SEARCH = "$STRING_LITERAL_USERS/$STRING_LITERAL_SEARCH"

internal const val URL_HOME_EXACT = "$PATH_BASE/$STRING_LITERAL_HOME"

internal const val URL_USERS_WITH_FILTER =


"$PATH_BASE/$PATH_INCLUDE/{$STRING_LITERAL_FILTER}"

internal val URL_SEARCH = "$PATH_BASE/$PATH_SEARCH" +

"?${SearchKey::[Link]}={${SearchKey::[Link]}}" +

"&${SearchKey::[Link]}={${SearchKey::[Link]}}" +

"&${SearchKey::[Link]}={${SearchKey::[Link]}}" +

"&${SearchKey::[Link]}={${SearchKey::[Link]}}"

```

You might also like