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测试,结果一致。
解决方案建议
修正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 }修正邮箱验证的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 }使用导航组件的Arguments获取数据
当前两个路由直接从Activity Intent中提取数据,未使用导航组件传递的arguments,这会忽略导航匹配的结果。改为从backStackEntry.arguments获取参数:// VerificationRoute示例 val id = backStackEntry.arguments?.getString("id") ?: "" val expires = backStackEntry.arguments?.getString("expires") ?: "" // ...其他参数处理后台启动的DeepLink
当App处于后台时,点击Deeplink不会触发onCreate,需要在MainActivity中重写onNewIntent方法,让导航组件处理新的Intent:override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) navController.handleDeepLink(intent) }注意需要在
MainActivity中持有navController的引用,可通过rememberSaveable或全局变量实现。测试前清除App缓存
部分情况下,系统会缓存Intent数据,测试前先清除App缓存或卸载重装,确保测试环境干净。
内容的提问来源于stack exchange,提问作者Ezzy Wachira

