Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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.parseintent.singleModule.ParseIntentLandingActivity"
android:exported="true"
android:theme="@style/Theme.Nav3Recipes">
</activity>
<activity
android:name=".deeplink.parseintent.singleModule.ParseIntentActivity"
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.parseintent.singleModule.ParseIntentLandingActivity
Copy link
Collaborator

Choose a reason for hiding this comment

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

The package name could just be com.example.nav3recipes.deeplinks and all the library code could be placed in a subpackage say common or library. We could make it clear that to add deeplinks to your app you can just copy the library package into your own app. WDYT?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it is really important for each recipe to stand on its own - the simple example should include only simple code.

Trying to make just a single shared library means that to understand even what the simple recipe is doing you have to understand a library that handles way more than what is needed for that recipe.

I do think we should separate the recipe into the activity code (e.g., the example of how to use it) from the library code (e.g., what we expect developers to use in their own app if they want to 'adopt' that specific recipe).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated with

1. Simplified the base package from "deeplink.parseIntent.singleModule" to "deeplink.basic".
2. Added `ui` and `deeplinkutil` packages to separate sample ui code from parsing/matching helpers

WDYT?

Copy link
Collaborator

Choose a reason for hiding this comment

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

You can simplify deeplinkutil to just util since it's a subpackage of deeplink.basic

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", ParseIntentLandingActivity::class.java),
)

class RecipePickerActivity : ComponentActivity() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.example.nav3recipes.deeplink.parseintent.singleModule

/**
* 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),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package com.example.nav3recipes.deeplink.parseintent.singleModule

import android.content.Context
import android.content.Intent
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.net.toUri

@Composable
internal fun EntryScreen(text: String, block: @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)
block()
}
}
}

@Composable
internal fun FriendsList(users: List<User>) {
// display list of matching targets
if (users.isEmpty()) {
Text("List is Empty", fontWeight = FontWeight.Bold)
} else {
LazyColumn {
items(users.size) { idx ->
val user = users[idx]
TextContent("${user.firstName}(${user.age}), ${user.location}")
}
}
}
}

/**
* Displays a text input menu, may include several text fields
*/
@Composable
internal fun MenuTextInput(
menuLabels: List<String>,
onValueChange: (String, String) -> Unit = { _, _ ->},
) {
Column {
menuLabels.forEach { label ->
var inputText by remember { mutableStateOf("") }

OutlinedTextField(
value = inputText,
onValueChange = {
inputText = it
onValueChange(label, it)
},
placeholder = { Text("enter integer") },
label = { Text(label) },
)
}
}

}

/**
* Displays a drop down menu, may include multiple drop downs
*/
@Composable
internal fun MenuDropDown(
menuOptions: Map<String, List<String>>,
onSelect: (label: String, selection: String) -> Unit = { _, _ ->},
) {
Column(
modifier = Modifier.animateContentSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
menuOptions.forEach { entry ->
val key = entry.key
ArgumentDropDownMenu(label = key, menuItemOptions = entry.value) { label, selection ->
onSelect(key, selection)
}
}
}
}

// Display list of selections for one drop down
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ArgumentDropDownMenu(
label: String,
menuItemOptions: List<String>,
onSelect: (label: String, selection: String) -> Unit,
) {
val initValue = menuItemOptions.firstOrNull() ?: ""
var expanded by remember { mutableStateOf(false) }
var currSelected by remember { mutableStateOf(initValue) }
Box(
modifier = Modifier
.padding(16.dp)
) {
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = !expanded }
) {
TextField(
readOnly = true,
value = currSelected,
onValueChange = { },
label = { Text(label) },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
},
modifier = Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, true)
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
menuItemOptions.forEach { text ->
DropdownMenuItem(
text = { Text(text) },
onClick = {
expanded = false
currSelected = text
onSelect(label, text)
}
)
}
}
}
}
}

@Composable
internal fun DeepLinkButton(
context: Context,
targetActivity: Class<*>,
deepLinkUrl: String,
) {
Button(
onClick = {
val intent = Intent(
context,
targetActivity
)
// start activity with the url
intent.data = deepLinkUrl.toUri()
context.startActivity(intent)
}
) {
Text("Deeplink away!")
}
}

@Composable
fun TextContent(text: String) {
Text(
text = text,
modifier = Modifier.width(300.dp),
textAlign = TextAlign.Center,
fontSize = FONT_SIZE_TEXT,
)
}

internal val FONT_SIZE_TITLE: TextUnit = 20.sp
internal val FONT_SIZE_TEXT: TextUnit = 15.sp
Loading