Công thức về đối số URI của đường liên kết sâu
Công thức này minh hoạ cách phân tích cú pháp URL đường liên kết sâu từ một Ý định của Android thành một khoá Điều hướng.
Thành phần của công thức
Ứng dụng này bao gồm hai hoạt động
UriWithArgumentsDeepLinkActivitytạo và kích hoạt yêu cầu liên kết sâuMainActivityphân tích cú pháp ý định thành khoá điều hướng mục tiêu.
Cách hoạt động
MainActivity xử lý yêu cầu theo các bước sau
-
Khai báo một
UriDeepLinkMatchercho mỗi mẫu URL có thể được liên kết sâu. Mỗi đối tượng so khớp chấp nhận một mẫu uri và KSerializer của NavKey hỗ trợ đường liên kết sâu này. -
Tạo một
DeepLinkRequestbằng ý định đến. -
So khớp tất cả
UriDeepLinkMatchersđề xuất với yêu cầu và so sánhUriMatchResultsthu được để tìm ra kết quả phù hợp nhất. -
Đọc khoá khớp từ
UriMatchResult.keyhoặc sử dụng khoá mặc định nếu không có khoá nào khớp.
Công thức này tập trung vào việc xử lý một ý định và không bao gồm những điểm cần cân nhắc sau:
- Tạo backStack tổng hợp
- Thiết lập nhiều mô-đun
- DI
- Quản lý TaskStack
- Nút mũi tên lên so với nút quay lại
Các dạng đường liên kết sâu được minh hoạ
MainActivity có một số khoá backStack để minh hoạ các loại đường liên kết sâu được hỗ trợ:
HomeKey– đường liên kết sâu có URL chính xác (không có đối số đường liên kết sâu)UsersKey– đường liên kết sâu có đối số đường dẫnSearchKey– đường liên kết sâu có đối số truy vấn
Hãy xem MainActivity.deepLinkMatchers để biết mẫu URL thực tế của từng mẫu.
package com.example.nav3recipes.deeplink.handlerequests.uriarguments import androidx.navigation3.runtime.NavKey import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.STRING_LITERAL_FILTER import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.STRING_LITERAL_HOME import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.STRING_LITERAL_SEARCH import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.STRING_LITERAL_USERS import kotlinx.serialization.Serializable internal interface NavRecipeKey: NavKey { val name: String } @Serializable internal object HomeKey: NavRecipeKey { override val name: String = STRING_LITERAL_HOME } @Serializable internal data class UsersKey( val filter: String, ): NavRecipeKey { override val name: String = STRING_LITERAL_USERS companion object { const val FILTER_KEY = STRING_LITERAL_FILTER const val FILTER_OPTION_RECENTLY_ADDED = "recentlyAdded" const val FILTER_OPTION_ALL = "all" } } @Serializable internal data class SearchKey( val firstName: String? = null, val ageMin: Int? = null, val ageMax: Int? = null, val location: String? = null, ): NavRecipeKey { override val name: String = STRING_LITERAL_SEARCH }
package com.example.nav3recipes.deeplink.handlerequests.uriarguments import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.core.net.toUri import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.deeplink.DeepLinkRequest import androidx.navigation3.runtime.deeplink.UriDeepLinkMatcher import androidx.navigation3.runtime.deeplink.invoke import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.ui.NavDisplay import com.example.nav3recipes.common.deeplink.EntryScreen import com.example.nav3recipes.common.deeplink.FriendsList import com.example.nav3recipes.common.deeplink.LIST_USERS import com.example.nav3recipes.common.deeplink.TextContent import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.URL_HOME_EXACT import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.URL_SEARCH import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.URL_USERS_WITH_FILTER import com.example.nav3recipes.ui.setEdgeToEdgeConfig import kotlinx.serialization.serializer /** * See README.md for how this recipe works. */ class MainActivity : ComponentActivity() { /** STEP 1. Declare supported deep links */ internal val deepLinkMatchers: List<UriDeepLinkMatcher<NavKey>> = listOf( // "https://www.nav3recipes.com/home" UriDeepLinkMatcher(URL_HOME_EXACT.toUri(), serializer<HomeKey>()), // "https://www.nav3recipes.com/users/with/{filter}" UriDeepLinkMatcher(URL_USERS_WITH_FILTER.toUri(), serializer<UsersKey>()), // "https://www.nav3recipes.com/users/search?{firstName}&{age}&{location}" UriDeepLinkMatcher(URL_SEARCH.toUri(), serializer<SearchKey>()), ) override fun onCreate(savedInstanceState: Bundle?) { setEdgeToEdgeConfig() super.onCreate(savedInstanceState) /** STEP 2. Create a [DeepLinkRequest] from the intent */ val request = DeepLinkRequest(intent) /** STEP 3. Match the request to the DeepLinkMatchers*/ // First get all the possible matching UriMatchResult val matches = deepLinkMatchers.mapNotNull { // returns null if no match it.match(request) } // compare all matches to find best match val bestMatch = matches.maxOrNull() /** STEP 4. Get the key from the match or use default key if no match*/ val key = bestMatch?.key ?: HomeKey /** * STEP 5. pass the initial key to backstack */ setContent { val backStack: NavBackStack<NavKey> = rememberNavBackStack(key) NavDisplay( backStack = backStack, onBack = { backStack.removeLastOrNull() }, entryProvider = entryProvider { entry<HomeKey> { key -> EntryScreen(key.name) { TextContent("<matches exact url>") } } entry<UsersKey> { key -> EntryScreen("${key.name} : ${key.filter}") { TextContent("<matches path argument>") val list = when { key.filter.isEmpty() -> LIST_USERS key.filter == UsersKey.FILTER_OPTION_ALL -> LIST_USERS else -> LIST_USERS.take(5) } FriendsList(list) } } entry<SearchKey> { search -> EntryScreen(search.name) { TextContent("<matches query parameters, if any>") val matchingUsers = LIST_USERS.filter { user -> (search.firstName == null || user.firstName == search.firstName) && (search.location == null || user.location == search.location) && (search.ageMin == null || user.age >= search.ageMin) && (search.ageMax == null || user.age <= search.ageMax) } FriendsList(matchingUsers) } } } ) } } }
package com.example.nav3recipes.deeplink.handlerequests.uriarguments import android.content.Intent 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 androidx.core.net.toUri import androidx.lifecycle.compose.dropUnlessResumed import com.example.nav3recipes.common.deeplink.EMPTY import com.example.nav3recipes.common.deeplink.EntryScreen import com.example.nav3recipes.common.deeplink.FIRST_NAME_JOHN import com.example.nav3recipes.common.deeplink.FIRST_NAME_JULIE import com.example.nav3recipes.common.deeplink.FIRST_NAME_MARY import com.example.nav3recipes.common.deeplink.FIRST_NAME_TOM import com.example.nav3recipes.common.deeplink.LOCATION_BC import com.example.nav3recipes.common.deeplink.LOCATION_BR import com.example.nav3recipes.common.deeplink.LOCATION_CA import com.example.nav3recipes.common.deeplink.LOCATION_US import com.example.nav3recipes.common.deeplink.MenuDropDown import com.example.nav3recipes.common.deeplink.MenuTextInput import com.example.nav3recipes.common.deeplink.PaddedButton import com.example.nav3recipes.common.deeplink.TextContent import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.PATH_BASE import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.PATH_INCLUDE import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.PATH_SEARCH import com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui.STRING_LITERAL_HOME import com.example.nav3recipes.ui.setEdgeToEdgeConfig /** * See README.md for how this recipe works. * * See [MainActivity] for how the requested deeplink is handled. */ class UriWithArgumentsDeepLinkActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { setEdgeToEdgeConfig() 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 PaddedButton("Deeplink Away!", onClick = dropUnlessResumed { val intent = Intent( this@UriWithArgumentsDeepLinkActivity, MainActivity::class.java ) // start activity with the url intent.data = finalUrl.toUri() startActivity(intent) }) } } } } 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)
package com.example.nav3recipes.deeplink.handlerequests.uriarguments.ui import com.example.nav3recipes.deeplink.handlerequests.uriarguments.SearchKey /** * 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}}"