From e79249e34dbaefec48bd7719710f239e1aef2ee2 Mon Sep 17 00:00:00 2001 From: Ben Sagmoe Date: Mon, 3 Aug 2026 15:48:20 +0000 Subject: [PATCH 1/3] Add Navigation 3 deep link snippets and bump version --- .../navigation3/deeplinks/DeepLinkSnippets.kt | 275 ++++++++++++++++++ gradle/libs.versions.toml | 2 +- 2 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/DeepLinkSnippets.kt diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/DeepLinkSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/DeepLinkSnippets.kt new file mode 100644 index 000000000..5dea6d4eb --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/DeepLinkSnippets.kt @@ -0,0 +1,275 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.navigation3.deeplinks + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.deeplink.DeepLinkRequest +import androidx.navigation3.runtime.deeplink.DeepLinkMatcher +import androidx.navigation3.runtime.deeplink.DeepLinkUri +import androidx.navigation3.runtime.deeplink.UriDeepLinkMatcher +import androidx.navigation3.runtime.deeplink.StaticKeyDeepLinkMatcher +import androidx.navigation3.runtime.deeplink.BackStackMatchResult +import androidx.navigation3.runtime.deeplink.RequestExtrasKey +import androidx.navigation3.runtime.deeplink.withBackStack +import androidx.navigation3.runtime.deeplink.actionFilter +import androidx.navigation3.runtime.deeplink.actionExtra +import androidx.navigation3.runtime.deeplink.ActionExtrasKey +import androidx.navigation3.runtime.deeplink.DeepLinkRequest.Companion.MimeTypeExtrasKey +import androidx.navigation3.runtime.deeplink.invoke +import androidx.navigation3.runtime.deeplink.requestExtras +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer + +// Define keys used in snippets +@Serializable +data object HomeKey : NavKey + +@Serializable +data class UserProfileKey(val id: String) : NavKey + +@Serializable +data class UserListKey(val page: Int = 0) : NavKey + +@Serializable +data class Gallery(val id: String) : NavKey + +@Serializable +data class Editor(val id: String) : NavKey + +@Serializable +data object PreferencesScreen : NavKey + +@Serializable +data class DialerKey(val phoneNumber: String?) : NavKey + +@Serializable +data object MovieKey : NavKey + +@Serializable +data class MovieDetailsKey(val id: String) : NavKey + +enum class SortOrder { RELEVANCE, DATE, RATING } + +@Serializable +data class SearchFilters(val category: String, val sortBy: SortOrder) + +@Serializable +data class SearchKey(val query: String, val filters: SearchFilters) : NavKey + +enum class UserDetailsTab { INFO, ACTIVITY, SETTINGS } + +@Serializable +data class UserDetailsKey(val id: Int, val initialTab: UserDetailsTab = UserDetailsTab.INFO) : NavKey + +object CampaignIdExtrasKey : RequestExtrasKey + +class DeepLinkSnippets { + + fun requestCreation() { + // [START android_compose_navigation3_deeplinks_request] + // Create a request with only a URI + val request = DeepLinkRequest(uri = "https://www.example.com/home") + + // Create a request with a URI and action + val requestWithAction = DeepLinkRequest( + uri = "https://www.example.com/home", + extras = DeepLinkRequest.actionExtra("android.intent.action.VIEW") + ) + + // Create a request with URI, action and mimeType + val requestWithMimeType = DeepLinkRequest( + uri = "https://www.example.com/image", + extras = requestExtras { + put(DeepLinkRequest.ActionExtrasKey, "android.intent.action.VIEW") + put(MimeTypeExtrasKey, "image/png") + } + ) + // [END android_compose_navigation3_deeplinks_request] + } + + fun requestExtrasDsl() { + // [START android_compose_navigation3_deeplinks_extras_dsl] + val extras: Map = requestExtras { + put(MimeTypeExtrasKey, "application/json") + put(DeepLinkRequest.ActionExtrasKey, Intent.ACTION_VIEW) + } + + val mimeTypeExtraMap: Map = DeepLinkRequest.mimeTypeExtra("application/json") + // [END android_compose_navigation3_deeplinks_extras_dsl] + + // [START android_compose_navigation3_deeplinks_custom_extras] + // object CampaignIdExtrasKey : RequestExtrasKey // Defined outside + + val customExtras = requestExtras { + put(CampaignIdExtrasKey, "123") + } + // [END android_compose_navigation3_deeplinks_custom_extras] + } + + // [START android_compose_navigation3_deeplinks_filter] + val viewFilter = DeepLinkMatcher.actionFilter(Intent.ACTION_VIEW) + val editFilter = DeepLinkMatcher.actionFilter(Intent.ACTION_EDIT) + + val imageUriPattern = DeepLinkUri("www.example.com/image/{id}") + + val viewMatcher = UriDeepLinkMatcher(imageUriPattern, serializer(), filters = listOf(viewFilter)) + val editMatcher = UriDeepLinkMatcher(imageUriPattern, serializer(), filters = listOf(editFilter)) + // [END android_compose_navigation3_deeplinks_filter] + + fun staticMatcher() { + // [START android_compose_navigation3_deeplinks_static_matcher] + val preferencesActionFilter = DeepLinkMatcher.actionFilter(Intent.ACTION_APPLICATION_PREFERENCES) + + val preferencesActionDeepLinkMatcher = StaticKeyDeepLinkMatcher(PreferencesScreen, listOf(preferencesActionFilter)) + // [END android_compose_navigation3_deeplinks_static_matcher] + } + + fun uriMatcher() { + // [START android_compose_navigation3_deeplinks_uri_matcher] + // @Serializable data class UserProfileKey(val id: String): NavKey // Defined outside + + val userProfilePatternUri = DeepLinkUri("www.example.com/users/{id}") + val userProfileMatcher = UriDeepLinkMatcher(userProfilePatternUri, serializer()) + + val request = DeepLinkRequest("https://www.example.com/users/123") + val matchResult = userProfileMatcher.match(request) + val key = matchResult?.key // UserProfileKey(id = "123") + // [END android_compose_navigation3_deeplinks_uri_matcher] + } + + fun uriMatcherQuery() { + // [START android_compose_navigation3_deeplinks_uri_matcher_query] + // Defined outside: SortOrder, SearchFilters, SearchKey + + val pattern = DeepLinkUri("www.example.com/search?q={query}&category={category}&sortBy={sortBy}") + val matcher = UriDeepLinkMatcher(pattern, serializer()) + val request = DeepLinkRequest("https://www.example.com/search?q=kotlin&category=books&sortBy=DATE") + val matchResult = matcher.match(request) + val key = matchResult?.key // SearchKey(query = "kotlin", filters = SearchFilters(category = "books", sortBy = SortOrder.DATE)) + // [END android_compose_navigation3_deeplinks_uri_matcher_query] + } + + fun uriMatcherValidation() { + // [START android_compose_navigation3_deeplinks_uri_matcher_validation] + // Defined outside: UserDetailsTab, UserDetailsKey + + val matcher = UriDeepLinkMatcher( + DeepLinkUri("example.com/user/{id}?tab={initialTab}"), + serializer() + ) + // [END android_compose_navigation3_deeplinks_uri_matcher_validation] + } + + // [START android_compose_navigation3_deeplinks_custom_matcher] + // @Serializable data class DialerKey(val phoneNumber: String?) : NavKey // Defined outside + + class TelDeepLinkMatcher : DeepLinkMatcher>() { + override fun matchRequest(request: DeepLinkRequest): MatchResult? { + val uri = request.uri ?: return null + if (uri.scheme != "tel") return null + + // Note: schemeSpecificPart is only available on Android + val phoneNumber = uri.schemeSpecificPart ?: return null + return MatchResult(DialerKey(phoneNumber = phoneNumber)) + } + } + // [END android_compose_navigation3_deeplinks_custom_matcher] + + // [START android_compose_navigation3_deeplinks_custom_matcher_result] + class TelMatchResult( + key: DialerKey, + val isExactMatch: Boolean, + val patternLength: Int + ) : DeepLinkMatcher.MatchResult(key) { + override fun compareTo(other: DeepLinkMatcher.MatchResult): Int { + if (other !is TelMatchResult) { + // Determine precedence relative to other MatchResult types (e.g. UriMatchResult) + return 1 + } + + // An exact match wins over a wildcard/prefix match + if (isExactMatch && !other.isExactMatch) return 1 + if (!isExactMatch && other.isExactMatch) return -1 + + // The more specific (longer) pattern wins (e.g., tel:1800* vs tel:*) + val lengthDiff = this.patternLength - other.patternLength + if (lengthDiff != 0) { + return lengthDiff + } + + return 0 + } + } + // [END android_compose_navigation3_deeplinks_custom_matcher_result] + + fun backStackMatcher() { + // [START android_compose_navigation3_deeplinks_backstack] + val homeMatcher = StaticKeyDeepLinkMatcher(HomeKey, listOf(DeepLinkMatcher.actionFilter(Intent.ACTION_VIEW))) + + val userListMatcher = UriDeepLinkMatcher( + DeepLinkUri("www.example.com/users?page={page}"), + serializer() + ).withBackStack { matchResult -> + listOf(HomeKey, matchResult.key) + } + + val userProfileMatcher = UriDeepLinkMatcher( + DeepLinkUri("www.example.com/users/{id}"), + serializer() + ).withBackStack { matchResult -> + listOf(HomeKey, UserListKey(), matchResult.key) + } + // [END android_compose_navigation3_deeplinks_backstack] + } +} + +// [START android_compose_navigation3_deeplinks_match_request] +val deepLinkMatchers = listOf( + StaticKeyDeepLinkMatcher(HomeKey, listOf(DeepLinkMatcher.actionFilter(Intent.ACTION_VIEW))), + UriDeepLinkMatcher(DeepLinkUri("www.example.com/users/{id}"), serializer()), + DeepLinkSnippets.TelDeepLinkMatcher() +) + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // ... + val request = DeepLinkRequest(intent = intent) + val matchResult = deepLinkMatchers + .mapNotNull { it.match(request) } + .maxOrNull() + + val backStack = when (matchResult) { + null -> listOf(HomeKey) + is BackStackMatchResult<*, *> -> { + @Suppress("UNCHECKED_CAST") + matchResult.backStack as List + } + else -> listOf(matchResult.key) + } + } +} +// [END android_compose_navigation3_deeplinks_match_request] diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 52f66638b..35ac0d1d8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,7 +27,7 @@ androidx-lifecycle-compose = "2.11.0" androidx-lifecycle-runtime-compose = "2.11.0" androidx-lifecycle-viewmodel-navigation3 = "2.11.0" androidx-navigation = "2.9.8" -androidx-navigation3 = "1.1.3" +androidx-navigation3 = "1.2.0-alpha07" androidx-paging = "3.5.0" androidx-startup-runtime = "1.2.0" androidx-test = "1.7.0" From 9b6a89ef3e61f980ae94a55ee6403f323b4abd5b Mon Sep 17 00:00:00 2001 From: bsagmoe <13055315+bsagmoe@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:49:49 +0000 Subject: [PATCH 2/3] Apply Spotless --- .../navigation3/deeplinks/DeepLinkSnippets.kt | 58 +++++++++---------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/DeepLinkSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/DeepLinkSnippets.kt index 5dea6d4eb..aa40bf1ef 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/DeepLinkSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/DeepLinkSnippets.kt @@ -16,30 +16,24 @@ package com.example.compose.snippets.navigation3.deeplinks -import android.content.Context import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.remember -import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.deeplink.DeepLinkRequest +import androidx.navigation3.runtime.deeplink.ActionExtrasKey +import androidx.navigation3.runtime.deeplink.BackStackMatchResult import androidx.navigation3.runtime.deeplink.DeepLinkMatcher +import androidx.navigation3.runtime.deeplink.DeepLinkRequest +import androidx.navigation3.runtime.deeplink.DeepLinkRequest.Companion.MimeTypeExtrasKey import androidx.navigation3.runtime.deeplink.DeepLinkUri -import androidx.navigation3.runtime.deeplink.UriDeepLinkMatcher -import androidx.navigation3.runtime.deeplink.StaticKeyDeepLinkMatcher -import androidx.navigation3.runtime.deeplink.BackStackMatchResult import androidx.navigation3.runtime.deeplink.RequestExtrasKey -import androidx.navigation3.runtime.deeplink.withBackStack -import androidx.navigation3.runtime.deeplink.actionFilter +import androidx.navigation3.runtime.deeplink.StaticKeyDeepLinkMatcher +import androidx.navigation3.runtime.deeplink.UriDeepLinkMatcher import androidx.navigation3.runtime.deeplink.actionExtra -import androidx.navigation3.runtime.deeplink.ActionExtrasKey -import androidx.navigation3.runtime.deeplink.DeepLinkRequest.Companion.MimeTypeExtrasKey +import androidx.navigation3.runtime.deeplink.actionFilter import androidx.navigation3.runtime.deeplink.invoke import androidx.navigation3.runtime.deeplink.requestExtras +import androidx.navigation3.runtime.deeplink.withBackStack import kotlinx.serialization.Serializable import kotlinx.serialization.serializer @@ -200,28 +194,28 @@ class DeepLinkSnippets { // [START android_compose_navigation3_deeplinks_custom_matcher_result] class TelMatchResult( - key: DialerKey, - val isExactMatch: Boolean, - val patternLength: Int + key: DialerKey, + val isExactMatch: Boolean, + val patternLength: Int ) : DeepLinkMatcher.MatchResult(key) { - override fun compareTo(other: DeepLinkMatcher.MatchResult): Int { - if (other !is TelMatchResult) { - // Determine precedence relative to other MatchResult types (e.g. UriMatchResult) - return 1 - } + override fun compareTo(other: DeepLinkMatcher.MatchResult): Int { + if (other !is TelMatchResult) { + // Determine precedence relative to other MatchResult types (e.g. UriMatchResult) + return 1 + } - // An exact match wins over a wildcard/prefix match - if (isExactMatch && !other.isExactMatch) return 1 - if (!isExactMatch && other.isExactMatch) return -1 + // An exact match wins over a wildcard/prefix match + if (isExactMatch && !other.isExactMatch) return 1 + if (!isExactMatch && other.isExactMatch) return -1 - // The more specific (longer) pattern wins (e.g., tel:1800* vs tel:*) - val lengthDiff = this.patternLength - other.patternLength - if (lengthDiff != 0) { - return lengthDiff - } + // The more specific (longer) pattern wins (e.g., tel:1800* vs tel:*) + val lengthDiff = this.patternLength - other.patternLength + if (lengthDiff != 0) { + return lengthDiff + } - return 0 - } + return 0 + } } // [END android_compose_navigation3_deeplinks_custom_matcher_result] From 9ce7f60d3a4a550d36fa1bba8784f0bfb41916be Mon Sep 17 00:00:00 2001 From: Ben Sagmoe Date: Mon, 3 Aug 2026 15:49:55 +0000 Subject: [PATCH 3/3] Add deep link modularization snippets --- .../navigation3/deeplinks/DeepLinkSnippets.kt | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/DeepLinkSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/DeepLinkSnippets.kt index aa40bf1ef..0734771bb 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/DeepLinkSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/DeepLinkSnippets.kt @@ -34,6 +34,12 @@ import androidx.navigation3.runtime.deeplink.actionFilter import androidx.navigation3.runtime.deeplink.invoke import androidx.navigation3.runtime.deeplink.requestExtras import androidx.navigation3.runtime.deeplink.withBackStack +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.multibindings.IntoSet +import javax.inject.Inject import kotlinx.serialization.Serializable import kotlinx.serialization.serializer @@ -267,3 +273,43 @@ class MainActivity : ComponentActivity() { } } // [END android_compose_navigation3_deeplinks_match_request] + +// [START android_compose_navigation3_deeplinks_modularization_feature] +@Module +@InstallIn(ActivityRetainedComponent::class) +object FeatureADeepLinkModule { + @IntoSet + @Provides + fun provideUserMatcher(): DeepLinkMatcher<*, *> { + return UriDeepLinkMatcher( + DeepLinkUri("www.example.com/users/{id}"), + serializer() + ) + } +} +// [END android_compose_navigation3_deeplinks_modularization_feature] + +// [START android_compose_navigation3_deeplinks_modularization_app] +class MainActivityWithDI : ComponentActivity() { + @Inject + lateinit var deepLinkMatchers: Set<@JvmSuppressWildcards DeepLinkMatcher<*, *>> + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val request = DeepLinkRequest(intent = intent) + val matchResult = deepLinkMatchers + .mapNotNull { it.match(request) } + .maxOrNull() + + val backStack = when (matchResult) { + null -> listOf(HomeKey) + is BackStackMatchResult<*, *> -> { + @Suppress("UNCHECKED_CAST") + matchResult.backStack as List + } + else -> listOf(matchResult.key) + } + } +} +// [END android_compose_navigation3_deeplinks_modularization_app]