You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose Deeplink配置不同却始终跳转至ResetPasswordRoute问题

问题概述

配置了不同uriPattern的邮箱验证和密码重置Deeplink,但所有Deeplink点击后均重定向至ResetPasswordRoute,无论链接来自https://handypro.pro还是https://staging.handypro.pro域名。

配置详情

Manifest配置

<activity
    android:name=".activity.MainActivity"
    android:exported="true"
    android:theme="@style/Theme.ServiceSpotter">

    <!-- Intent filter for email verification -->
    <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="handypro.pro" />
    </intent-filter>

    <!-- Intent filter for password reset -->
    <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="staging.handypro.pro" />
    </intent-filter>
</activity>

导航设置

@Composable
fun RootNavigation(
    isLoggedIn: Boolean,
    isWalkthroughCompleted: Boolean,
    navController: NavHostController,
    isSystemInDarkTheme: Boolean
) {
    NavHost(
        navController = navController,
        startDestination = SplashScreenDestination.route
    ) {
        splashNavGraph(
            isLoggedIn,
            isWalkthroughCompleted,
            onNavigateToHome = {
                navController.navigate(MainScreenDestination.destination) {
                    popUpTo(SplashScreenDestination.route) {
                        inclusive = true
                    }
                }
            },
            onNavigateToLogin = {
                navController.navigate(AuthDestination.route) {
                    popUpTo(SplashScreenDestination.route) {
                        inclusive = true
                    }
                }
            },
            onNavigateToWalkthrough = {
                navController.navigate(OnBoardingDestination.route) {
                    popUpTo(SplashScreenDestination.route) {
                        inclusive = true
                    }
                }
            }
        )

        walkthroughNavGraph(
            onSignInClicked = {
                navController.navigate(AuthDestination.route)
            },
            onGoogleSignInClicked = {},
            onGetStartedClicked = {
                navController.navigate(RegisterDestination.route)
            }
        )

        authNavGraph(
            onNavigateToHome = {
                navController.navigate(MainScreenDestination.destination) {
                    popUpTo(SplashScreenDestination.route) {
                        inclusive = true
                    }
                }
            },
            onNavigateToOtp = {},
            onGoogleSignInClicked = {},
            onBackClicked = {
                navController.popBackStack()
            },
            onRegisterBackClicked = {
                navController.popBackStack()
            },
            onCreateAccountClicked = {
                navController.navigate(RegisterDestination.route)
            },
            onForgotPasswordClicked = {
                navController.navigate(ForgotPasswordDestination.route)
            },
            isSystemInDarkMode = isSystemInDarkTheme,
        )

        mainScreenNavGraph(isSystemInDarkTheme)
    }
}

Auth导航图

fun NavGraphBuilder.authNavGraph(
    onNavigateToHome: () -> Unit = {},
    onForgotPasswordClicked: () -> Unit = {},
    onGoogleSignInClicked: () -> Unit = {},
    onCreateAccountClicked: () -> Unit = {},
    onBackClicked: () -> Unit = {},
    onRegisterBackClicked: () -> Unit = {},
    onNavigateToOtp: () -> Unit = {},
    isSystemInDarkMode: Boolean = false,
) {
    navigation(
        startDestination = LoginDestination.destination,
        route = AuthDestination.route
    ) {
        composable(route = LoginDestination.destination,
            enterTransition = { slideIn },
            exitTransition = { slideOut }) {
            LoginRoute(
                onNavigateHome = onNavigateToHome,
                onForgotPasswordClicked = onForgotPasswordClicked,
                onGoogleSignInClicked = onGoogleSignInClicked,
                onCreateAccountClicked = onCreateAccountClicked,
                onBackClicked = onBackClicked
            )
        }

        composable(
            route = RegisterDestination.route,
            enterTransition = { slideIn },
            exitTransition = { slideOut }
        ) {
            RegisterRoute(
                onNavigateToOtp = onNavigateToOtp,
                onBackClick = onRegisterBackClicked
            )
        }

        composable(
            route = ForgotPasswordDestination.route,
            enterTransition = { slideIn },
            exitTransition = { slideOut }
        ) {
            ForgotPasswordRoute(
                isSystemInDarkMode = isSystemInDarkMode,
                onBackClick = onBackClicked,
            )
        }

        composable(
            route = VerificationDestination.route,
            deepLinks = listOf(
                navDeepLink {
                    uriPattern = BuildConfig.BASE_URL + "email/verify/{id}?expires={expires}&hash={hash}&signature={signature}"
                    action = Intent.ACTION_VIEW
                }
            ),
            arguments = listOf(
                navArgument("id") {
                    type = StringType
                    defaultValue = ""
                },
                navArgument("expires") {
                    type = StringType
                    defaultValue = ""
                },
                navArgument("hash") {
                    type = StringType
                    defaultValue = ""
                },
                navArgument("signature") {
                    type = StringType
                    defaultValue = ""
                }
            ),
            enterTransition = { slideIn },
            exitTransition = { slideOut },
        ) { backStackEntry ->
            val context = LocalContext.current
            val intent = remember(backStackEntry) {
                context.findActivity()?.intent
            }
            val uri = intent?.data

            val id = uri?.pathSegments?.get(3) ?: ""
            val expires = uri?.getQueryParameter("expires") ?: ""
            val hash = uri?.getQueryParameter("hash") ?: ""
            val signature = uri?.getQueryParameter("signature") ?: ""
            VerificationRoute(
                isSystemInDarkMode = isSystemInDarkMode,
                id = id,
                expires = expires,
                hash = hash,
                signature = signature
            )
        }

        composable(
            route = ResetPasswordDestination.route,
            deepLinks = listOf(
                navDeepLink {
                    uriPattern = BuildConfig.BASE_URL + "password/email?token={token}&email={email}"
                    action = Intent.ACTION_DEFAULT
                }
            ),
            arguments = listOf(
                navArgument("token") {
                    type = StringType
                    defaultValue = ""
                },
                navArgument("email") {
                    type = StringType
                    defaultValue = ""
                }
            ),
            enterTransition = { slideIn },
            exitTransition = { slideOut }
        ) { backStackEntry ->
            val context = LocalContext.current
            val intent = remember(backStackEntry) {
                context.findActivity()?.intent
            }
            val uri = intent?.data

            val token = uri?.getQueryParameter("token") ?: ""
            val email = uri?.getQueryParameter("email") ?: ""
            ResetPasswordRoute(
                isSystemInDarkMode = false,
                onBackClick = onBackClicked,
                token = token,
                email = email
            )
        }
    }
}

fun Context.findActivity(): Activity? = when (this) {
    is Activity -> this
    is ContextWrapper -> baseContext.findActivity()
    else -> null
}

MainActivity配置

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    private val viewModel by viewModels<MainVewModel>()
    val notificationLauncher = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) {

    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (
            isAtLeastApi(Build.VERSION_CODES.TIRAMISU) &&
            hasPermission(Manifest.permission.POST_NOTIFICATIONS).not()
        ) {
            notificationLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
        }
        setContent {
            val isLoggedIn by viewModel.isLoggedIn.collectAsStateWithLifecycle()
            val isWalkthroughCompleted by viewModel.walkthroughCompleted.collectAsStateWithLifecycle()
            ServiceSpotterTheme {
                val navController = rememberNavController()
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    RootNavigation(
                        isLoggedIn = isLoggedIn,
                        navController = navController,
                        isWalkthroughCompleted = isWalkthroughCompleted,
                        isSystemInDarkTheme = isSystemInDarkTheme()
                    )
                }
            }
        }
    }
}

预期行为

Deeplink应根据uriPattern配置正确导航至对应路由:邮箱验证链接跳转到VerificationRoute,密码重置链接跳转到ResetPasswordRoute。

额外上下文

  • 已验证Deeplink URL与定义的uriPattern配置匹配。
  • 已通过服务器上的assetlinks.json验证handypro.pro和staging.handypro.pro域名。
  • 问题在不同设备和环境中均存在。
  • 已在模拟器、物理设备上测试,且使用Android Studio App Link Assistant测试,结果一致。

解决方案建议

  1. 修正Deeplink的Action匹配
    ResetPasswordDestination的deepLink配置使用了Intent.ACTION_DEFAULT,但Manifest中所有Intent Filter的Action都是Intent.ACTION_VIEW,这会导致匹配逻辑异常。将ResetPasswordDestination的deepLink action改为Intent.ACTION_VIEW:

    navDeepLink {
        uriPattern = BuildConfig.BASE_URL + "password/email?token={token}&email={email}"
        action = Intent.ACTION_VIEW
    }
    
  2. 修正邮箱验证的URI Pattern
    邮箱验证链接的域名是handypro.pro,但当前VerificationDestination的uriPattern使用了BuildConfig.BASE_URL(对应staging.handypro.pro),导致Pattern与实际链接不匹配。单独定义邮箱验证的uriPattern:

    navDeepLink {
        uriPattern = "https://handypro.pro/api/email/verify/{id}?expires={expires}&hash={hash}&signature={signature}"
        action = Intent.ACTION_VIEW
    }
    
  3. 使用导航组件的Arguments获取数据
    当前两个路由直接从Activity Intent中提取数据,未使用导航组件传递的arguments,这会忽略导航匹配的结果。改为从backStackEntry.arguments获取参数:

    // VerificationRoute示例
    val id = backStackEntry.arguments?.getString("id") ?: ""
    val expires = backStackEntry.arguments?.getString("expires") ?: ""
    // ...其他参数
    
  4. 处理后台启动的DeepLink
    当App处于后台时,点击Deeplink不会触发onCreate,需要在MainActivity中重写onNewIntent方法,让导航组件处理新的Intent:

    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        navController.handleDeepLink(intent)
    }
    

    注意需要在MainActivity中持有navController的引用,可通过rememberSaveable或全局变量实现。

  5. 测试前清除App缓存
    部分情况下,系统会缓存Intent数据,测试前先清除App缓存或卸载重装,确保测试环境干净。

内容的提问来源于stack exchange,提问作者Ezzy Wachira

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 22:47:32