diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index bcc9d73..d5f8e8e 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -72,7 +72,6 @@ dependencies {
implementation(libs.androidx.adaptive.layout)
implementation(libs.androidx.material3.navigation3)
-
implementation(libs.kotlinx.serialization.core)
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.navigation3.runtime)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d749e4c..d75d220 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -37,7 +37,6 @@
android:theme="@style/Theme.Nav3Recipes">
-
@@ -141,6 +140,48 @@
android:name=".migration.step7.Step7MigrationActivity"
android:exported="true"
android:theme="@style/Theme.Nav3Recipes"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/example/nav3recipes/RecipePickerActivity.kt b/app/src/main/java/com/example/nav3recipes/RecipePickerActivity.kt
index 9532983..9fcc6bd 100644
--- a/app/src/main/java/com/example/nav3recipes/RecipePickerActivity.kt
+++ b/app/src/main/java/com/example/nav3recipes/RecipePickerActivity.kt
@@ -31,6 +31,7 @@ import com.example.nav3recipes.basicdsl.BasicDslActivity
import com.example.nav3recipes.basicsaveable.BasicSaveableActivity
import com.example.nav3recipes.commonui.CommonUiActivity
import com.example.nav3recipes.conditional.ConditionalActivity
+import com.example.nav3recipes.deeplink.basic.CreateDeepLinkActivity
import com.example.nav3recipes.dialog.DialogActivity
import com.example.nav3recipes.modular.hilt.ModularActivity
import com.example.nav3recipes.passingarguments.viewmodels.basic.BasicViewModelsActivity
@@ -81,6 +82,9 @@ private val recipes = listOf(
Heading("Returning Results"),
Recipe("Return result as Event", ResultEventActivity::class.java),
Recipe("Return result as State", ResultStateActivity::class.java),
+
+ Heading("Deeplink"),
+ Recipe("Parse Intent", CreateDeepLinkActivity::class.java),
)
class RecipePickerActivity : ComponentActivity() {
diff --git a/app/src/main/java/com/example/nav3recipes/deeplink/basic/CreateDeepLinkActivity.kt b/app/src/main/java/com/example/nav3recipes/deeplink/basic/CreateDeepLinkActivity.kt
new file mode 100644
index 0000000..94a1a43
--- /dev/null
+++ b/app/src/main/java/com/example/nav3recipes/deeplink/basic/CreateDeepLinkActivity.kt
@@ -0,0 +1,189 @@
+package com.example.nav3recipes.deeplink.basic
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateMapOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import com.example.nav3recipes.deeplink.basic.ui.DeepLinkButton
+import com.example.nav3recipes.deeplink.basic.ui.EMPTY
+import com.example.nav3recipes.deeplink.basic.ui.EntryScreen
+import com.example.nav3recipes.deeplink.basic.ui.FIRST_NAME_JOHN
+import com.example.nav3recipes.deeplink.basic.ui.FIRST_NAME_JULIE
+import com.example.nav3recipes.deeplink.basic.ui.FIRST_NAME_MARY
+import com.example.nav3recipes.deeplink.basic.ui.FIRST_NAME_TOM
+import com.example.nav3recipes.deeplink.basic.ui.LOCATION_BC
+import com.example.nav3recipes.deeplink.basic.ui.LOCATION_BR
+import com.example.nav3recipes.deeplink.basic.ui.LOCATION_CA
+import com.example.nav3recipes.deeplink.basic.ui.LOCATION_US
+import com.example.nav3recipes.deeplink.basic.ui.MenuDropDown
+import com.example.nav3recipes.deeplink.basic.ui.MenuTextInput
+import com.example.nav3recipes.deeplink.basic.ui.PATH_BASE
+import com.example.nav3recipes.deeplink.basic.ui.PATH_INCLUDE
+import com.example.nav3recipes.deeplink.basic.ui.PATH_SEARCH
+import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_HOME
+import com.example.nav3recipes.deeplink.basic.ui.SearchKey
+import com.example.nav3recipes.deeplink.basic.ui.TextContent
+import com.example.nav3recipes.deeplink.basic.ui.HomeKey
+import com.example.nav3recipes.deeplink.basic.ui.UsersKey
+
+/**
+ * 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 [MainActivity.deepLinkPatterns] for the actual url pattern of each.
+ *
+ * **RECIPE STRUCTURE** This recipe consists of three main packages:
+ * 1. basic.deeplink - Contains the two activities
+ * 2. basic.deeplink.ui - Contains the activity UI code, i.e. Screens, global string variables etc
+ * 3. basic.deeplink.deeplinkutil - 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?) {
+ super.onCreate(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() }
+
+ // manage path options
+ MenuDropDown(
+ menuOptions = MENU_OPTIONS_PATH,
+ ) { _, selection ->
+ selectedPath.value = 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.values.first().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.entries.first()
+ selectedSearchQuery[initEntry.key] = initEntry.value.first()
+ } else {
+ selectedSearchQuery.clear()
+ }
+ }
+ 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 (selectedPath.value) {
+ PATH_INCLUDE -> "/${selectedFilter}"
+ PATH_SEARCH -> {
+ buildString {
+ selectedSearchQuery.forEach { entry ->
+ if (entry.value.isNotEmpty()) {
+ val prefix = if (isEmpty()) "?" else "&"
+ append("$prefix${entry.key}=${entry.value}")
+ }
+ }
+ }
+ }
+
+ else -> ""
+ }
+ val finalUrl = "${PATH_BASE}/${selectedPath.value}$arguments"
+ TextContent("Final url:\n$finalUrl")
+ // deeplink to target
+ DeepLinkButton(
+ context = this@CreateDeepLinkActivity,
+ targetActivity = MainActivity::class.java,
+ deepLinkUrl = finalUrl
+ )
+ }
+ }
+ }
+}
+
+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::firstName.name to listOf(
+ EMPTY,
+ FIRST_NAME_JOHN,
+ FIRST_NAME_TOM,
+ FIRST_NAME_MARY,
+ FIRST_NAME_JULIE
+ ),
+ SearchKey::location.name to listOf(EMPTY, LOCATION_CA, LOCATION_BC, LOCATION_BR, LOCATION_US)
+)
+
+private val MENU_LABELS_SEARCH = listOf(SearchKey::ageMin.name, SearchKey::ageMax.name)
+
+
diff --git a/app/src/main/java/com/example/nav3recipes/deeplink/basic/MainActivity.kt b/app/src/main/java/com/example/nav3recipes/deeplink/basic/MainActivity.kt
new file mode 100644
index 0000000..c3a6e7a
--- /dev/null
+++ b/app/src/main/java/com/example/nav3recipes/deeplink/basic/MainActivity.kt
@@ -0,0 +1,128 @@
+package com.example.nav3recipes.deeplink.basic
+
+import android.net.Uri
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.core.net.toUri
+import androidx.navigation3.runtime.NavBackStack
+import androidx.navigation3.runtime.NavKey
+import androidx.navigation3.runtime.entryProvider
+import androidx.navigation3.runtime.rememberNavBackStack
+import androidx.navigation3.ui.NavDisplay
+import com.example.nav3recipes.deeplink.basic.deeplinkutil.DeepLinkMatcher
+import com.example.nav3recipes.deeplink.basic.deeplinkutil.DeepLinkPattern
+import com.example.nav3recipes.deeplink.basic.deeplinkutil.DeepLinkRequest
+import com.example.nav3recipes.deeplink.basic.deeplinkutil.DeepLinkMatchResult
+import com.example.nav3recipes.deeplink.basic.deeplinkutil.KeyDecoder
+import com.example.nav3recipes.deeplink.basic.ui.EntryScreen
+import com.example.nav3recipes.deeplink.basic.ui.FriendsList
+import com.example.nav3recipes.deeplink.basic.ui.HomeKey
+import com.example.nav3recipes.deeplink.basic.ui.LIST_USERS
+import com.example.nav3recipes.deeplink.basic.ui.SearchKey
+import com.example.nav3recipes.deeplink.basic.ui.TextContent
+import com.example.nav3recipes.deeplink.basic.ui.URL_HOME_EXACT
+import com.example.nav3recipes.deeplink.basic.ui.URL_SEARCH
+import com.example.nav3recipes.deeplink.basic.ui.URL_USERS_WITH_FILTER
+import com.example.nav3recipes.deeplink.basic.ui.UsersKey
+
+/**
+ * Parses a target deeplink into a NavKey. There are several crucial steps involved:
+ *
+ * STEP 1.Parse 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 kotlinx.serialization.
+ *
+ * **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> = listOf(
+ // "https://www.nav3recipes.com/home"
+ DeepLinkPattern(HomeKey.serializer(), (URL_HOME_EXACT).toUri()),
+ // "https://www.nav3recipes.com/users/with/{filter}"
+ DeepLinkPattern(UsersKey.serializer(), (URL_USERS_WITH_FILTER).toUri()),
+ // "https://www.nav3recipes.com/users/search?{firstName}&{age}&{location}"
+ DeepLinkPattern(SearchKey.serializer(), (URL_SEARCH.toUri())),
+ )
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // retrieve the target Uri
+ val uri: Uri? = intent.data
+ // 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 = deepLinkPatterns.firstNotNullOfOrNull { pattern ->
+ DeepLinkMatcher(request, pattern).match()
+ }
+ /** STEP 4. If match is found, associate match to the correct key*/
+ match?.let {
+ //leverage kotlinx.serialization's Decoder to decode
+ // match result into a backstack key
+ KeyDecoder(match.args)
+ .decodeSerializableValue(match.serializer)
+ }
+ } ?: HomeKey // fallback if intent.uri is null or match is not found
+
+ /**
+ * Then pass starting key to backstack
+ */
+ setContent {
+ val backStack: NavBackStack = rememberNavBackStack(key)
+ NavDisplay(
+ backStack = backStack,
+ onBack = { backStack.removeLastOrNull() },
+ entryProvider = entryProvider {
+ entry { key ->
+ EntryScreen(key.name) {
+ TextContent("")
+ }
+ }
+ entry { key ->
+ EntryScreen("${key.name} : ${key.filter}") {
+ TextContent("")
+ val list = when {
+ key.filter.isEmpty() -> LIST_USERS
+ key.filter == UsersKey.FILTER_OPTION_ALL -> LIST_USERS
+ else -> LIST_USERS.take(5)
+ }
+ FriendsList(list)
+ }
+ }
+ entry { search ->
+ EntryScreen(search.name) {
+ TextContent("")
+ val matchingUsers = LIST_USERS.filter { user ->
+ (search.firstName == null || user.firstName == search.firstName) &&
+ (search.location == null || user.location == search.location) &&
+ (search.ageMin == null || user.age >= search.ageMin) &&
+ (search.ageMax == null || user.age <= search.ageMax)
+ }
+ FriendsList(matchingUsers)
+ }
+ }
+ }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/nav3recipes/deeplink/basic/deeplinkutil/DeepLinkMatcher.kt b/app/src/main/java/com/example/nav3recipes/deeplink/basic/deeplinkutil/DeepLinkMatcher.kt
new file mode 100644
index 0000000..fdaf5bd
--- /dev/null
+++ b/app/src/main/java/com/example/nav3recipes/deeplink/basic/deeplinkutil/DeepLinkMatcher.kt
@@ -0,0 +1,80 @@
+package com.example.nav3recipes.deeplink.basic.deeplinkutil
+
+import android.util.Log
+import androidx.navigation3.runtime.NavKey
+import kotlinx.serialization.KSerializer
+
+internal class DeepLinkMatcher(
+ val request: DeepLinkRequest,
+ val deepLinkPattern: DeepLinkPattern
+) {
+ /**
+ * Match a [DeepLinkRequest] to a [DeepLinkPattern].
+ *
+ * Returns a [DeepLinkMatchResult] if this matches the pattern, returns null otherwise
+ */
+ fun match(): DeepLinkMatchResult? {
+ if (request.pathSegments.size != deepLinkPattern.pathSegments.size) return null
+ // exact match (url does not contain any arguments)
+ if (request.uri == deepLinkPattern.uriPattern)
+ return DeepLinkMatchResult(deepLinkPattern.serializer, mapOf())
+
+ val args = mutableMapOf()
+ // match the path
+ request.pathSegments
+ .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(deepLinkPattern.pathSegments.asSequence())
+ .forEach { it ->
+ // retrieve the two path segments to compare
+ val requestedSegment = it.first
+ val candidateSegment = it.second
+ // if the potential match expects a path arg for this segment, try to parse the
+ // requested segment into the expected type
+ if (candidateSegment.isParamArg) {
+ val parsedValue = try {
+ candidateSegment.typeParser.invoke(requestedSegment)
+ } catch (e: IllegalArgumentException) {
+ Log.e(TAG_LOG_ERROR, "Failed to parse path value:[$requestedSegment].", e)
+ return null
+ }
+ args[candidateSegment.stringValue] = parsedValue
+ } else if(requestedSegment != candidateSegment.stringValue){
+ // if it's path arg is not the expected type, its not a match
+ return null
+ }
+ }
+ // match queries (if any)
+ request.queries.forEach { query ->
+ val name = query.key
+ val queryStringParser = deepLinkPattern.queryValueParsers[name]
+ val queryParsedValue = try {
+ queryStringParser!!.invoke(query.value)
+ } catch (e: IllegalArgumentException) {
+ Log.e(TAG_LOG_ERROR, "Failed to parse query name:[$name] value:[${query.value}].", e)
+ return null
+ }
+ args[name] = queryParsedValue
+ }
+ // provide the serializer of the matching key and map of arg names to parsed arg values
+ return DeepLinkMatchResult(deepLinkPattern.serializer, 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(
+ val serializer: KSerializer,
+ val args: Map
+)
+
+const val TAG_LOG_ERROR = "Nav3RecipesDeepLink"
\ No newline at end of file
diff --git a/app/src/main/java/com/example/nav3recipes/deeplink/basic/deeplinkutil/DeepLinkPattern.kt b/app/src/main/java/com/example/nav3recipes/deeplink/basic/deeplinkutil/DeepLinkPattern.kt
new file mode 100644
index 0000000..c19c624
--- /dev/null
+++ b/app/src/main/java/com/example/nav3recipes/deeplink/basic/deeplinkutil/DeepLinkPattern.kt
@@ -0,0 +1,118 @@
+package com.example.nav3recipes.deeplink.basic.deeplinkutil
+
+import android.net.Uri
+import androidx.navigation3.runtime.NavKey
+import com.example.nav3recipes.deeplink.basic.ui.NavRecipeKey
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.SerialKind
+import java.io.Serializable
+
+/**
+ * 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 = www.nav3recipes.com/home
+ * val deeplink2 = www.nav3recipes.com/profile/{userId}
+ * ```
+ * Then two [DeepLinkPattern] should be created
+ * ```
+ * val parsedDeeplink1 = DeepLinkPattern(T.serializer(), deeplink1)
+ * val parsedDeeplink2 = DeepLinkPattern(T.serializer(), 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. "abc.com/home/{pathArg}"
+ */
+internal class DeepLinkPattern(
+ val serializer: KSerializer,
+ 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 = buildList {
+ uriPattern.pathSegments.forEach { segment ->
+ // first, check if it is a path arg
+ var result = regexPatternFillIn.find(segment)
+ if (result != null) {
+ // if so, extract the path arg name (the string value within the curly braces)
+ val argName = result.groups[1]!!.value
+ // from [T], read the primitive type of this argument to get the correct type parser
+ val elementIndex = serializer.descriptor.getElementIndex(argName)
+ val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex)
+ // finally, add the arg name and its respective type parser to the map
+ add(PathSegment(argName, true,getTypeParser(elementDescriptor.kind)))
+ } else {
+ // if its not a path arg, then its just a static string path segment
+ add(PathSegment(segment,false, getTypeParser(PrimitiveKind.STRING)))
+ }
+ }
+ }
+
+ /**
+ * 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 = buildMap {
+ uriPattern.queryParameterNames.forEach { paramName ->
+ val elementIndex = serializer.descriptor.getElementIndex(paramName)
+ val elementDescriptor = serializer.descriptor.getElementDescriptor(elementIndex)
+ this[paramName] = getTypeParser(elementDescriptor.kind)
+ }
+ }
+
+ /**
+ * 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) {
+ PrimitiveKind.STRING -> Any::toString
+ PrimitiveKind.INT -> String::toInt
+ PrimitiveKind.BOOLEAN -> String::toBoolean
+ PrimitiveKind.BYTE -> String::toByte
+ PrimitiveKind.CHAR -> String::toCharArray
+ PrimitiveKind.DOUBLE -> String::toDouble
+ PrimitiveKind.FLOAT -> String::toFloat
+ PrimitiveKind.LONG -> String::toLong
+ PrimitiveKind.SHORT -> String::toShort
+ else -> throw IllegalArgumentException(
+ "Unsupported argument type of SerialKind:$kind. The argument type must be a Primitive."
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/nav3recipes/deeplink/basic/deeplinkutil/DeepLinkRequest.kt.kt b/app/src/main/java/com/example/nav3recipes/deeplink/basic/deeplinkutil/DeepLinkRequest.kt.kt
new file mode 100644
index 0000000..fe27e2f
--- /dev/null
+++ b/app/src/main/java/com/example/nav3recipes/deeplink/basic/deeplinkutil/DeepLinkRequest.kt.kt
@@ -0,0 +1,28 @@
+package com.example.nav3recipes.deeplink.basic.deeplinkutil
+
+import android.net.Uri
+
+/**
+ * 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 = uri.pathSegments
+
+ /**
+ * A map of query name to query value
+ */
+ val queries = buildMap {
+ uri.queryParameterNames.forEach { argName ->
+ this[argName] = uri.getQueryParameter(argName)!!
+ }
+ }
+
+ // TODO add parsing for other Uri components, i.e. fragments, mimeType, action
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/nav3recipes/deeplink/basic/deeplinkutil/KeyDecoder.kt b/app/src/main/java/com/example/nav3recipes/deeplink/basic/deeplinkutil/KeyDecoder.kt
new file mode 100644
index 0000000..85a50df
--- /dev/null
+++ b/app/src/main/java/com/example/nav3recipes/deeplink/basic/deeplinkutil/KeyDecoder.kt
@@ -0,0 +1,70 @@
+package com.example.nav3recipes.deeplink.basic.deeplinkutil
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.AbstractDecoder
+import kotlinx.serialization.encoding.CompositeDecoder
+import kotlinx.serialization.modules.EmptySerializersModule
+import kotlinx.serialization.modules.SerializersModule
+
+/**
+ * Decodes the list of arguments into a a backstack key
+ *
+ * **IMPORTANT** This decoder assumes that all argument types are Primitives.
+ */
+@OptIn(ExperimentalSerializationApi::class)
+internal class KeyDecoder(
+ private val arguments: Map,
+) : 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 >= descriptor.elementsCount) return CompositeDecoder.DECODE_DONE
+ val currentName = descriptor.getElementName(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 (arguments.contains(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
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/nav3recipes/deeplink/basic/ui/CommonResources.kt b/app/src/main/java/com/example/nav3recipes/deeplink/basic/ui/CommonResources.kt
new file mode 100644
index 0000000..416677f
--- /dev/null
+++ b/app/src/main/java/com/example/nav3recipes/deeplink/basic/ui/CommonResources.kt
@@ -0,0 +1,52 @@
+package com.example.nav3recipes.deeplink.basic.ui
+
+/**
+ * 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 = "https://www.nav3recipes.com"
+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::ageMin.name}={${SearchKey::ageMin.name}}" +
+ "&${SearchKey::ageMax.name}={${SearchKey::ageMax.name}}" +
+ "&${SearchKey::firstName.name}={${SearchKey::firstName.name}}" +
+ "&${SearchKey::location.name}={${SearchKey::location.name}}"
+
+/**
+ * User data
+ */
+internal const val FIRST_NAME_JOHN = "John"
+internal const val FIRST_NAME_TOM = "Tom"
+internal const val FIRST_NAME_MARY = "Mary"
+internal const val FIRST_NAME_JULIE = "Julie"
+internal const val LOCATION_CA = "CA"
+internal const val LOCATION_BC = "BC"
+internal const val LOCATION_BR = "BR"
+internal const val LOCATION_US = "US"
+internal const val EMPTY = ""
+internal val LIST_USERS = listOf(
+ User(FIRST_NAME_JOHN, 15, LOCATION_CA),
+ User(FIRST_NAME_JOHN, 22, LOCATION_BC),
+ User(FIRST_NAME_TOM, 25, LOCATION_CA),
+ User(FIRST_NAME_TOM, 68, LOCATION_BR),
+ User(FIRST_NAME_JULIE, 48, LOCATION_BR),
+ User(FIRST_NAME_JULIE, 33, LOCATION_US),
+ User(FIRST_NAME_JULIE, 9, LOCATION_BR),
+ User(FIRST_NAME_MARY, 64, LOCATION_US),
+ User(FIRST_NAME_MARY, 5, LOCATION_CA),
+ User(FIRST_NAME_MARY, 52, LOCATION_BC),
+ User(FIRST_NAME_TOM, 94, LOCATION_BR),
+ User(FIRST_NAME_JULIE, 46, LOCATION_CA),
+ User(FIRST_NAME_JULIE, 37, LOCATION_BC),
+ User(FIRST_NAME_JULIE, 73 ,LOCATION_US),
+ User(FIRST_NAME_MARY, 51, LOCATION_US),
+ User(FIRST_NAME_MARY, 63, LOCATION_BR),
+)
diff --git a/app/src/main/java/com/example/nav3recipes/deeplink/basic/ui/CommonScreens.kt b/app/src/main/java/com/example/nav3recipes/deeplink/basic/ui/CommonScreens.kt
new file mode 100644
index 0000000..c815efa
--- /dev/null
+++ b/app/src/main/java/com/example/nav3recipes/deeplink/basic/ui/CommonScreens.kt
@@ -0,0 +1,232 @@
+package com.example.nav3recipes.deeplink.basic.ui
+
+import android.content.Context
+import android.content.Intent
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material3.Button
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuAnchorType
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.net.toUri
+import androidx.navigation3.runtime.NavKey
+import kotlinx.serialization.Serializable
+
+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
+}
+
+@Serializable
+internal data class User(
+ val firstName: String,
+ val age: Int,
+ val location: String,
+)
+
+@Composable
+internal fun EntryScreen(text: String, content: @Composable () -> Unit = { }) {
+ Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(10.dp)) {
+ Text(text, fontWeight = FontWeight.Bold, fontSize = FONT_SIZE_TITLE)
+ content()
+ }
+ }
+}
+
+@Composable
+internal fun FriendsList(users: List) {
+ // display list of matching targets
+ if (users.isEmpty()) {
+ Text("List is Empty", fontWeight = FontWeight.Bold)
+ } else {
+ LazyColumn {
+ items(users.size) { idx ->
+ val user = users[idx]
+ TextContent("${user.firstName}(${user.age}), ${user.location}")
+ }
+ }
+ }
+}
+
+/**
+ * Displays a text input menu, may include several text fields
+ */
+@Composable
+internal fun MenuTextInput(
+ menuLabels: List,
+ onValueChange: (String, String) -> Unit = { _, _ ->},
+) {
+ Column {
+ menuLabels.forEach { label ->
+ var inputText by remember { mutableStateOf("") }
+
+ OutlinedTextField(
+ value = inputText,
+ onValueChange = {
+ inputText = it
+ onValueChange(label, it)
+ },
+ placeholder = { Text("enter integer") },
+ label = { Text(label) },
+ )
+ }
+ }
+
+}
+
+/**
+ * Displays a drop down menu, may include multiple drop downs
+ */
+@Composable
+internal fun MenuDropDown(
+ menuOptions: Map>,
+ onSelect: (label: String, selection: String) -> Unit = { _, _ ->},
+) {
+ Column(
+ modifier = Modifier.animateContentSize(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ menuOptions.forEach { entry ->
+ val key = entry.key
+ ArgumentDropDownMenu(label = key, menuItemOptions = entry.value) { label, selection ->
+ onSelect(key, selection)
+ }
+ }
+ }
+}
+
+// Display list of selections for one drop down
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun ArgumentDropDownMenu(
+ label: String,
+ menuItemOptions: List,
+ onSelect: (label: String, selection: String) -> Unit,
+) {
+ val initValue = menuItemOptions.firstOrNull() ?: ""
+ var expanded by remember { mutableStateOf(false) }
+ var currSelected by remember { mutableStateOf(initValue) }
+ Box(
+ modifier = Modifier
+ .padding(16.dp)
+ ) {
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = { expanded = !expanded }
+ ) {
+ TextField(
+ readOnly = true,
+ value = currSelected,
+ onValueChange = { },
+ label = { Text(label) },
+ trailingIcon = {
+ ExposedDropdownMenuDefaults.TrailingIcon(
+ expanded = expanded
+ )
+ },
+ modifier = Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, true)
+ )
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = {
+ expanded = false
+ }
+ ) {
+ menuItemOptions.forEach { text ->
+ DropdownMenuItem(
+ text = { Text(text) },
+ onClick = {
+ expanded = false
+ currSelected = text
+ onSelect(label, text)
+ }
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+internal fun DeepLinkButton(
+ context: Context,
+ targetActivity: Class<*>,
+ deepLinkUrl: String,
+) {
+ Button(
+ onClick = {
+ val intent = Intent(
+ context,
+ targetActivity
+ )
+ // start activity with the url
+ intent.data = deepLinkUrl.toUri()
+ context.startActivity(intent)
+ }
+ ) {
+ Text("Deeplink away!")
+ }
+}
+
+@Composable
+fun TextContent(text: String) {
+ Text(
+ text = text,
+ modifier = Modifier.width(300.dp),
+ textAlign = TextAlign.Center,
+ fontSize = FONT_SIZE_TEXT,
+ )
+}
+
+internal val FONT_SIZE_TITLE: TextUnit = 20.sp
+internal val FONT_SIZE_TEXT: TextUnit = 15.sp