diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/CreateMatchersSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/CreateMatchersSnippets.kt new file mode 100644 index 000000000..ab3d9fbd5 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/CreateMatchersSnippets.kt @@ -0,0 +1,73 @@ +/* + * 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.Intent +import androidx.navigation3.runtime.deeplink.DeepLinkMatcher +import androidx.navigation3.runtime.deeplink.DeepLinkUri +import androidx.navigation3.runtime.deeplink.StaticKeyDeepLinkMatcher +import androidx.navigation3.runtime.deeplink.UriDeepLinkMatcher +import androidx.navigation3.runtime.deeplink.actionFilter +import androidx.navigation3.runtime.deeplink.withBackStack +import kotlinx.serialization.serializer + +object CreateMatchersSnippets { + + // [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 filterLambda() { + // [START android_compose_navigation3_deeplinks_filter_lambda] + val myFilter = DeepLinkMatcher.Filter { request -> request.uri != null } + // [END android_compose_navigation3_deeplinks_filter_lambda] + } + + 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 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] + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/CustomMatchersSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/CustomMatchersSnippets.kt new file mode 100644 index 000000000..08eca0373 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/CustomMatchersSnippets.kt @@ -0,0 +1,63 @@ +/* + * 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 androidx.navigation3.runtime.deeplink.DeepLinkMatcher +import androidx.navigation3.runtime.deeplink.DeepLinkRequest +import androidx.navigation3.runtime.deeplink.WrappedMatchResult + +// [START android_compose_navigation3_deeplinks_custom_matcher] +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 { + // Unwrap if the other result is wrapped in a BackStackMatchResult or custom WrappedMatchResult + val target = if (other is WrappedMatchResult<*>) other.matchResult else other + if (target !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 && !target.isExactMatch) return 1 + if (!isExactMatch && target.isExactMatch) return -1 + + // The more specific (longer) pattern wins (e.g., tel:1800* versus tel:*) + val lengthDiff = this.patternLength - target.patternLength + if (lengthDiff != 0) { + return lengthDiff + } + + return 0 + } +} +// [END android_compose_navigation3_deeplinks_custom_matcher_result] 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..087704649 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/DeepLinkSnippets.kt @@ -0,0 +1,205 @@ +/* + * 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.Intent +import android.net.Uri +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.navigation3.runtime.NavKey +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.DeepLinkUri +import androidx.navigation3.runtime.deeplink.IntentExtrasKey +import androidx.navigation3.runtime.deeplink.RequestExtras +import androidx.navigation3.runtime.deeplink.RequestExtrasKey +import androidx.navigation3.runtime.deeplink.StaticKeyDeepLinkMatcher +import androidx.navigation3.runtime.deeplink.UriDeepLinkMatcher +import androidx.navigation3.runtime.deeplink.actionExtra +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 androidx.savedstate.SavedState +import androidx.savedstate.read +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer + +// Shared keys used across 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 + +object DeepLinkSnippets { + + fun requestCreation() { + // [START android_compose_navigation3_deeplinks_request] + // Create a request with a String URI + val request = DeepLinkRequest(uri = "https://www.example.com/home") + + // Create a request from a DeepLinkUri + val deepLinkUri = DeepLinkUri("https://www.example.com/home") + val requestFromUri = DeepLinkRequest(uri = deepLinkUri) + + // 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(DeepLinkRequest.Companion.MimeTypeExtrasKey, "image/png") + } + ) + // [END android_compose_navigation3_deeplinks_request] + } + + fun requestExtrasDsl() { + // [START android_compose_navigation3_deeplinks_extras_dsl] + val extras: RequestExtras = requestExtras { + put(DeepLinkRequest.Companion.MimeTypeExtrasKey, "application/json") + put(DeepLinkRequest.ActionExtrasKey, Intent.ACTION_VIEW) + } + + // Access typed values using the get operator + val mimeType: String? = extras[DeepLinkRequest.Companion.MimeTypeExtrasKey] + val action: String? = extras[DeepLinkRequest.ActionExtrasKey] + + // Create extras using helper functions and combine them + val mimeTypeExtras: RequestExtras = DeepLinkRequest.mimeTypeExtra("application/json") + val combinedExtras: RequestExtras = extras + DeepLinkRequest.actionExtra(Intent.ACTION_VIEW) + // [END android_compose_navigation3_deeplinks_extras_dsl] + } + + object CustomExtrasSnippet { + // [START android_compose_navigation3_deeplinks_custom_extras] + // Define a custom typed key: + object CampaignIdExtrasKey : RequestExtrasKey + + val customExtras: RequestExtras = requestExtras { + put(CampaignIdExtrasKey, "123") + } + + val campaignId: String? = customExtras[CampaignIdExtrasKey] + // [END android_compose_navigation3_deeplinks_custom_extras] + } + + object RequestIntentSnippet { + // [START android_compose_navigation3_deeplinks_request_intent] + object CampaignIdExtrasKey : RequestExtrasKey + + val intent = Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse("https://www.example.com/item/42") + type = "application/json" + putExtra("user_id", "123") + } + + val request = DeepLinkRequest( + intent = intent, + extras = requestExtras { + put(CampaignIdExtrasKey, "spring_promo") + } + ) + + // The resulting DeepLinkRequest contains: + val uri = request.uri // "https://www.example.com/item/42" + val action = request.extras[DeepLinkRequest.ActionExtrasKey] // "android.intent.action.VIEW" + val mimeType = request.extras[DeepLinkRequest.Companion.MimeTypeExtrasKey] // "application/json" + val intentExtras: SavedState? = + request.extras[DeepLinkRequest.IntentExtrasKey] + val userId: String? = intentExtras?.read { getStringOrNull("user_id") } // "123" + val campaignId: String? = request.extras[CampaignIdExtrasKey] // "spring_promo" + // [END android_compose_navigation3_deeplinks_request_intent] + } +} + +// [START android_compose_navigation3_deeplinks_match_request] +// 1. Instantiate your DeepLinkMatcher instances. +val homeMatcher = StaticKeyDeepLinkMatcher(HomeKey, listOf(DeepLinkMatcher.actionFilter(Intent.ACTION_VIEW))) + +val userProfileMatcher = UriDeepLinkMatcher( + DeepLinkUri("www.example.com/users/{id}"), + serializer() +).withBackStack { matchResult -> + listOf(HomeKey, matchResult.key) +} + +val telMatcher = TelDeepLinkMatcher() + +// 2. Collate all of your DeepLinkMatcher instances. +// Note: Collating matchers with different generic types requires wildcards, +// erasing the specific generic types. +val deepLinkMatchers: List> = listOf( + homeMatcher, + userProfileMatcher, + telMatcher +) + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // ... + + // 3. Create a DeepLinkRequest from the incoming Intent + val request = DeepLinkRequest(intent = intent) + + // 4. Match the request against all matchers and find the best match. + // Because DeepLinkMatcher.MatchResult implements Comparable, you can + // use maxOrNull() to find the best match. + val matchResult = deepLinkMatchers + .mapNotNull { it.match(request) } // List> + .maxOrNull() // DeepLinkMatcher.MatchResult<*>? + + // 5. Create the back stack from the match result (or fall back to a default). + val backStack: List = when (matchResult) { + // If no match is found, use the default back stack (e.g., HomeKey) + null -> listOf(HomeKey) + // If a BackStackMatchResult is found, use the back stack from the result + is BackStackMatchResult<*, *> -> { + // Because star-projected matchers erase the key type, cast the back stack to List. + @Suppress("UNCHECKED_CAST") + matchResult.backStack as List + } + // Otherwise, use the key from the match result to make a single-item back stack + else -> listOf(matchResult.key as NavKey) + } + } +} +// [END android_compose_navigation3_deeplinks_match_request] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/UriMatcherSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/UriMatcherSnippets.kt new file mode 100644 index 000000000..825a9d249 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/deeplinks/UriMatcherSnippets.kt @@ -0,0 +1,265 @@ +/* + * 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 androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.deeplink.DeepLinkRequest +import androidx.navigation3.runtime.deeplink.DeepLinkSerializer +import androidx.navigation3.runtime.deeplink.DeepLinkUri +import androidx.navigation3.runtime.deeplink.UriDeepLinkMatcher +import androidx.navigation3.runtime.deeplink.UriMatchResult +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.serializer + +object UriMatcherSnippets { + + object UriMatcherSnippet { + // [START android_compose_navigation3_deeplinks_uri_matcher] + @Serializable + data class UserProfileKey(val id: String) : NavKey + + val userProfilePattern = DeepLinkUri("www.example.com/users/{id}") + val userProfileMatcher = UriDeepLinkMatcher(userProfilePattern, serializer()) + + val request = DeepLinkRequest(uri = "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] + } + + object UriMatcherPrimitivesSnippet { + // [START android_compose_navigation3_deeplinks_uri_matcher_primitives] + @Serializable + data class UserProfileKey(val id: Int) : NavKey + + val matcher = UriDeepLinkMatcher( + DeepLinkUri("www.example.com/users/{id}"), + serializer() + ) + + val request = DeepLinkRequest(uri = "https://www.example.com/users/123") + val key = matcher.match(request)?.key // UserProfileKey(id = 123) + // [END android_compose_navigation3_deeplinks_uri_matcher_primitives] + } + + object UriMatcherEnumsSnippet { + // [START android_compose_navigation3_deeplinks_uri_matcher_enums] + enum class SortOrder { RELEVANCE, DATE, POPULARITY } + + @Serializable + data class ProductsKey(val sort: SortOrder) : NavKey + + val matcher = UriDeepLinkMatcher( + DeepLinkUri("www.example.com/products?sort={sort}"), + serializer() + ) + + val request = DeepLinkRequest(uri = "https://www.example.com/products?sort=DATE") + val key = matcher.match(request)?.key // ProductsKey(sort = SortOrder.DATE) + // [END android_compose_navigation3_deeplinks_uri_matcher_enums] + } + + object UriMatcherRepeatedQuerySnippet { + // [START android_compose_navigation3_deeplinks_uri_matcher_repeated_query] + @Serializable + data class FilteredItemsKey(val ids: List) : NavKey + + val matcher = UriDeepLinkMatcher( + DeepLinkUri("www.example.com/items?id={ids}"), + serializer() + ) + + val request = DeepLinkRequest(uri = "https://www.example.com/items?id=10&id=20") + val key = matcher.match(request)?.key // FilteredItemsKey(ids = listOf(10, 20)) + // [END android_compose_navigation3_deeplinks_uri_matcher_repeated_query] + } + + object UriMatcherQuerySnippet { + // [START android_compose_navigation3_deeplinks_uri_matcher_query] + enum class SortOrder { RELEVANCE, DATE, POPULARITY } + + @Serializable + data class SearchFilters( + val category: String, + val sortBy: SortOrder = SortOrder.RELEVANCE + ) + + @Serializable + data class SearchKey( + val query: String, + val page: Int = 1, + // Flattened into {category} and {sortBy} + val filters: SearchFilters + ) : NavKey + + val matcher = UriDeepLinkMatcher( + DeepLinkUri("www.example.com/search?q={query}&page={page}&category={category}&sortBy={sortBy}"), + serializer() + ) + + val request = DeepLinkRequest(uri = "https://www.example.com/search?q=kotlin&category=books&sortBy=DATE") + val key = matcher.match(request)?.key + // SearchKey(query = "kotlin", page = 1, filters = SearchFilters(category = "books", sortBy = SortOrder.DATE)) + // [END android_compose_navigation3_deeplinks_uri_matcher_query] + } + + // [START android_compose_navigation3_deeplinks_filter_serializer] + @Serializable + data class Filter(val key: String, val value: String) + + object FilterSerializer : DeepLinkSerializer() { + override val serialName: String = "com.example.Filter" + + override fun deserialize(value: String): Filter { + val parts = value.split(":", limit = 2) + if (parts.size < 2) { + throw SerializationException("Invalid filter: $value. Expected key:value.") + } + return Filter(key = parts[0], value = parts[1]) + } + + override fun serialize(value: Filter): String = "${value.key}:${value.value}" + } + // [END android_compose_navigation3_deeplinks_filter_serializer] + + object CustomObjectSnippet { + // [START android_compose_navigation3_deeplinks_uri_matcher_custom_object] + @Serializable + data class CatalogKey( + @Serializable(with = FilterSerializer::class) + val filter: Filter + ) : NavKey + + val matcher = UriDeepLinkMatcher( + DeepLinkUri("www.example.com/catalog?filter={filter}"), + serializer() + ) + + val request = DeepLinkRequest(uri = "https://www.example.com/catalog?filter=brand:google") + val key = matcher.match(request)?.key // CatalogKey(filter = Filter("brand", "google")) + // [END android_compose_navigation3_deeplinks_uri_matcher_custom_object] + } + + object CustomSerializerCollectionSnippet { + // [START android_compose_navigation3_deeplinks_custom_serializer] + @Serializable + data class SearchResultsKey( + val query: String, + val filters: List<@Serializable(with = FilterSerializer::class) Filter> = emptyList() + ) : NavKey + + val searchResultsPattern = DeepLinkUri("www.example.com/search?q={query}&filter={filters}") + val searchResultsMatcher = UriDeepLinkMatcher(searchResultsPattern, serializer()) + + val request = DeepLinkRequest(uri = "https://www.example.com/search?q=phone&filter=brand:google&filter=color:hazel") + val matchResult = searchResultsMatcher.match(request) + val key = matchResult?.key + // SearchResultsKey(query = "phone", filters = listOf(Filter("brand", "google"), Filter("color", "hazel"))) + // [END android_compose_navigation3_deeplinks_custom_serializer] + } + + object CsvSnippet { + // [START android_compose_navigation3_deeplinks_uri_matcher_csv] + object IntListCsvSerializer : DeepLinkSerializer>() { + override val serialName: String = "com.example.IntListCsv" + + override fun deserialize(value: String): List { + if (value.isEmpty()) return emptyList() + return value.split(",").map { it.trim().toInt() } + } + + override fun serialize(value: List): String = value.joinToString(",") + } + + @Serializable + data class ItemListKey( + @Serializable(with = IntListCsvSerializer::class) + val ids: List + ) : NavKey + + val itemListPattern = DeepLinkUri("www.example.com/items/{ids}") + val itemListMatcher = UriDeepLinkMatcher(itemListPattern, serializer()) + + val request = DeepLinkRequest(uri = "https://www.example.com/items/10,20,30") + val key = itemListMatcher.match(request)?.key // ItemListKey(ids = listOf(10, 20, 30)) + // [END android_compose_navigation3_deeplinks_uri_matcher_csv] + } + + object MismatchesSnippet { + // [START android_compose_navigation3_deeplinks_uri_matcher_mismatches] + enum class MapLayer { STANDARD, SATELLITE, TERRAIN } + + @Serializable + data class LayerOptions( + val style: String, + val layer: MapLayer = MapLayer.STANDARD + ) + + @Serializable + data class MapKey( + val location: String, + val zoom: Int = 12, + val options: LayerOptions + ) : NavKey + + val matcher = UriDeepLinkMatcher( + DeepLinkUri("www.example.com/map/{location}?zoom={zoom}&style={style}&layer={layer}"), + serializer() + ) + // [END android_compose_navigation3_deeplinks_uri_matcher_mismatches] + } + + // [START android_compose_navigation3_deeplinks_uri_matcher_unsupported] + // [START_EXCLUDE silent] + object InvalidMapScope { + // [END_EXCLUDE] + // Throws IllegalArgumentException: Map decoding is not supported. + @Serializable + data class InvalidKey(val tags: Map) : NavKey + // [START_EXCLUDE silent] + } + + object InvalidListScope { + // [END_EXCLUDE] + // Throws SerializationException: Only collections of primitives are supported. + @Serializable + data class InvalidKey(val filters: List) : NavKey + // [START_EXCLUDE silent] + } + // [END_EXCLUDE] + // [END android_compose_navigation3_deeplinks_uri_matcher_unsupported] + + // [START android_compose_navigation3_deeplinks_custom_uri_matcher] + class LegacyPrefixUriDeepLinkMatcher( + uriPattern: DeepLinkUri, + serializer: KSerializer + ) : UriDeepLinkMatcher(uriPattern, serializer) { + + override fun matchUri(uri: DeepLinkUri): UriMatchResult? { + val path = uri.path + val normalizedUri = if (path != null && path.startsWith("/legacy/")) { + DeepLinkUri(uri.toString().replaceFirst("/legacy", "")) + } else { + uri + } + return super.matchUri(normalizedUri) + } + } + // [END android_compose_navigation3_deeplinks_custom_uri_matcher] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/migration/NavigationMigrationAfter.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/migration/NavigationMigrationAfter.kt index 3a52756c3..938bb92f8 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/migration/NavigationMigrationAfter.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/migration/NavigationMigrationAfter.kt @@ -27,7 +27,17 @@ import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.scene.DialogSceneStrategy import androidx.navigation3.ui.NavDisplay +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.navigation3.runtime.deeplink.BackStackMatchResult +import androidx.navigation3.runtime.deeplink.DeepLinkMatcher +import androidx.navigation3.runtime.deeplink.DeepLinkRequest +import androidx.navigation3.runtime.deeplink.DeepLinkUri +import androidx.navigation3.runtime.deeplink.UriDeepLinkMatcher +import androidx.navigation3.runtime.deeplink.invoke import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer // Dummy screens for compilation @Composable private fun ScreenA(title: String) {} @@ -141,3 +151,44 @@ private object SnippetLifecycleAfter { // [END android_compose_navigation3_lifecycle_after] } } + +private object SnippetDeepLinksAfter { + @Serializable data class RouteA(val id: String) : NavKey + @Serializable data object HomeKey : NavKey + + // [START android_compose_navigation3_deeplinks_after_matcher] + val userMatcher = UriDeepLinkMatcher( + DeepLinkUri("www.example.com/user/{id}"), + serializer() + ) + // [END android_compose_navigation3_deeplinks_after_matcher] + + // [START android_compose_navigation3_deeplinks_after_activity] + val deepLinkMatchers: List> = listOf( + userMatcher, + ) + + 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) + } + + // Use backStack with NavDisplay + } + } + // [END android_compose_navigation3_deeplinks_after_activity] +} + diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/migration/NavigationMigrationBefore.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/migration/NavigationMigrationBefore.kt index 298fd72f0..b0f135731 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/migration/NavigationMigrationBefore.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/migration/NavigationMigrationBefore.kt @@ -31,6 +31,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.dialog import androidx.navigation.compose.navigation +import androidx.navigation.navDeepLink import androidx.navigation.toRoute import kotlinx.serialization.Serializable import kotlin.reflect.KClass @@ -124,3 +125,20 @@ private object SnippetLifecycleBefore { // [END android_compose_navigation3_lifecycle_before] } } + +private object SnippetDeepLinksBefore { + @Serializable data class RouteA(val id: String) + + fun NavGraphBuilder.deepLinksBefore() { + // [START android_compose_navigation3_deeplinks_before] + composable( + deepLinks = listOf( + navDeepLink { uriPattern = "www.example.com/user/{id}" } + ) + ) { + // ... + } + // [END android_compose_navigation3_deeplinks_before] + } +} + diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/modularization/ModularizationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/modularization/ModularizationSnippets.kt index b666fa050..0a58f4499 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/modularization/ModularizationSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation3/modularization/ModularizationSnippets.kt @@ -22,6 +22,12 @@ import androidx.activity.compose.setContent import androidx.compose.runtime.Composable import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.deeplink.BackStackMatchResult +import androidx.navigation3.runtime.deeplink.DeepLinkMatcher +import androidx.navigation3.runtime.deeplink.DeepLinkRequest +import androidx.navigation3.runtime.deeplink.DeepLinkUri +import androidx.navigation3.runtime.deeplink.UriDeepLinkMatcher +import androidx.navigation3.runtime.deeplink.invoke import androidx.navigation3.runtime.entryProvider import androidx.navigation3.ui.NavDisplay import com.example.compose.snippets.navigation3.ContentGreen @@ -29,10 +35,13 @@ import com.example.compose.snippets.navigation3.ContentRed import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.components.ActivityRetainedComponent import dagger.multibindings.IntoSet import javax.inject.Inject import kotlin.collections.emptyList +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer data object KeyA : NavKey data object KeyA2 : NavKey @@ -123,5 +132,55 @@ class MainActivity : ComponentActivity() { } // [END android_compose_navigation3_modularization_4] +@Serializable +private data class UserProfileKey(val id: String) : NavKey + +private data object HomeKey : NavKey + +// [START android_compose_navigation3_deeplinks_modularization_feature] +@Module +@InstallIn(ActivityRetainedComponent::class) +object FeatureADeepLinkModule { + @IntoSet + @Provides + // [START_EXCLUDE silent] + @JvmSuppressWildcards + // [END_EXCLUDE] + 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] +@AndroidEntryPoint +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] + + diff --git a/datastore/build.gradle.kts b/datastore/build.gradle.kts index d38b2b191..1fb477be1 100644 --- a/datastore/build.gradle.kts +++ b/datastore/build.gradle.kts @@ -24,7 +24,7 @@ android { defaultConfig { applicationId = "com.example.datastore.snippets" - minSdk = 23 + minSdk = 24 targetSdk = 37 versionCode = 1 versionName = "1.0" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d5bf8f4e9..89b7dae56 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,7 +28,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.5" +androidx-navigation3 = "1.2.0-beta01" androidx-paging = "3.5.0" androidx-startup-runtime = "1.2.0" androidx-test = "1.7.0"