Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions advanceddeeplinkapp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
67 changes: 67 additions & 0 deletions advanceddeeplinkapp/build.gradle.kts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If at all possible, I would like to avoid creating separate modules in the recipes repo. Currently each recipe has its own activity - is it not possible to demonstrate the deep linking concepts by having separate activities for each deep link app?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've explored how to represent different Task stacks without a second app module. Separate activities is not enough - we need to show the difference between an app's Activity in another App's Task, and the an app's Activity in its own Task. We do need a second app for this :(

Copy link
Collaborator Author

@claraf3 claraf3 Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you deep link to another Activity within the same module, the Activity would still be started in the original Task even if you flagged it with Intent.FLAG_ACTIVITY_NEW_TASK

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK fair enough, thanks for clarifying.

Original file line number Diff line number Diff line change
@@ -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)
}
21 changes: 21 additions & 0 deletions advanceddeeplinkapp/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions advanceddeeplinkapp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Nav3Recipes"
android:enableOnBackInvokedCallback="true">
<activity
android:name="AdvancedDeeplinkAppActivity"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTop"
android:theme="@style/Theme.Nav3Recipes">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"
android:host="www.nav3deeplink.com"/>
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -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<NavKey> = 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<Home> { key ->
EntryScreen(key.screenTitle) {
PaddedButton("See Users") {
backStack.add(Users)
}
}
}
entry<Users> { key ->
EntryScreen(key.screenTitle) {
FriendsList(LIST_USERS) { user ->
backStack.add(UserDetail(user))
}
}
}
entry<UserDetail> { result ->
EntryScreen(result.screenTitle) {
FriendsList(listOf(result.user))
}
}
}
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be better named RecipeNavKey as it's a recipe-specific NavKey.

val screenTitle: String
}

internal interface NavDeepLinkRecipeKey: NavRecipeKey {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better as DeepLinkRecipeNavKey

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"
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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
)
}
Loading