diff --git a/advanceddeeplinkapp/.gitignore b/advanceddeeplinkapp/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/advanceddeeplinkapp/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/advanceddeeplinkapp/build.gradle.kts b/advanceddeeplinkapp/build.gradle.kts
new file mode 100644
index 00000000..c82a3eb3
--- /dev/null
+++ b/advanceddeeplinkapp/build.gradle.kts
@@ -0,0 +1,67 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.kotlin.serialization)
+}
+
+android {
+ namespace = "com.example.nav3recipes.deeplink.advanced"
+ compileSdk = 36
+
+ defaultConfig {
+ applicationId = "com.example.nav3recipes.deeplink.advanced"
+ minSdk = 24
+ targetSdk = 36
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+ buildFeatures {
+ compose = true
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+
+ implementation(libs.kotlinx.serialization.core)
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.androidx.navigation3.runtime)
+ implementation(libs.androidx.navigation3.ui)
+ implementation(project(":common"))
+
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+}
\ No newline at end of file
diff --git a/advanceddeeplinkapp/proguard-rules.pro b/advanceddeeplinkapp/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/advanceddeeplinkapp/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/advanceddeeplinkapp/src/main/AndroidManifest.xml b/advanceddeeplinkapp/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..d9ea26a7
--- /dev/null
+++ b/advanceddeeplinkapp/src/main/AndroidManifest.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/AdvancedDeeplinkAppActivity.kt b/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/AdvancedDeeplinkAppActivity.kt
new file mode 100644
index 00000000..de163ea1
--- /dev/null
+++ b/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/AdvancedDeeplinkAppActivity.kt
@@ -0,0 +1,111 @@
+package com.example.nav3recipes.deeplink.advanced
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+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.advanced.util.buildBackStack
+import com.example.nav3recipes.deeplink.advanced.util.navigateUp
+import com.example.nav3recipes.deeplink.advanced.util.toKey
+import com.example.nav3recipes.deeplink.common.EntryScreen
+import com.example.nav3recipes.deeplink.common.FriendsList
+import com.example.nav3recipes.deeplink.common.LIST_USERS
+import com.example.nav3recipes.deeplink.common.PaddedButton
+
+class AdvancedDeeplinkAppActivity: ComponentActivity() {
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val startKey = intent.data.toKey()
+
+ val flags = intent.flags
+ val isNewTask = flags and Intent.FLAG_ACTIVITY_NEW_TASK != 0 &&
+ flags and Intent.FLAG_ACTIVITY_CLEAR_TASK != 0
+
+ val syntheticBackStack = buildBackStack(
+ startKey = startKey,
+ buildFullPath = isNewTask
+ )
+ setContent {
+ val backStack: NavBackStack = rememberNavBackStack(*(syntheticBackStack.toTypedArray()))
+
+ Scaffold(
+ topBar = {
+ // top app bar to display up button
+ TopAppBar(
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.primary,
+ titleContentColor = MaterialTheme.colorScheme.primary,
+ ),
+ title = { stringResource(R.string.app_name)},
+ navigationIcon = {
+ /**
+ * Up button should never exit your app. Do not display it
+ * on the root Screen.
+ */
+ if (backStack.last() != Home) {
+ IconButton(onClick = {
+ backStack.navigateUp(
+ this@AdvancedDeeplinkAppActivity,
+ this@AdvancedDeeplinkAppActivity
+ )
+ }) {
+ Icon(
+ painter = painterResource(id = R.drawable.outline_arrow_upward_24),
+ contentDescription = "Up Button",
+ )
+ }
+ }
+ },
+ )
+ },
+ ) { innerPadding ->
+ NavDisplay(
+ backStack = backStack,
+ onBack = { backStack.removeLastOrNull()},
+ modifier = Modifier.padding(innerPadding),
+ entryProvider = entryProvider {
+ entry { key ->
+ EntryScreen(key.screenTitle) {
+ PaddedButton("See Users") {
+ backStack.add(Users)
+ }
+ }
+ }
+ entry { key ->
+ EntryScreen(key.screenTitle) {
+ FriendsList(LIST_USERS) { user ->
+ backStack.add(UserDetail(user))
+ }
+ }
+ }
+ entry { result ->
+ EntryScreen(result.screenTitle) {
+ FriendsList(listOf(result.user))
+ }
+ }
+ }
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/NavRecipeKey.kt b/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/NavRecipeKey.kt
new file mode 100644
index 00000000..773e99e3
--- /dev/null
+++ b/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/NavRecipeKey.kt
@@ -0,0 +1,63 @@
+package com.example.nav3recipes.deeplink.advanced
+
+import androidx.navigation3.runtime.NavKey
+import com.example.nav3recipes.deeplink.common.User
+import kotlinx.serialization.Serializable
+import androidx.navigation3.runtime.NavBackStack
+import com.example.nav3recipes.deeplink.advanced.util.navigateUp
+
+internal const val PATH_BASE = "https://www.nav3deeplink.com"
+
+/**
+ * Defines the NavKey used for this app.
+ *
+ * The keys are defined with this inheritance structure:
+ * [NavDeepLinkRecipeKey] extends [NavRecipeKey] extends [NavKey].
+ *
+ * [NavKey] - the base Navigation 3 interface that can be used with [NavBackStack]
+ *
+ * [NavRecipeKey] - a sub-interface to supports member variables and functions
+ * specific to this app
+ *
+ * [NavDeepLinkRecipeKey] - a sub-interface to ensure that all keys
+ * that support deeplinking (or all keys that have children keys that can be deeplinked into)
+ * implement these two fields:
+ * 1. parent - the hierarchical parent of this key, required for building a synthetic backStack
+ * 2. deeplinkUrl - the deeplink url associated with this key, required for supporting the
+ * Up button (see [navigateUp] for more on this).
+ */
+
+internal interface NavRecipeKey: NavKey {
+ val screenTitle: String
+}
+
+internal interface NavDeepLinkRecipeKey: NavRecipeKey {
+ val parent: NavKey
+ val deeplinkUrl: String
+}
+
+@Serializable
+object Home: NavRecipeKey {
+ override val screenTitle: String = "Home"
+}
+
+@Serializable
+object Users: NavDeepLinkRecipeKey {
+ override val screenTitle: String = "Users"
+ override val parent: NavKey = Home
+ override val deeplinkUrl: String
+ get() = "$PATH_BASE/$DEEPLINK_URL_TAG_USERS"
+}
+
+@Serializable
+internal data class UserDetail(
+ val user: User
+): NavDeepLinkRecipeKey {
+ override val screenTitle: String = "User"
+ override val parent: NavKey = Users
+ override val deeplinkUrl: String
+ get() = "$PATH_BASE/$DEEPLINK_URL_TAG_USER/${user.firstName}/${user.location}"
+}
+
+internal const val DEEPLINK_URL_TAG_USER = "user"
+internal const val DEEPLINK_URL_TAG_USERS = "users"
\ No newline at end of file
diff --git a/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/ui/theme/Color.kt b/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/ui/theme/Color.kt
new file mode 100644
index 00000000..17953810
--- /dev/null
+++ b/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+package com.example.nav3recipes.deeplink.advanced.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
diff --git a/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/ui/theme/Theme.kt b/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/ui/theme/Theme.kt
new file mode 100644
index 00000000..6e844b83
--- /dev/null
+++ b/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/ui/theme/Theme.kt
@@ -0,0 +1,58 @@
+package com.example.nav3recipes.deeplink.advanced.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+)
+
+@Composable
+fun Nav3RecipesTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/ui/theme/Type.kt b/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/ui/theme/Type.kt
new file mode 100644
index 00000000..421ee93a
--- /dev/null
+++ b/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+package com.example.nav3recipes.deeplink.advanced.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+)
\ No newline at end of file
diff --git a/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/util/DeepLinkBackStackUtil.kt b/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/util/DeepLinkBackStackUtil.kt
new file mode 100644
index 00000000..d79f53e9
--- /dev/null
+++ b/advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/advanced/util/DeepLinkBackStackUtil.kt
@@ -0,0 +1,175 @@
+package com.example.nav3recipes.deeplink.advanced.util
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import androidx.core.app.TaskStackBuilder
+import androidx.core.net.toUri
+import androidx.navigation3.runtime.NavBackStack
+import androidx.navigation3.runtime.NavKey
+import com.example.nav3recipes.deeplink.advanced.DEEPLINK_URL_TAG_USER
+import com.example.nav3recipes.deeplink.advanced.DEEPLINK_URL_TAG_USERS
+import com.example.nav3recipes.deeplink.advanced.Home
+import com.example.nav3recipes.deeplink.advanced.NavDeepLinkRecipeKey
+import com.example.nav3recipes.deeplink.advanced.UserDetail
+import com.example.nav3recipes.deeplink.advanced.Users
+import com.example.nav3recipes.deeplink.common.LIST_USERS
+
+/**
+ * A function that build a synthetic backStack.
+ *
+ * This helper returns one of two possible backStacks:
+ *
+ * 1. a backStack with only the deeplinked key if [buildFullPath] is false.
+ * 2. a backStack containing the deeplinked key and its hierarchical parent keys
+ * if [buildFullPath] is true.
+ *
+ * In the context of this recipe, [buildFullPath] is true if the deeplink intent has the
+ * [android.content.Intent.FLAG_ACTIVITY_NEW_TASK] and [android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK]
+ * flags.
+ * These flags indicate that the deeplinked Activity was started as the root Activity of a new Task, in which case
+ * a full synthetic backStack is required in order to support the proper, expected back button behavior.
+ *
+ * If those flags were not present, it means the deeplinked Activity was started
+ * in the app that originally triggered the deeplink. In this case, that original app is assumed to
+ * already have existing screens that users can system back into, therefore a synthetic backstack
+ * is OPTIONAL.
+ *
+ */
+internal fun buildBackStack(
+ startKey: NavKey,
+ buildFullPath: Boolean
+): List {
+ if (!buildFullPath) return listOf(startKey)
+ /**
+ * iterate up the parents of the startKey until it reaches the root key (a key without a parent)
+ */
+ return buildList {
+ var node: NavKey? = startKey
+ while (node != null) {
+ add(0, node)
+ val parent = if (node is NavDeepLinkRecipeKey) {
+ node.parent
+ } else null
+ node = parent
+ }
+ }
+}
+
+/**
+ * If this app was started on its own Task stack, then navigate up would simply
+ * pop from the backStack.
+ *
+ * Otherwise, it will restart this app in a new Task and build a full synthetic backStack
+ * starting from the root key to current key's parent (current key is "popped" upon user click on up button).
+ * This operation is required because by definition, an Up button is expected to:
+ * 1. Move from current screen to its hierarchical parent
+ * 2. Stay within this app
+ *
+ * Therefore, we need to build a synthetic backStack to fulfill expectation 1., and we need to
+ * restart the app in its own Task so that this app's screens are displayed within
+ * this app instead of being displayed within the originating app that triggered the deeplink.
+ */
+internal fun NavBackStack.navigateUp(
+ activity: Activity,
+ context: Context
+) {
+ /**
+ * The root key (the first key on synthetic backStack) would/should never display the Up button.
+ * So if the backStack only contains a non-root key, it means a synthetic backStack had not
+ * been built (aka the app was opened in the originating Task).
+ */
+ if (size == 1) {
+ val currKey = last()
+ /**
+ * upon navigating up, the current key is popped, so the restarted activity
+ * lands on the current key's parent
+ */
+ val deeplinkKey = if (currKey is NavDeepLinkRecipeKey) {
+ currKey.parent
+ } else null
+
+ /**
+ * create a [androidx.core.app.TaskStackBuilder] that will restart the
+ * Activity as the root Activity of a new Task
+ */
+ val builder = createTaskStackBuilder(deeplinkKey, activity, context)
+ // ensure current activity is finished
+ activity.finish()
+ // trigger restart
+ builder.startActivities()
+ } else {
+ removeLastOrNull()
+ }
+
+}
+
+/**
+ * Creates a [androidx.core.app.TaskStackBuilder].
+ *
+ * The builder takes the current context and Activity and builds a new Task stack with the
+ * restarted activity as the root Activity. The resulting TaskStack is used to restart
+ * the Activity in its own Task.
+ */
+private fun createTaskStackBuilder(
+ deeplinkKey: NavKey?,
+ activity: Activity,
+ context: Context
+): TaskStackBuilder {
+ /**
+ * The intent to restart the current activity.
+ */
+ val intent = Intent(context, activity.javaClass)
+
+ /**
+ * Pass in the deeplink url of the target key so that upon restart, the app
+ * can build the synthetic backStack starting from the deeplink key all the way up to the
+ * root key.
+ *
+ * See [buildBackStack] for building synthetic backStack.
+ */
+ if (deeplinkKey != null && deeplinkKey is NavDeepLinkRecipeKey) {
+ intent.data = deeplinkKey.deeplinkUrl.toUri()
+ }
+
+ /**
+ * Ensure that the Activity is restarted as the root of a new Task
+ */
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+
+ /**
+ * Lastly, attach the intent to the TaskStackBuilder.
+ *
+ * By using `addNextIntentWithParentStack`, the TaskStackBuilder will automatically
+ * add the intents for the parent activities (if any) of [activity].
+ */
+ return TaskStackBuilder.create(context).addNextIntentWithParentStack(intent)
+}
+
+/**
+ * A function that converts a deeplink uri into a NavKey.
+ *
+ * This helper is intentionally simple and basic. For a recipe that focuses on parsing a
+ * deeplink uri into a NavKey, please see [com.example.nav3recipes.deeplink.basic].
+ */
+internal fun Uri?.toKey(): NavKey {
+ if (this == null) return Home
+
+ val paths = pathSegments
+
+ if (pathSegments.isEmpty()) return Home
+
+ return when(paths.first()) {
+ DEEPLINK_URL_TAG_USERS -> Users
+ DEEPLINK_URL_TAG_USER -> {
+ val firstName = pathSegments[1]
+ val location = pathSegments[2]
+ val user = LIST_USERS.find {
+ it.firstName == firstName && it.location == location
+ }
+ if (user == null) Users else UserDetail(user)
+ }
+ else -> Home
+ }
+}
\ No newline at end of file
diff --git a/advanceddeeplinkapp/src/main/res/drawable/ic_launcher_background.xml b/advanceddeeplinkapp/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..f1566239
--- /dev/null
+++ b/advanceddeeplinkapp/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/advanceddeeplinkapp/src/main/res/drawable/ic_launcher_foreground.xml b/advanceddeeplinkapp/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 00000000..2b068d11
--- /dev/null
+++ b/advanceddeeplinkapp/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/advanceddeeplinkapp/src/main/res/drawable/outline_arrow_upward_24.xml b/advanceddeeplinkapp/src/main/res/drawable/outline_arrow_upward_24.xml
new file mode 100644
index 00000000..2d898ede
--- /dev/null
+++ b/advanceddeeplinkapp/src/main/res/drawable/outline_arrow_upward_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/advanceddeeplinkapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/advanceddeeplinkapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..6f3b755b
--- /dev/null
+++ b/advanceddeeplinkapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/advanceddeeplinkapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/advanceddeeplinkapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..6f3b755b
--- /dev/null
+++ b/advanceddeeplinkapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/advanceddeeplinkapp/src/main/res/mipmap-hdpi/ic_launcher.webp b/advanceddeeplinkapp/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 00000000..c209e78e
Binary files /dev/null and b/advanceddeeplinkapp/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/advanceddeeplinkapp/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/advanceddeeplinkapp/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..b2dfe3d1
Binary files /dev/null and b/advanceddeeplinkapp/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/advanceddeeplinkapp/src/main/res/mipmap-mdpi/ic_launcher.webp b/advanceddeeplinkapp/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 00000000..4f0f1d64
Binary files /dev/null and b/advanceddeeplinkapp/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/advanceddeeplinkapp/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/advanceddeeplinkapp/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..62b611da
Binary files /dev/null and b/advanceddeeplinkapp/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/advanceddeeplinkapp/src/main/res/mipmap-xhdpi/ic_launcher.webp b/advanceddeeplinkapp/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 00000000..948a3070
Binary files /dev/null and b/advanceddeeplinkapp/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/advanceddeeplinkapp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/advanceddeeplinkapp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..1b9a6956
Binary files /dev/null and b/advanceddeeplinkapp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/advanceddeeplinkapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/advanceddeeplinkapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..28d4b77f
Binary files /dev/null and b/advanceddeeplinkapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/advanceddeeplinkapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/advanceddeeplinkapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..9287f508
Binary files /dev/null and b/advanceddeeplinkapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/advanceddeeplinkapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/advanceddeeplinkapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..aa7d6427
Binary files /dev/null and b/advanceddeeplinkapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/advanceddeeplinkapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/advanceddeeplinkapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..9126ae37
Binary files /dev/null and b/advanceddeeplinkapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/advanceddeeplinkapp/src/main/res/values/colors.xml b/advanceddeeplinkapp/src/main/res/values/colors.xml
new file mode 100644
index 00000000..a720bc5d
--- /dev/null
+++ b/advanceddeeplinkapp/src/main/res/values/colors.xml
@@ -0,0 +1,11 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+ #7AFFFFFF
+
\ No newline at end of file
diff --git a/advanceddeeplinkapp/src/main/res/values/strings.xml b/advanceddeeplinkapp/src/main/res/values/strings.xml
new file mode 100644
index 00000000..2124e90d
--- /dev/null
+++ b/advanceddeeplinkapp/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Nav3 Deeplink App
+
\ No newline at end of file
diff --git a/advanceddeeplinkapp/src/main/res/values/themes.xml b/advanceddeeplinkapp/src/main/res/values/themes.xml
new file mode 100644
index 00000000..d0035769
--- /dev/null
+++ b/advanceddeeplinkapp/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index af42baa9..9395340d 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -80,6 +80,7 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
implementation(libs.androidx.material.icons.extended)
implementation(libs.androidx.navigation2)
+ implementation(project(":common"))
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 04012888..f95e6c1c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -179,6 +179,11 @@
+
+
diff --git a/app/src/main/java/com/example/nav3recipes/RecipePickerActivity.kt b/app/src/main/java/com/example/nav3recipes/RecipePickerActivity.kt
index ad52738f..578fbc20 100644
--- a/app/src/main/java/com/example/nav3recipes/RecipePickerActivity.kt
+++ b/app/src/main/java/com/example/nav3recipes/RecipePickerActivity.kt
@@ -63,6 +63,7 @@ import com.example.nav3recipes.results.state.ResultStateActivity
import com.example.nav3recipes.scenes.listdetail.ListDetailActivity
import com.example.nav3recipes.scenes.twopane.TwoPaneActivity
import com.example.nav3recipes.ui.setEdgeToEdgeConfig
+import com.example.nav3recipes.deeplink.advanced.AdvancedCreateDeepLinkActivity
/**
* Activity to show all available recipes and allow users to launch each one.
@@ -113,6 +114,7 @@ private val recipes = listOf(
Heading("Deeplink"),
Recipe("Parse Intent", CreateDeepLinkActivity::class.java),
+ Recipe("Synthetic BackStack", AdvancedCreateDeepLinkActivity::class.java),
)
class RecipePickerActivity : ComponentActivity() {
diff --git a/app/src/main/java/com/example/nav3recipes/deeplink/advanced/AdvancedCreateDeepLinkActivity.kt b/app/src/main/java/com/example/nav3recipes/deeplink/advanced/AdvancedCreateDeepLinkActivity.kt
new file mode 100644
index 00000000..05fc0cfa
--- /dev/null
+++ b/app/src/main/java/com/example/nav3recipes/deeplink/advanced/AdvancedCreateDeepLinkActivity.kt
@@ -0,0 +1,121 @@
+package com.example.nav3recipes.deeplink.advanced
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.core.net.toUri
+import com.example.nav3recipes.deeplink.common.EntryScreen
+import com.example.nav3recipes.deeplink.common.LIST_FIRST_NAMES
+import com.example.nav3recipes.deeplink.common.LIST_LOCATIONS
+import com.example.nav3recipes.deeplink.common.MenuDropDown
+import com.example.nav3recipes.deeplink.common.PaddedButton
+import com.example.nav3recipes.deeplink.common.TextContent
+
+internal const val ADVANCED_PATH_BASE = "https://www.nav3deeplink.com"
+
+/**
+ * The recipe entry point that allows users to create a deep link and make a request with it.
+ *
+ * **HOW THIS RECIPE WORKS** This recipe simulates a real-world scenario where "App A" deeplinks
+ * into "App B".
+ *
+ * "App A" is simulated by this current module [com.example.nav3recipes.deeplink.advanced], which
+ * contains the [AdvancedCreateDeepLinkActivity] that allows you to create a deeplink intent and
+ * trigger that in either the existing Task, or in a new Task.
+ *
+ * "App B" is simulated by the module [com.example.nav3recipes.deeplink.advanced], which contains
+ * the MainActivity that you deeplink into. That module shows you how to build a synthetic backStack
+ * and how to manage the Task stack properly in order to support both Back and Up buttons.
+ *
+ * See the [README](README.md) file of current module for more info on advanced deep linking.
+ */
+class AdvancedCreateDeepLinkActivity: ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ EntryScreen("Sandbox - Build Your Deeplink Intent") {
+ val initFirstName = MENU_OPTIONS_FIRST_NAME.values.first().first()
+ val initLocation = MENU_OPTIONS_LOCATION.values.last().first()
+ val initTaskStack = MENU_OPTIONS_TASK_STACK.values.first().first()
+ var firstName by remember { mutableStateOf(initFirstName) }
+ var location by remember { mutableStateOf(initLocation) }
+ var taskStack by remember { mutableStateOf(initTaskStack) }
+
+ // select first name
+ MenuDropDown(
+ menuOptions = MENU_OPTIONS_FIRST_NAME,
+ ) { _, selected ->
+ firstName = selected
+ }
+
+ // select first name
+ MenuDropDown(
+ menuOptions = MENU_OPTIONS_LOCATION,
+ ) { _, selected ->
+ location = selected
+ }
+
+ // select current task stack or build new task stack
+ MenuDropDown(
+ menuOptions = MENU_OPTIONS_TASK_STACK,
+ ) { _, selected ->
+ taskStack = selected
+ }
+
+ // build final deeplink URL and Intent
+ val finalUrl = "${ADVANCED_PATH_BASE}/user/$firstName/$location"
+
+ // display Intent info
+ val flagString = if (taskStack == TAG_NEW_TASK) {
+ "Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK"
+ } else ""
+ val intentString = """
+ | Final Intent:
+ | data = "$finalUrl"
+ | action = Intent.ACTION_VIEW
+ | flags = $flagString
+ """.trimMargin()
+
+ TextContent(intentString)
+
+ // deeplink to target
+ PaddedButton("Deeplink Away!") {
+ val intent = Intent().apply {
+ data = finalUrl.toUri()
+ action = Intent.ACTION_VIEW
+ if (taskStack == TAG_NEW_TASK) {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ }
+ }
+
+ startActivity(intent)
+ }
+ }
+ }
+ }
+}
+
+private const val TAG_FIRST_NAME = "firstName"
+private const val TAG_LOCATION = "location"
+private const val TAG_TASK_STACK = "Task stack"
+private const val TAG_CURRENT_TASK = "Use Current Task Stack"
+private const val TAG_NEW_TASK = "Start New Task Stack"
+
+private val MENU_OPTIONS_FIRST_NAME = mapOf(
+ TAG_FIRST_NAME to LIST_FIRST_NAMES
+)
+
+private val MENU_OPTIONS_LOCATION = mapOf(
+ TAG_LOCATION to LIST_LOCATIONS
+)
+
+private val MENU_OPTIONS_TASK_STACK = mapOf(
+ TAG_TASK_STACK to listOf(TAG_CURRENT_TASK, TAG_NEW_TASK),
+)
diff --git a/app/src/main/java/com/example/nav3recipes/deeplink/advanced/README.md b/app/src/main/java/com/example/nav3recipes/deeplink/advanced/README.md
new file mode 100644
index 00000000..c1f52b09
--- /dev/null
+++ b/app/src/main/java/com/example/nav3recipes/deeplink/advanced/README.md
@@ -0,0 +1,26 @@
+# Deep Link Advanced Recipe
+
+This recipe demonstrates how to apply the principles of navigation in the context of deep links by
+managing a synthetic backStack and Task stacks.
+
+# Recipe Structure
+This recipe simulates a real-world scenario where "App A" deeplinks
+into "App B".
+
+"App A" is simulated by the module [com.example.nav3recipes.deeplink.advanced], which
+contains the [CreateAdvancedDeepLinkActivity] that allows you to create a deeplink intent and
+trigger that in either the existing Task, or in a new Task.
+
+"App B" is simulated by the module [com.example.nav3recipes.deeplink.app], which contains
+the MainActivity that you deeplink into. That module shows you how to build a synthetic backStack
+and how to manage the Task stack properly in order to support both Back and Up buttons.
+
+# Core implementation
+A couple classes of note:
+
+1. **navigate up** is demonstrated in [Navigator](../../../../../../../../../appdeeplink/src/main/java/com/example/nav3recipes/deeplink/app/util/Navigator.kt)
+2. **synthetic backStack** is demonstrated in [buildBackStack](../../../../../../../../../appdeeplink/src/main/java/com/example/nav3recipes/deeplink/app/util/BackStackUtil.kt)
+
+# Further Read
+Check out the [deep link guide](../../../../../../../../../docs/deeplink-guide.md) for a
+comprehensive guide on Deep linking principles and how to apply them in Navigation 3.
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
index 0a2914ca..624c37bb 100644
--- a/app/src/main/java/com/example/nav3recipes/deeplink/basic/CreateDeepLinkActivity.kt
+++ b/app/src/main/java/com/example/nav3recipes/deeplink/basic/CreateDeepLinkActivity.kt
@@ -1,5 +1,6 @@
package com.example.nav3recipes.deeplink.basic
+import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -9,27 +10,25 @@ 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 androidx.core.net.toUri
+import com.example.nav3recipes.deeplink.common.PaddedButton
+import com.example.nav3recipes.deeplink.common.EMPTY
+import com.example.nav3recipes.deeplink.common.EntryScreen
+import com.example.nav3recipes.deeplink.common.FIRST_NAME_JOHN
+import com.example.nav3recipes.deeplink.common.FIRST_NAME_JULIE
+import com.example.nav3recipes.deeplink.common.FIRST_NAME_MARY
+import com.example.nav3recipes.deeplink.common.FIRST_NAME_TOM
+import com.example.nav3recipes.deeplink.common.LOCATION_BC
+import com.example.nav3recipes.deeplink.common.LOCATION_BR
+import com.example.nav3recipes.deeplink.common.LOCATION_CA
+import com.example.nav3recipes.deeplink.common.LOCATION_US
+import com.example.nav3recipes.deeplink.common.MenuDropDown
+import com.example.nav3recipes.deeplink.common.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
+import com.example.nav3recipes.deeplink.common.TextContent
/**
* This activity allows the user to create a deep link and make a request with it.
@@ -47,7 +46,7 @@ import com.example.nav3recipes.deeplink.basic.ui.UsersKey
*
* **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
+ * 2. basic.deeplink.ui - Contains the activity UI code, i.e. global string variables, deeplink URLs etc
* 3. basic.deeplink.util - Contains the classes and helper methods to parse and match
* the deeplinks
*
@@ -150,11 +149,15 @@ class CreateDeepLinkActivity : ComponentActivity() {
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
- )
+ PaddedButton("Deeplink Away!") {
+ val intent = Intent(
+ this@CreateDeepLinkActivity,
+ MainActivity::class.java
+ )
+ // start activity with the url
+ intent.data = finalUrl.toUri()
+ startActivity(intent)
+ }
}
}
}
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
index fb3420e7..e90b5b63 100644
--- a/app/src/main/java/com/example/nav3recipes/deeplink/basic/MainActivity.kt
+++ b/app/src/main/java/com/example/nav3recipes/deeplink/basic/MainActivity.kt
@@ -15,16 +15,13 @@ import com.example.nav3recipes.deeplink.basic.util.DeepLinkPattern
import com.example.nav3recipes.deeplink.basic.util.DeepLinkRequest
import com.example.nav3recipes.deeplink.basic.util.DeepLinkMatchResult
import com.example.nav3recipes.deeplink.basic.util.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.common.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
+import com.example.nav3recipes.deeplink.common.EntryScreen
+import com.example.nav3recipes.deeplink.common.FriendsList
+import com.example.nav3recipes.deeplink.common.LIST_USERS
/**
* Parses a target deeplink into a NavKey. There are several crucial steps involved:
diff --git a/app/src/main/java/com/example/nav3recipes/deeplink/basic/NavRecipeKey.kt b/app/src/main/java/com/example/nav3recipes/deeplink/basic/NavRecipeKey.kt
new file mode 100644
index 00000000..7a951a5f
--- /dev/null
+++ b/app/src/main/java/com/example/nav3recipes/deeplink/basic/NavRecipeKey.kt
@@ -0,0 +1,39 @@
+package com.example.nav3recipes.deeplink.basic
+
+import androidx.navigation3.runtime.NavKey
+import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_FILTER
+import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_HOME
+import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_SEARCH
+import com.example.nav3recipes.deeplink.basic.ui.STRING_LITERAL_USERS
+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
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/nav3recipes/deeplink/basic/README.md b/app/src/main/java/com/example/nav3recipes/deeplink/basic/README.md
index 6122f71a..cabb881e 100644
--- a/app/src/main/java/com/example/nav3recipes/deeplink/basic/README.md
+++ b/app/src/main/java/com/example/nav3recipes/deeplink/basic/README.md
@@ -19,5 +19,5 @@ See `MainActivity.deepLinkPatterns` for the actual url pattern of each.
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
+2. `basic.deeplink.ui` - Contains the activity UI code, i.e. global string variables, deeplink URLs etc
3. `basic.deeplink.util` - Contains the classes and helper methods to parse and match the deeplinks
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
deleted file mode 100644
index 416677f4..00000000
--- a/app/src/main/java/com/example/nav3recipes/deeplink/basic/ui/CommonResources.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-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/UrlResources.kt b/app/src/main/java/com/example/nav3recipes/deeplink/basic/ui/UrlResources.kt
new file mode 100644
index 00000000..16e3ef76
--- /dev/null
+++ b/app/src/main/java/com/example/nav3recipes/deeplink/basic/ui/UrlResources.kt
@@ -0,0 +1,23 @@
+package com.example.nav3recipes.deeplink.basic.ui
+
+import com.example.nav3recipes.deeplink.basic.SearchKey
+
+/**
+ * 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}}"
diff --git a/common/.gitignore b/common/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/common/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/common/build.gradle.kts b/common/build.gradle.kts
new file mode 100644
index 00000000..bb5cf50b
--- /dev/null
+++ b/common/build.gradle.kts
@@ -0,0 +1,53 @@
+plugins {
+ id("com.android.library")
+ alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.kotlin.android)
+}
+
+android {
+ namespace = "com.example.nav3recipes"
+ compileSdk = 36
+
+ defaultConfig {
+ minSdk = 24
+ targetSdk = 36
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+
+ buildFeatures {
+ compose = true
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+}
+
+dependencies {
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.material3)
+ implementation(libs.androidx.material3.windowsizeclass)
+ implementation(libs.androidx.adaptive.layout)
+ implementation(platform(libs.androidx.compose.bom))
+
+ implementation(libs.kotlinx.serialization.core)
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.androidx.navigation3.runtime)
+ implementation(libs.androidx.navigation3.ui)
+}
diff --git a/app/src/main/java/com/example/nav3recipes/deeplink/basic/ui/CommonScreens.kt b/common/src/main/kotlin/com/example/nav3recipes/deeplink/common/Screens.kt
similarity index 73%
rename from app/src/main/java/com/example/nav3recipes/deeplink/basic/ui/CommonScreens.kt
rename to common/src/main/kotlin/com/example/nav3recipes/deeplink/common/Screens.kt
index c815efaf..de62984c 100644
--- a/app/src/main/java/com/example/nav3recipes/deeplink/basic/ui/CommonScreens.kt
+++ b/common/src/main/kotlin/com/example/nav3recipes/deeplink/common/Screens.kt
@@ -1,11 +1,11 @@
-package com.example.nav3recipes.deeplink.basic.ui
+package com.example.nav3recipes.deeplink.common
-import android.content.Context
-import android.content.Intent
import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
@@ -26,55 +26,17 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
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 = { }) {
+public 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)
@@ -84,7 +46,10 @@ internal fun EntryScreen(text: String, content: @Composable () -> Unit = { }) {
}
@Composable
-internal fun FriendsList(users: List) {
+public fun FriendsList(
+ users: List,
+ onClick: ((user: User) -> Unit)? = null
+) {
// display list of matching targets
if (users.isEmpty()) {
Text("List is Empty", fontWeight = FontWeight.Bold)
@@ -92,7 +57,12 @@ internal fun FriendsList(users: List) {
LazyColumn {
items(users.size) { idx ->
val user = users[idx]
- TextContent("${user.firstName}(${user.age}), ${user.location}")
+ val userString = "${user.firstName}(${user.age}), ${user.location}"
+ if (onClick != null) {
+ TextClickable(userString) { onClick(user) }
+ } else {
+ TextContent(userString)
+ }
}
}
}
@@ -102,7 +72,7 @@ internal fun FriendsList(users: List) {
* Displays a text input menu, may include several text fields
*/
@Composable
-internal fun MenuTextInput(
+public fun MenuTextInput(
menuLabels: List,
onValueChange: (String, String) -> Unit = { _, _ ->},
) {
@@ -124,11 +94,24 @@ internal fun MenuTextInput(
}
+@Composable
+public fun PaddedButton(
+ text: String,
+ onClick: () -> Unit,
+) {
+ Button(
+ modifier = Modifier.padding(BUTTON_PADDING),
+ onClick = onClick
+ ) {
+ Text(text)
+ }
+}
+
/**
* Displays a drop down menu, may include multiple drop downs
*/
@Composable
-internal fun MenuDropDown(
+public fun MenuDropDown(
menuOptions: Map>,
onSelect: (label: String, selection: String) -> Unit = { _, _ ->},
) {
@@ -197,26 +180,6 @@ private fun ArgumentDropDownMenu(
}
}
-@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) {
@@ -228,5 +191,24 @@ fun TextContent(text: String) {
)
}
-internal val FONT_SIZE_TITLE: TextUnit = 20.sp
-internal val FONT_SIZE_TEXT: TextUnit = 15.sp
+@Composable
+public fun TextClickable(
+ text: String,
+ onClick: () -> Unit,
+) {
+ Text(
+ text = text,
+ color = Color.Blue,
+ style = TextStyle(textDecoration = TextDecoration.Underline),
+ modifier = Modifier.width(300.dp).clickable(
+ true,
+ onClick = onClick
+ ),
+ textAlign = TextAlign.Center,
+ fontSize = FONT_SIZE_TEXT,
+ )
+}
+
+public val FONT_SIZE_TITLE: TextUnit = 20.sp
+public val FONT_SIZE_TEXT: TextUnit = 15.sp
+private val BUTTON_PADDING = PaddingValues(12.dp, 12.dp, 12.dp, 12.dp)
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/example/nav3recipes/deeplink/common/User.kt b/common/src/main/kotlin/com/example/nav3recipes/deeplink/common/User.kt
new file mode 100644
index 00000000..ae6ba705
--- /dev/null
+++ b/common/src/main/kotlin/com/example/nav3recipes/deeplink/common/User.kt
@@ -0,0 +1,52 @@
+package com.example.nav3recipes.deeplink.common
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+public data class User(
+ val firstName: String,
+ val age: Int,
+ val location: String,
+)
+
+/**
+ * User data
+ */
+public const val FIRST_NAME_JOHN = "John"
+public const val FIRST_NAME_TOM = "Tom"
+public const val FIRST_NAME_MARY = "Mary"
+public const val FIRST_NAME_JULIE = "Julie"
+public const val LOCATION_CA = "CA"
+public const val LOCATION_BC = "BC"
+public const val LOCATION_BR = "BR"
+public const val LOCATION_US = "US"
+public const val EMPTY = ""
+public val LIST_USERS = listOf(
+ User(FIRST_NAME_JOHN, 15, LOCATION_CA),
+ User(FIRST_NAME_JOHN, 22, LOCATION_BC),
+ User(FIRST_NAME_JOHN, 22, LOCATION_BR),
+ User(FIRST_NAME_JOHN, 22, LOCATION_US),
+ User(FIRST_NAME_TOM, 25, LOCATION_CA),
+ User(FIRST_NAME_TOM, 68, LOCATION_BR),
+ User(FIRST_NAME_TOM, 94, LOCATION_BC),
+ User(FIRST_NAME_TOM, 22, LOCATION_US),
+ User(FIRST_NAME_JULIE, 48, LOCATION_BR),
+ User(FIRST_NAME_JULIE, 33, LOCATION_US),
+ User(FIRST_NAME_JULIE, 46, LOCATION_CA),
+ User(FIRST_NAME_JULIE, 37, LOCATION_BC),
+ User(FIRST_NAME_MARY, 51, LOCATION_US),
+ User(FIRST_NAME_MARY, 63, LOCATION_BR),
+ User(FIRST_NAME_MARY, 5, LOCATION_CA),
+ User(FIRST_NAME_MARY, 52, LOCATION_BC),
+)
+
+public val LIST_FIRST_NAMES = listOf(
+ FIRST_NAME_JOHN,
+ FIRST_NAME_TOM,
+ FIRST_NAME_MARY,
+ FIRST_NAME_JULIE
+)
+
+public val LIST_LOCATIONS = listOf(
+ LOCATION_CA, LOCATION_BC, LOCATION_BR, LOCATION_US
+)
diff --git a/docs/deeplink-guide.md b/docs/deeplink-guide.md
new file mode 100644
index 00000000..2342af77
--- /dev/null
+++ b/docs/deeplink-guide.md
@@ -0,0 +1,406 @@
+# Deep Link Guide
+
+This guide covers the basic principles of deep linking and demonstrates how to apply them
+in Navigation 3.
+
+The implementation covers parsing intents into navigation keys and managing synthetic backStacks for
+proper "Back" and "Up" navigation behavior.
+
+But first, let's go over some key concepts of navigation and deep linking.
+
+# Principles of Navigation
+[Princples of Navigation](https://developer.android.com/guide/navigation/principles) highlights
+key concepts for implementing navigation systems that provide a consistent and pleasant user
+experience.
+
+**Important Terms & Concepts**: Consider familiarizing with terms and
+concepts [here](#important-terms--concepts) before continuing with this guide.
+
+Three principles are particularly relevant for deep linking.
+
+## Up and Back are identical within your app's task
+When deep linking to your app's Task, Up and Back should both return
+to a screen that the user would most likely have seen before the current screen.
+
+This implies that Up and Back behaves differently when your Activity is started in another
+App's Task. The Back button should return to the screen before deep linking, which is likely the
+screen of the other App. The Up button should redirect to a screen *within your app* that the
+user would most likely have seen before the current screen.
+
+## The Up button never exits your app
+The Up button should always navigate within your App's screens and never exit to another app
+or to the device's home screen. This means the Up button should be
+disabled on your App's start destination.
+
+## Deep linking simulates manual navigation
+Deep linking jumps directly to any destination on your App, with the backStack populated with
+just the one destination. To support natural Up/Back to the start destination, you should
+build a *synthetic back stack* that simulates the path the user would have taken to reach
+that destination manually.
+
+# Implementing Deep Links in Navigation 3
+
+This guide applies the [Principles of Navigation](#principles-of-navigation) to demonstrate how to
+handle deep links in your app.
+
+Note that each step of this guide can be implemented in various ways depending on your app's
+architecture and requirements. The approach shown here is just one possible way to handle deep links
+with Navigation 3.
+
+Let's start with the basics: how to parse an Intent's URL into a navigation key.
+
+## Parsing an intent into a navigation key
+
+When your app receives a deep link, you need to convert the incoming `Intent`
+(specifically the data URI) into a `NavKey` that your navigation system can
+understand.
+
+This process involves four main steps:
+
+1. **Define Supported Deep Links**: Create `DeepLinkPattern` objects that
+ describe the URLs your app supports. These patterns link a URI structure to
+ a specific `NavKey` class.
+2. **Parse the Request**: Convert the incoming `Intent`'s data URI into a
+ readable format, such as a `DeepLinkRequest`.
+3. **Match Request to Pattern**: Compare the incoming request against your list
+ of supported patterns to find a match.
+4. **Decode to Key**: Use the match result to extract arguments and create an
+ instance of the corresponding `NavKey`.
+
+### Example Implementation
+
+The `com.example.nav3recipes.deeplink.basic` package provides an example of
+this flow.
+
+**Step 1: Define Patterns**
+
+In a file (e.g., `MainActivity`), define a list of supported deeplink patterns.
+You can leverage the `kotlinx.serialization` library by annotating your Navigation keys with
+`@Serializable`, and then use the generated `Serializer` to map deep link arguments to a
+key argument.
+
+For example:
+
+```kotlin
+internal val deepLinkPatterns: List> = listOf(
+ // URL pattern with exact match: "https://www.nav3recipes.com/home"
+ DeepLinkPattern(HomeKey.serializer(), (URL_HOME_EXACT).toUri()),
+
+ // URL pattern with Path arguments: "https://www.nav3recipes.com/users/with/{filter}"
+ DeepLinkPattern(UsersKey.serializer(), (URL_USERS_WITH_FILTER).toUri()),
+
+ // URL pattern with Query arguments: "https://www.nav3recipes.com/users/search?{firstName}&{age}..."
+ DeepLinkPattern(SearchKey.serializer(), (URL_SEARCH.toUri())),
+)
+```
+
+The sample `DeepLinkPattern` class takes the URL pattern and serializer for the associated
+key, then maps each argument ("{...}") to a field in the key. Using the serializer,
+`DeepLinkPattern` stores the metadata for each argument such as its KType and its argument name.
+
+**Step 2: Parse the Request**
+
+In your Activity's `onCreate` method, you will need to retrieve the deep link URI from the
+intent. Then parse that URI into a readable format. In this recipe, we use a
+`DeepLinkRequest` class which parses the URI's path segments and query parameters into a map. This
+map will make it easier to compare against the patterns defined in Step 1.
+
+**Step 3: Match Request to Pattern**
+
+Next, iterate through your list of `deepLinkPatterns`. For each pattern,
+use a matcher (e.g., `DeepLinkMatcher`) to check if the request's path and arguments align with
+the pattern's structure. The matcher should return a result object containing the matched arguments
+if successful, or null if not.
+
+**Step 4: Decode to Key**
+
+If a match is found, use the matched arguments to instantiate the corresponding `NavKey`.
+Since we used `kotlinx.serialization` in Step 1, we can leverage a custom Decoder (`KeyDecoder`)
+to decode the map of arguments directly into the strongly-typed key object.
+
+```kotlin
+override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val uri: Uri? = intent.data
+
+ val key: NavKey = uri?.let {
+ // Step 2: Parse request
+ val request = DeepLinkRequest(uri)
+
+ // Step 3: Find match
+ val match = deepLinkPatterns.firstNotNullOfOrNull { pattern ->
+ DeepLinkMatcher(request, pattern).match()
+ }
+
+ // Step 4: Decode to NavKey
+ match?.let {
+ KeyDecoder(match.args).decodeSerializableValue(match.serializer)
+ }
+ } ?: HomeKey // Fallback to home if no match or no URI
+
+ setContent {
+ val backStack = rememberNavBackStack(key)
+ // ... setup NavDisplay
+ }
+}
+```
+
+For more details, refer to the [Basic Deep Link Recipe](../app/src/main/java/com/example/nav3recipes/deeplink/basic/README.md)
+and the helper functions in the [util](../app/src/main/java/com/example/nav3recipes/deeplink/basic/util) package.
+
+## Synthetic backStack & managing Tasks
+
+Now we are ready to apply the Principles of Navigation. Their application is demonstrated
+in the [deeplink.advanced](../app/src/main/java/com/example/nav3recipes/deeplink/advanced) and [deeplink.app](../advanceddeeplinkapp/src/main/java/com/example/nav3recipes/deeplink/app)
+modules.
+
+### Identifying the Task
+
+First, how do you check which Task your Activity is started on? One of the most
+common way to start an Activity in a new Task is by attaching the `Intent.FLAG_ACTIVITY_NEW_TASK`
+flag to the deep link Intent.
+
+In the Activity's `onCreate` method, you can access the Intent and check if that flag is present.
+
+```kotlin
+override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val flags = intent.flags
+ val isNewTask = flags and Intent.FLAG_ACTIVITY_NEW_TASK != 0
+ // ...
+}
+```
+If `isNewTask` is true, the Activity is started in a new Task. Otherwise, the Activity
+is started in the current Task.
+
+### Applying Principle 1: Up and Back in New Task
+
+If the Activity is started in a new Task, you will need to build a synthetic backStack to support
+both Up and Back.
+
+First, create an interface with a `parent` field that all deep link keys inherit.
+
+```kotlin
+interface DeepLinkKey: NavKey {
+ val parent: NavKey
+}
+
+object HomeKey: NavKey
+
+object UserListKey: DeepLinkKey {
+ override val parent = HomeKey
+}
+
+class UserDetailKey(user: User): DeepLinkKey {
+ override val parent = UserListKey
+}
+```
+
+After you parse the deep link url into a key, build a synthetic backStack by iterating
+up the key's parents.
+
+```kotlin
+fun buildSyntheticBackStack(
+ deeplinkKey: DeepLinkKey,
+): List {
+ return buildList {
+ var node: DeepLinkKey? = startKey
+ while (node != null) {
+ // ensure the parent is added to the start of the list
+ add(0, node)
+ val parent = if (node is DeepLinkKey) {
+ node.parent
+ } else null
+ node = parent
+ }
+ }
+}
+```
+
+**Note:** There are no hard rules as to what the "parent" key should be. In general it should
+be the screen that is most natural / likely to come before the child key if the user
+had manually navigated to the child key.
+
+Now pass that returned backStack to the NavDisplay
+
+```kotlin
+val syntheticBackStack = buildBackStack(deeplinkKey)
+
+setContent {
+ val backStack: NavBackStack = rememberNavBackStack(*(syntheticBackStack.toTypedArray()))
+ NavDisplay(
+ backStack = backStack,
+ // ...
+ ) {
+ // ...
+ }
+}
+```
+
+### Applying Principle 1: Up and Back in Original Task
+
+If the Activity is started in the current Task, Up and Back buttons are handled differently.
+
+#### Back button
+You simply need to add the deeplink key to the backStack
+
+```kotlin
+setContent {
+ // val deeplinkKey = parse deep link into key...
+ val backStack: NavBackStack = rememberNavBackStack(deeplinkKey)
+ NavDisplay(
+ backStack = backStack,
+ // ...
+ )
+}
+```
+
+Optionally you can also create a synthetic backStack appropriate to your app's needs.
+
+#### Up button
+Up button should never exit your app or return to the previous app. So you need to restart your
+App in a new Task and then build a synthetic backStack as demonstrated above.
+
+To restart the App, you will need a few things:
+1. the Context of the Activity
+2. the Activity itself
+3. the deep link url of the current screen's parent (remember, the current screen is technically
+ already popped from the backStack once the user clicked Up)
+
+Then build a TaskStackBuilder with an Intent containing the deep link url:
+
+```kotlin
+private fun createTaskStackBuilder(
+ deeplinkKey: NavKey?,
+ activity: Activity?,
+ context: Context
+): TaskStackBuilder {
+
+ // The intent to restart the current activity
+ val intent = if (activity != null) {
+ Intent(context, activity.javaClass)
+ } else {
+ val launchIntent =
+ context.packageManager.getLaunchIntentForPackage(context.packageName)
+ launchIntent ?: Intent()
+ }
+
+ // Pass in the deeplink url of the parent key
+ if (deeplinkKey != null && deeplinkKey is NavDeepLinkRecipeKey) {
+ intent.data = deeplinkKey.deeplinkUrl.toUri()
+ }
+
+ // Ensure that the MainActivity is restarted as the root of a new Task
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+
+ // Lastly, attach the intent to the TaskStackBuilder
+ return TaskStackBuilder.create(context).addNextIntentWithParentStack(intent)
+}
+```
+
+Now start the activity:
+
+```kotlin
+fun navigateUp(
+ // pass in required context, activity etc
+) {
+ if (onOriginalTask) {
+ val taskStackBuilder = createTaskStackBuilder(deeplinkKey, activity, context)
+ // ensure current activity is finished
+ activity?.finish()
+ // invoke restart
+ taskStackBuilder.startActivities()
+ }
+}
+```
+
+You Activity should see that the `Intent.FLAG_ACTIVITY_NEW_TASK` is present and build a
+synthetic backStack as demonstrated above.
+
+### Applying Principle 2: The Up button never exits your app
+
+Aside from restarting your Activity on a new Task, make sure your App's start destination does
+not show the Up button
+
+```kotlin
+TopAppBar(
+ // ...
+ navigationIcon = {
+ /**
+ * Only display Up button if not on Root screen
+ */
+ if (currentKey != backStack.first()) {
+ IconButton(onClick = {
+ backStack.navigateUp()
+ }) {
+ // ...
+ }
+ }
+ },
+)
+```
+### Applying Principle 3: Deep linking simulates manual navigation
+
+In general this means simulating a backStack as if the user had manually navigated from the
+start destination to the deep linked destination, which is demonstrated in the above sections.
+
+There is no hard definition for what the "correct" parent but some possible considerations
+include:
+
+1. shortest path from start destination to deeplinked destination
+2. a parent that allows navigating to sibling destinations
+3. conditional parenting based on app state
+
+## Summary
+
+To summarize, the tables below show the recommended behavior of Up and Back buttons
+depending on the Task.
+
+**Existing Task**
+
+| Task | Target | Synthetic backStack |
+|-------------|-----------------------------|-------------------------------------------------|
+| Up Button | Deep linked Screen's Parent | Restart Activity on new Task & build backStack |
+| Back Button | Screen before deep linking | None |
+
+**New Task**
+
+| Task | Target | Synthetic backStack |
+|-------------|-----------------------------|------------------------------------------------|
+| Up Button | Deep linked Screen's Parent | Build backStack on Activity creation |
+| Back Button | Deep linked Screen's Parent | Build backStack on Activity creation |
+
+
+### Important Terms & Concepts
+
+**Task**: A collection of Activities arranged in a stack. A cohesive unit that can move from
+the foreground to the background and vice versa. On a mobile device, each Task is represented by
+one of the windows in the "recent apps" view when you swipe up from the bottom
+of the screen. Each one of these windows (or Task) is usually marked with the launcher icon of
+the App that is at the root of the Task stack. For more info see [this doc](https://developer.android.com/guide/components/activities/tasks-and-back-stack).
+
+**BackStack**: Can be used to described a Task's stack. More commonly used to describe a stack of
+destinations (or representations of destinations) that the user navigates through, starting from
+the app's start destination to the current destination. Common operations on a backStack
+include pushing new destinations onto the top of the stack (navigating forward), and popping
+off destinations from the top of the stack (navigating back).
+
+**Synthetic BackStack** A manually constructed stack of destinations that simulates
+the path the user *would* have taken from the root destination to the current destination. This
+path should represent the most natural or most likely sequence of screens that users would have
+naturally navigated through in order to reach the current screen.
+
+**Activity** A window in which an App draws its UI. An App may consist of one or more Activities.
+See [this documentation](https://developer.android.com/guide/components/activities/intro-activities) for more details on Activities. Each App should have exactly
+one "Launcher" Activity that serves as the entry point to the App, with optional additional
+Activities that the user can navigate to.
+
+**Task vs App vs Activity** As mentioned, a Task is a stack of Activities. You can think of the
+App of the root Activity as the owner of this Task. It is also possible to start the Activity of
+*another* app within this Task, such as when you deep link. For example *App A* owns *Task A*, and
+App A deep links into *App B*'s Activity. In this scenario if you go to the "recept apps"
+view (swipe up from the bottom of the screen), you will see that even though you are seeing
+App B's screen, the launcher icon attached to the window is App A's. You will need to launch
+App B's Activity in a brand new Task with App B as the root in order to be in App B's own Task.
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 9adc0a3c..d2d173dd 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -47,4 +47,5 @@ dependencyResolutionManagement {
rootProject.name = "Nav3Recipes"
include(":app")
-
\ No newline at end of file
+include(":advanceddeeplinkapp")
+include(":common")