You need to enable JavaScript to run this 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');
  }
}
  • 确保文件放在服务器的/.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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 08:20:54