Skip to content

Commit d22b204

Browse files
committed
Add deeplink recipe for single module
A recipe that shows how to parse a requested deeplink into a backStack key. It demonstrates a few crucial steps: STEP 1.Parse supported deeplinks (URLs that can be deeplinked into) into a readily readable format (see [DeepLinkCandidate]) STEP 2. Parse the requested deeplink into a readily readable, format (see [DeepLinkTarget]) **note** the parsed requested deeplink and parsed supported deeplinks should be cohesive with each other to facilitate comparison and finding a match STEP 3. Compare the requested deeplink target with supported deeplinks in order to find a match (see [DeepLinkMatchResult]). The match result's format should enable conversion from result to backstack key, regardless of what the conversion method may be. STEP 4. Associate the match results with the correct backstack key This recipes provides an example for each of the above steps by way of kotlinx.serialization. Note that this recipe is designed to focus on parsing an intent into a key, and therefore the following deeplink considerations are not included in this scope: - Create synthetic backStack - Multi-modular setup - DI - Managing TaskStack - Up button ves Back Button
1 parent 56d29d1 commit d22b204

File tree

9 files changed

+868
-2
lines changed

9 files changed

+868
-2
lines changed

app/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ dependencies {
7272
implementation(libs.androidx.adaptive.layout)
7373
implementation(libs.androidx.material3.navigation3)
7474

75-
7675
implementation(libs.kotlinx.serialization.core)
7776
implementation(libs.kotlinx.serialization.json)
7877
implementation(libs.androidx.navigation3.runtime)

app/src/main/AndroidManifest.xml

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
android:theme="@style/Theme.Nav3Recipes">
3838
<intent-filter>
3939
<action android:name="android.intent.action.MAIN" />
40-
4140
<category android:name="android.intent.category.LAUNCHER" />
4241
</intent-filter>
4342
</activity>
@@ -141,6 +140,48 @@
141140
android:name=".migration.step7.Step7MigrationActivity"
142141
android:exported="true"
143142
android:theme="@style/Theme.Nav3Recipes"/>
143+
<activity
144+
android:name=".deeplink.parseintent.singleModule.ParseIntentLandingActivity"
145+
android:exported="true"
146+
android:theme="@style/Theme.Nav3Recipes">
147+
</activity>
148+
<activity
149+
android:name=".deeplink.parseintent.singleModule.ParseIntentActivity"
150+
android:exported="true"
151+
android:theme="@style/Theme.Nav3Recipes">
152+
<intent-filter android:autoVerify="true">
153+
<action android:name="android.intent.action.VIEW" />
154+
<category android:name="android.intent.category.DEFAULT" />
155+
<category android:name="android.intent.category.BROWSABLE" />
156+
<data android:scheme="https"
157+
android:host="www.nav3recipes.com"/>
158+
<!-- filter for exact url -->
159+
<uri-relative-filter-group android:allow="true">
160+
<data android:path="/home" />
161+
</uri-relative-filter-group>
162+
<!-- filter for exactly two path arguments -->
163+
<uri-relative-filter-group android:allow="true">
164+
<data android:pathPattern="/users/include/[^/]+$" />
165+
</uri-relative-filter-group>
166+
<!-- filter for optional query arguments -->
167+
<uri-relative-filter-group android:allow="true">
168+
<data android:path="/users/search" />
169+
<data android:query="firstName=value!" />
170+
</uri-relative-filter-group>
171+
<uri-relative-filter-group android:allow="true">
172+
<data android:path="/users/search" />
173+
<data android:query="minAge=value!" />
174+
</uri-relative-filter-group>
175+
<uri-relative-filter-group android:allow="true">
176+
<data android:path="/users/search" />
177+
<data android:query="maxAge=value!" />
178+
</uri-relative-filter-group>
179+
<uri-relative-filter-group android:allow="true">
180+
<data android:path="/users/search" />
181+
<data android:query="location=value!" />
182+
</uri-relative-filter-group>
183+
</intent-filter>
184+
</activity>
144185
</application>
145186

146187
</manifest>

app/src/main/java/com/example/nav3recipes/RecipePickerActivity.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import com.example.nav3recipes.basicdsl.BasicDslActivity
3131
import com.example.nav3recipes.basicsaveable.BasicSaveableActivity
3232
import com.example.nav3recipes.commonui.CommonUiActivity
3333
import com.example.nav3recipes.conditional.ConditionalActivity
34+
import com.example.nav3recipes.deeplink.parseintent.singleModule.ParseIntentLandingActivity
3435
import com.example.nav3recipes.dialog.DialogActivity
3536
import com.example.nav3recipes.modular.hilt.ModularActivity
3637
import com.example.nav3recipes.passingarguments.viewmodels.basic.BasicViewModelsActivity
@@ -81,6 +82,9 @@ private val recipes = listOf(
8182
Heading("Returning Results"),
8283
Recipe("Return result as Event", ResultEventActivity::class.java),
8384
Recipe("Return result as State", ResultStateActivity::class.java),
85+
86+
Heading("Deeplink"),
87+
Recipe("Parse Intent", ParseIntentLandingActivity::class.java),
8488
)
8589

8690
class RecipePickerActivity : ComponentActivity() {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.example.nav3recipes.deeplink.parseintent.singleModule
2+
3+
/**
4+
* String resources
5+
*/
6+
internal const val STRING_LITERAL_FILTER = "filter"
7+
internal const val STRING_LITERAL_HOME = "home"
8+
internal const val STRING_LITERAL_USERS = "users"
9+
internal const val STRING_LITERAL_SEARCH = "search"
10+
internal const val STRING_LITERAL_INCLUDE = "include"
11+
internal const val PATH_BASE = "https://www.nav3recipes.com"
12+
internal const val PATH_INCLUDE = "$STRING_LITERAL_USERS/$STRING_LITERAL_INCLUDE"
13+
internal const val PATH_SEARCH = "$STRING_LITERAL_USERS/$STRING_LITERAL_SEARCH"
14+
internal const val URL_HOME_EXACT = "$PATH_BASE/$STRING_LITERAL_HOME"
15+
16+
internal const val URL_USERS_WITH_FILTER = "$PATH_BASE/$PATH_INCLUDE/{$STRING_LITERAL_FILTER}"
17+
internal val URL_SEARCH = "$PATH_BASE/$PATH_SEARCH" +
18+
"?${SearchKey::ageMin.name}={${SearchKey::ageMin.name}}" +
19+
"&${SearchKey::ageMax.name}={${SearchKey::ageMax.name}}" +
20+
"&${SearchKey::firstName.name}={${SearchKey::firstName.name}}" +
21+
"&${SearchKey::location.name}={${SearchKey::location.name}}"
22+
23+
/**
24+
* User data
25+
*/
26+
internal const val FIRST_NAME_JOHN = "John"
27+
internal const val FIRST_NAME_TOM = "Tom"
28+
internal const val FIRST_NAME_MARY = "Mary"
29+
internal const val FIRST_NAME_JULIE = "Julie"
30+
internal const val LOCATION_CA = "CA"
31+
internal const val LOCATION_BC = "BC"
32+
internal const val LOCATION_BR = "BR"
33+
internal const val LOCATION_US = "US"
34+
internal const val EMPTY = ""
35+
internal val LIST_USERS = listOf(
36+
User(FIRST_NAME_JOHN, 15, LOCATION_CA),
37+
User(FIRST_NAME_JOHN, 22, LOCATION_BC),
38+
User(FIRST_NAME_TOM, 25, LOCATION_CA),
39+
User(FIRST_NAME_TOM, 68, LOCATION_BR),
40+
User(FIRST_NAME_JULIE, 48, LOCATION_BR),
41+
User(FIRST_NAME_JULIE, 33, LOCATION_US),
42+
User(FIRST_NAME_JULIE, 9, LOCATION_BR),
43+
User(FIRST_NAME_MARY, 64, LOCATION_US),
44+
User(FIRST_NAME_MARY, 5, LOCATION_CA),
45+
User(FIRST_NAME_MARY, 52, LOCATION_BC),
46+
User(FIRST_NAME_TOM, 94, LOCATION_BR),
47+
User(FIRST_NAME_JULIE, 46, LOCATION_CA),
48+
User(FIRST_NAME_JULIE, 37, LOCATION_BC),
49+
User(FIRST_NAME_JULIE, 73 ,LOCATION_US),
50+
User(FIRST_NAME_MARY, 51, LOCATION_US),
51+
User(FIRST_NAME_MARY, 63, LOCATION_BR),
52+
)
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package com.example.nav3recipes.deeplink.parseintent.singleModule
2+
3+
import android.content.Context
4+
import android.content.Intent
5+
import androidx.compose.animation.animateContentSize
6+
import androidx.compose.foundation.layout.Arrangement
7+
import androidx.compose.foundation.layout.Box
8+
import androidx.compose.foundation.layout.Column
9+
import androidx.compose.foundation.layout.fillMaxSize
10+
import androidx.compose.foundation.layout.padding
11+
import androidx.compose.foundation.layout.width
12+
import androidx.compose.foundation.lazy.LazyColumn
13+
import androidx.compose.material3.Button
14+
import androidx.compose.material3.DropdownMenuItem
15+
import androidx.compose.material3.ExperimentalMaterial3Api
16+
import androidx.compose.material3.ExposedDropdownMenuAnchorType
17+
import androidx.compose.material3.ExposedDropdownMenuBox
18+
import androidx.compose.material3.ExposedDropdownMenuDefaults
19+
import androidx.compose.material3.OutlinedTextField
20+
import androidx.compose.material3.Text
21+
import androidx.compose.material3.TextField
22+
import androidx.compose.runtime.Composable
23+
import androidx.compose.runtime.getValue
24+
import androidx.compose.runtime.mutableStateOf
25+
import androidx.compose.runtime.remember
26+
import androidx.compose.runtime.setValue
27+
import androidx.compose.ui.Alignment
28+
import androidx.compose.ui.Modifier
29+
import androidx.compose.ui.text.font.FontWeight
30+
import androidx.compose.ui.text.style.TextAlign
31+
import androidx.compose.ui.unit.TextUnit
32+
import androidx.compose.ui.unit.dp
33+
import androidx.compose.ui.unit.sp
34+
import androidx.core.net.toUri
35+
36+
@Composable
37+
internal fun EntryScreen(text: String, block: @Composable () -> Unit = { }) {
38+
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
39+
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(10.dp)) {
40+
Text(text, fontWeight = FontWeight.Bold, fontSize = FONT_SIZE_TITLE)
41+
block()
42+
}
43+
}
44+
}
45+
46+
@Composable
47+
internal fun FriendsList(users: List<User>) {
48+
// display list of matching targets
49+
if (users.isEmpty()) {
50+
Text("List is Empty", fontWeight = FontWeight.Bold)
51+
} else {
52+
LazyColumn {
53+
items(users.size) { idx ->
54+
val user = users[idx]
55+
TextContent("${user.firstName}(${user.age}), ${user.location}")
56+
}
57+
}
58+
}
59+
}
60+
61+
/**
62+
* Displays a text input menu, may include several text fields
63+
*/
64+
@Composable
65+
internal fun MenuTextInput(
66+
menuLabels: List<String>,
67+
onValueChange: (String, String) -> Unit = { _, _ ->},
68+
) {
69+
Column {
70+
menuLabels.forEach { label ->
71+
var inputText by remember { mutableStateOf("") }
72+
73+
OutlinedTextField(
74+
value = inputText,
75+
onValueChange = {
76+
inputText = it
77+
onValueChange(label, it)
78+
},
79+
placeholder = { Text("enter integer") },
80+
label = { Text(label) },
81+
)
82+
}
83+
}
84+
85+
}
86+
87+
/**
88+
* Displays a drop down menu, may include multiple drop downs
89+
*/
90+
@Composable
91+
internal fun MenuDropDown(
92+
menuOptions: Map<String, List<String>>,
93+
onSelect: (label: String, selection: String) -> Unit = { _, _ ->},
94+
) {
95+
Column(
96+
modifier = Modifier.animateContentSize(),
97+
horizontalAlignment = Alignment.CenterHorizontally
98+
) {
99+
menuOptions.forEach { entry ->
100+
val key = entry.key
101+
ArgumentDropDownMenu(label = key, menuItemOptions = entry.value) { label, selection ->
102+
onSelect(key, selection)
103+
}
104+
}
105+
}
106+
}
107+
108+
// Display list of selections for one drop down
109+
@OptIn(ExperimentalMaterial3Api::class)
110+
@Composable
111+
private fun ArgumentDropDownMenu(
112+
label: String,
113+
menuItemOptions: List<String>,
114+
onSelect: (label: String, selection: String) -> Unit,
115+
) {
116+
val initValue = menuItemOptions.firstOrNull() ?: ""
117+
var expanded by remember { mutableStateOf(false) }
118+
var currSelected by remember { mutableStateOf(initValue) }
119+
Box(
120+
modifier = Modifier
121+
.padding(16.dp)
122+
) {
123+
ExposedDropdownMenuBox(
124+
expanded = expanded,
125+
onExpandedChange = { expanded = !expanded }
126+
) {
127+
TextField(
128+
readOnly = true,
129+
value = currSelected,
130+
onValueChange = { },
131+
label = { Text(label) },
132+
trailingIcon = {
133+
ExposedDropdownMenuDefaults.TrailingIcon(
134+
expanded = expanded
135+
)
136+
},
137+
modifier = Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, true)
138+
)
139+
ExposedDropdownMenu(
140+
expanded = expanded,
141+
onDismissRequest = {
142+
expanded = false
143+
}
144+
) {
145+
menuItemOptions.forEach { text ->
146+
DropdownMenuItem(
147+
text = { Text(text) },
148+
onClick = {
149+
expanded = false
150+
currSelected = text
151+
onSelect(label, text)
152+
}
153+
)
154+
}
155+
}
156+
}
157+
}
158+
}
159+
160+
@Composable
161+
internal fun DeepLinkButton(
162+
context: Context,
163+
targetActivity: Class<*>,
164+
deepLinkUrl: String,
165+
) {
166+
Button(
167+
onClick = {
168+
val intent = Intent(
169+
context,
170+
targetActivity
171+
)
172+
// start activity with the url
173+
intent.data = deepLinkUrl.toUri()
174+
context.startActivity(intent)
175+
}
176+
) {
177+
Text("Deeplink away!")
178+
}
179+
}
180+
181+
@Composable
182+
fun TextContent(text: String) {
183+
Text(
184+
text = text,
185+
modifier = Modifier.width(300.dp),
186+
textAlign = TextAlign.Center,
187+
fontSize = FONT_SIZE_TEXT,
188+
)
189+
}
190+
191+
internal val FONT_SIZE_TITLE: TextUnit = 20.sp
192+
internal val FONT_SIZE_TEXT: TextUnit = 15.sp

0 commit comments

Comments
 (0)