# 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
[
```
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 () 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]}}"
```