自定义 DeepLinkMatcher 配方

此配方演示了如何在 Navigation 3 中使用自定义请求 extra 和 Kotlinx 序列化创建自定义 DeepLinkMatcher。

运作方式

此配方包含两个 activity:

  • CustomDeepLinkMatcherActivity:接受用户输入,将 HomeKey 实例序列化为 JSON,通过 RequestExtrasKey 将其附加到 Intent extra,并启动 MainActivity。
  • MainActivity:构造 DeepLinkRequest(intent),使用 JsonDeepLinkMatcher 对其求值,解码 HomeKey,并将其设置为 NavDisplay 中的起始路线。

关键概念

  1. 自定义 RequestExtrasKey: JsonDeepLinkMatcherKey 定义了一个自定义 extra 键,该键实现了 RequestExtrasKey<String>,用于在 DeepLinkRequest.extras 中以类型安全的方式存储和读取序列化的 JSON 载荷。

  2. 自定义 DeepLinkMatcher: JsonDeepLinkMatcher<T> 扩展了 DeepLinkMatcher<T, MatchResult<T>> 并实现了 matchRequest(request),以提取 request.extras[JsonDeepLinkMatcherKey] 并使用 Kotlinx 序列化将其解码为强类型 NavKey。

package com.example.nav3recipes.deeplink.usecases.matcher

import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
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.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.dropUnlessResumed
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.deeplink.DeepLinkMatcher
import androidx.navigation3.runtime.deeplink.DeepLinkRequest
import androidx.navigation3.runtime.deeplink.RequestExtrasKey
import com.example.nav3recipes.common.deeplink.EntryScreen
import com.example.nav3recipes.ui.setEdgeToEdgeConfig
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json

class CustomDeepLinkMatcherActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        setEdgeToEdgeConfig()
        super.onCreate(savedInstanceState)

        setContent {
            EntryScreen {
                Column(
                    verticalArrangement = Arrangement.spacedBy(32.dp),
                    horizontalAlignment = Alignment.CenterHorizontally,
                ) {
                    var text by remember { mutableStateOf("") }
                    OutlinedTextField(
                        placeholder = { Text("Your name...", color = Color.Black.copy(alpha = 0.5f)) },
                        value = text,
                        singleLine = true,
                        onValueChange = { text = it },
                    )

                    ElevatedButton(
                        onClick =
                            dropUnlessResumed {
                                val intent = Intent(
                                    this@CustomDeepLinkMatcherActivity,
                                    MainActivity::class.java
                                )
                                val json = Json.encodeToString(HomeKey.serializer(), HomeKey(text))
                                intent.putExtra(JsonDeepLinkMatcherKey.toString(), json)
                                startActivity(intent)
                        }
                    ) {
                        Text("Sign up")
                    }
                }
            }
        }
    }
}

internal data object JsonDeepLinkMatcherKey: RequestExtrasKey<String>

internal class JsonDeepLinkMatcher<T: NavKey>(val serializer: KSerializer<T>): DeepLinkMatcher<T, DeepLinkMatcher.MatchResult<T>>() {
    override fun matchRequest(request: DeepLinkRequest): MatchResult<T>? {
        val json = request.extras[JsonDeepLinkMatcherKey] ?: return null
        return try {
            val result = Json.decodeFromString(serializer, json)
            MatchResult(result)
        } catch (e: SerializationException) {
            Log.v("DeepLinkMatcher", "Failed to decode json", e)
            return null
        }
    }
}
package com.example.nav3recipes.deeplink.usecases.matcher

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.deeplink.DeepLinkRequest
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.TextContent
import com.example.nav3recipes.ui.setEdgeToEdgeConfig
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer

@Serializable
internal data class HomeKey(val name: String): NavKey

@Serializable
internal object FallbackKey: NavKey

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        setEdgeToEdgeConfig()
        super.onCreate(savedInstanceState)

        val request = DeepLinkRequest(intent)
        val deepLinkMatcher = createJsonDeepLinkMatcher<HomeKey>()

        val matchResult = deepLinkMatcher.match(request)
        val key = matchResult?.key ?: FallbackKey

        setContent {
            val backStack: NavBackStack<NavKey> = rememberNavBackStack(key)
            NavDisplay(
                backStack = backStack,
                onBack = backStack::removeLastOrNull,
                entryProvider = entryProvider {
                    entry<HomeKey> { key ->
                        EntryScreen("Welcome") {
                            TextContent(key.name)
                        }
                    }
                    entry<FallbackKey> { key ->
                        EntryScreen("Fallback Key") {
                            TextContent(
                                "Failed to deep link - DeepLinkRequest " +
                                        "did not match with any DeepLinkMatcher"
                            )
                        }
                    }
                }
            )
        }
    }
}

// Optional JsonDeepLinkMatcher factory function that automatically captures KSerializer for T.
private inline fun <reified T : NavKey> createJsonDeepLinkMatcher(): JsonDeepLinkMatcher<T> {
    val serializer = serializer<T>()
    return JsonDeepLinkMatcher(serializer)
}