Skip to content
Open
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: 0 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
43 changes: 42 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
android:theme="@style/Theme.Nav3Recipes">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
Expand Down Expand Up @@ -141,6 +140,48 @@
android:name=".migration.step7.Step7MigrationActivity"
android:exported="true"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
android:name=".deeplink.basic.CreateDeepLinkActivity"
android:exported="true"
android:theme="@style/Theme.Nav3Recipes">
</activity>
<activity
android:name=".deeplink.basic.MainActivity"
android:exported="true"
android:theme="@style/Theme.Nav3Recipes">
<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.nav3recipes.com"/>
<!-- filter for exact url -->
<uri-relative-filter-group android:allow="true">
<data android:path="/home" />
</uri-relative-filter-group>
<!-- filter for exactly two path arguments -->
<uri-relative-filter-group android:allow="true">
<data android:pathPattern="/users/include/[^/]+$" />
</uri-relative-filter-group>
<!-- filter for optional query arguments -->
<uri-relative-filter-group android:allow="true">
<data android:path="/users/search" />
<data android:query="firstName=value!" />
</uri-relative-filter-group>
<uri-relative-filter-group android:allow="true">
<data android:path="/users/search" />
<data android:query="minAge=value!" />
</uri-relative-filter-group>
<uri-relative-filter-group android:allow="true">
<data android:path="/users/search" />
<data android:query="maxAge=value!" />
</uri-relative-filter-group>
<uri-relative-filter-group android:allow="true">
<data android:path="/users/search" />
<data android:query="location=value!" />
</uri-relative-filter-group>
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
Copy link
Collaborator

Choose a reason for hiding this comment

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

This comment block will likely be the landing page from the README. It'd be good to have a more detailed description of:

  • how the recipe works: e.g. it consists of two activities, one to construct and trigger the deeplink request and another to handle that request (the app itself)
  • the screens in the app: Home, Users and Search
  • the deeplink format: either describe the format here, or link to where it's defined

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added docs in CreateDeepLinkActivity.kt starting line#35

* **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<String, String>() }

// 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)


Loading