Flutter DeepLink无法跳转APP,点击重置链接打开浏览器求助
Flutter DeepLink 唤起APP失败(跳转浏览器)排查与修复
问题背景
用app_links: ^6.3.3实现重置密码DeepLink功能,点击邮件里的重置链接时,没唤起APP反而直接打开了浏览器。已经在服务器上传assetlinks.json,也配置了Android Manifest,用的是自定义路由而非GoRouter。
排查与修复方案
1. Android Manifest 配置错误
你提供的Manifest代码里有两处明显问题:
- 重复了
<data android:host=hostName />节点 - 第二个host属性多了引号,语法错误
正确配置写法:
<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" /> <data android:host="你的实际域名" /> <!-- 可选:添加路径前缀,仅匹配重置密码相关链接,避免其他路径触发APP --> <data android:pathPrefix="/reset-password" /> </intent-filter>
- 验证
autoVerify是否生效:执行adb shell dumpsys package domain-preferred-apps命令,查看你的域名是否已和APP关联。
2. DeepLinkService 初始化时机错误
现在你在StartupView的build方法里调用DeepLink初始化,但build会多次执行,导致重复监听链接流,可能引发监听失效。
直接把初始化移到initState里,只执行一次:
class _StartupViewState extends ConsumerState<StartupView> { @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) async { await ref.read(startupVMProvider).initialize(); // 只初始化一次DeepLink服务 DeepLinkService.handleIncomingLinks(); await DeepLinkService.handleInitialUri(); }); } @override Widget build(BuildContext context) { // 移除build里的DeepLink初始化代码 final startupVM = ref.watch(startupVMProvider); // ... 原有build逻辑不变 } }
3. 路由导航逻辑问题
- 先确认全局
navigatorKey配置正确:要在全局定义final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();,并在MaterialApp里设置navigatorKey: navigatorKey,确保currentState不为空。 _handleIncomingLink里的canPop()判断可能限制跳转,比如APP启动时栈里有其他页面就无法跳转。可以改成强制跳转:
static void _handleIncomingLink(Uri uri) { try { final token = uri.queryParameters['token']; if (token != null && token.isNotEmpty) { // 清空路由栈后跳转,确保用户直接进入重置密码页 navigatorKey.currentState?.pushNamedAndRemoveUntil( AppRoutesNames.resetPasswordView, (route) => false, arguments: {'token': token}, ); } else { CustomLogger.error('URI中缺少或为空的token'); } } catch (e, st) { FirebaseCrashlytics.instance.recordError(e, st); CustomLogger.error('处理DeepLink出错: $e'); } }
4. assetlinks.json 验证
- 确保文件放在服务器的
/.well-known/目录下,浏览器访问https://你的域名/.well-known/assetlinks.json能直接打开,内容格式如下:
[ { "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "你的APP包名", "sha256_cert_fingerprints": ["你的APP签名SHA256指纹"] } } ]
- 注意:debug版和release版的签名指纹不同,测试时要对应使用。
5. 测试方式问题
- 别直接在浏览器里点链接测试,用邮件、短信发送链接,或者用adb命令测试:
adb shell am start -W -a android.intent.action.VIEW -d "https://你的域名/reset-password?token=xxx" - Android 12+ 必须确保
autoVerify验证通过,否则系统不会直接唤起APP。
用户提供的相关代码
DeepLinkService 代码
class DeepLinkService { // 处理APP已启动时(前台/后台)的链接 static void handleIncomingLinks() { log('called'); AppLinks().uriLinkStream.listen((Uri? uri) async { WidgetsBinding.instance.addPostFrameCallback((_) async { if (uri != null) { debugPrint('Incoming link: $uri'); _handleIncomingLink(uri); } }); }, onError: (Object err) { FirebaseCrashlytics.instance.recordError(err, null); CustomLogger.error('Error handling incoming link: $err'); }); } // 处理APP首次启动时的初始URI static Future<void> handleInitialUri() async { try { final uri = await AppLinks().getInitialLink(); log('$uri'); if (uri != null) { WidgetsBinding.instance.addPostFrameCallback((_) async { debugPrint('Initial URI: $uri'); _handleIncomingLink(uri); }); } } on PlatformException catch (e, st) { FirebaseCrashlytics.instance.recordError(e, st); CustomLogger.error('Failed to get initial URI: $e'); } on FormatException catch (e, st) { FirebaseCrashlytics.instance.recordError(e, st); CustomLogger.error('Malformed initial URI: $e'); } catch (e, st) { FirebaseCrashlytics.instance.recordError(e, st); CustomLogger.error('Unknown error: $e'); } } // 解析URI并导航 static void _handleIncomingLink(Uri uri) { try { final token = uri.queryParameters['token']; if (token != null && token.isNotEmpty) { if (!navigatorKey.currentState!.canPop()) { navigatorKey.currentState!.pushNamed( AppRoutesNames.resetPasswordView, arguments: {'token': token}, ); } } else { CustomLogger.error('Token is missing or empty in the URI.'); } } catch (e, st) { FirebaseCrashlytics.instance.recordError(e, st); CustomLogger.error('Error handling incoming link: $e'); } } }
StartupView 代码
// StartupView 根据登录状态跳转对应页面 class StartupView extends ConsumerStatefulWidget { const StartupView({super.key}); @override ConsumerState<ConsumerStatefulWidget> createState() => _StartupViewState(); } class _StartupViewState extends ConsumerState<StartupView> { @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) async { await ref.read(startupVMProvider).initialize(); }); } @override Widget build(BuildContext context) { DeepLinkService.handleIncomingLinks(); DeepLinkService.handleInitialUri(); final startupVM = ref.watch(startupVMProvider); if (!startupVM.isAuthStateLoaded) { return const Scaffold( body: Center( child: CircularProgressIndicator(), ), ); } return StreamBuilder( stream: ref.watch(authServiceProvider).userState, builder: (context, snapshot) { if (snapshot.hasError) { return Center( child: ErrorView(), ); } else if (snapshot.hasData) { if (ref.watch(authServiceProvider).isLogged) { return const LocalAuthView(); } else if (ref.watch(authServiceProvider).isLogged && !ref.watch(authServiceProvider).hasAcceptedTermsAndConditions) { return PrivacyPolicyView(); } else { return const WelcomeView(); } } else { return const SizedBox(); } }, ); } }
路由相关代码
class AppRoutesNames { static const String localAuthView = '/localAuthView'; static const String deactivePage = '/deactivePage'; static const String invitationView = '/invitationView'; static const String homeView = '/homeView'; static const String detailsView = '/detailView'; static const String errorView = '/errorView'; static const String registrationView = '/registrationView'; static const String loginView = '/loginView'; static const String emailOtpView = '/emailOtpView'; static const String resetPasswordView = '/resetPasswordView'; static const String recoverPasswordView = '/recoverPasswordView'; static const String welcomeView = '/welcomeView'; static const String allDoneSuccessView = "/allDoneSuccessView"; static const String resetPasscodeView = '/resetPasscodeView'; static const String noInternetView = '/noInternetView'; } class AppRouter { Route? onGenerateRoute(RouteSettings settings) { switch (settings.name) { case AppRoutesNames.homeView: return MaterialPageRoute( builder: (context) => const HomeView(), ); case AppRoutesNames.resetPasswordView: final args = settings.arguments as Map<String, dynamic>; return MaterialPageRoute( builder: (context) => ResetPasswordView(token: args['token'])); default: return null; } } }
Android Manifest 配置(原代码)
<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:host=hostName /> <data android:host=hostName" /> <data android:scheme="https" /> </intent-filter>
内容的提问来源于stack exchange,提问作者Vinaya Augusthy
相关产品推荐
相关产品推荐

