点击通知后如何导航到页面

问题描述 投票:0回答:1

在过去的几天里,我一直在尝试进行设置,以便我收到的 Firebase FCM 通知可以将用户重新路由到有效负载中设置的正确页面。我尝试了一些方法但都失败了。

  1. 尝试使用导航键。 我有一个

    rootNavigatorKey
    但是当我尝试通过
    main.rootNavigatorKey
    或通过传递它的思想方法来访问它时。
    rootNavigatorKey
    的值为空。

  2. 尝试使用上下文。 我在上下文中传递,但有时它会出现故障。这意味着它将不断地路由到第 X 页。因此,当我尝试在所述第 X 页上

    pop
    时,它会引导我到第 X 页。我可能需要弹出几次才能转到我想要的实际页面到.

     [log] Notification received: New Employee 12
     [log] Notification received: New Employee 13
     [log] Notification received: New Employee 14
     [log] Notification received: New Employee 15
     [log] Notification received: New Employee 16
    

    这里有趣的事情是,有时即使有大量的消息(

    Notification received
    ),我有时也只做 1
    pop
    ,就很完美了。 (我认为在页面之间导航时
    initNotif
    可能会出现一些问题)。 附注我尝试在主程序中初始化,但上下文显示为空。

    我尝试在

    context.push
    周围使用 MUTEX 和去抖器,但都不起作用,代码通过了检查。

  3. 尝试使用

    addPostFrameCallback
    这是有希望的,但在发现上述问题之后(#2)。这会导致弹出延迟,从而导致更多问题。

由于 Android 上的

PopScope 问题,我正在使用 
go_router: 12.1.3

这是我的通知类:

const appLinkLength = "xxxxxx".length;

Future<void> handler(RemoteMessage message) async {
  developer.log("Handling background message: ${message.messageId}");
}

class FirebaseNotificationAPI {
  final FirebaseMessaging fm = FirebaseMessaging.instance;
  final AndroidNotificationChannel androidChannel =
      const AndroidNotificationChannel(
    "high_importance_channel",
    "High Importance Notifications",
    description: "Notification that you must see",
    importance: Importance.max,
  );

  final FlutterLocalNotificationsPlugin localNotification =
      FlutterLocalNotificationsPlugin();

  Future<void> initNotif(BuildContext context) async {
    // Request permissions for iOS/Android.
    await fm.requestPermission();
    final token = await fm.getToken();
    developer.log("FCM Token: $token");

    // Initialize local and push notifications.
    await initPushNotif(context);
    await initLocalNotif(context);
  }

  Future<void> handleMessage(
      BuildContext context, RemoteMessage? message) async {
    if (message == null) return;
    developer.log("Notification received: ${message.notification?.title}");
    final deepLink =
        message.data["DeepLink"].toString().substring(appLinkLength);
    await GoRouter.of(context).push(deepLink);
  }

  Future<void> initLocalNotif(BuildContext context) async {
    const androidSettings =
        AndroidInitializationSettings('@drawable/notification_icon');
    const settings = InitializationSettings(android: androidSettings);

    await localNotification.initialize(
      settings,
      onDidReceiveNotificationResponse: (details) async {
        developer.log("Notification response details: $details");
        final deepLink = jsonDecode(details.payload!)["data"]!["DeepLink"]
            .toString()
            .substring(appLinkLength);
        ();
        await GoRouter.of(context).push(deepLink);
      },
    );

    // Create the notification channel on Android.
    final platform = localNotification.resolvePlatformSpecificImplementation<
        AndroidFlutterLocalNotificationsPlugin>();

    if (platform != null) {
      await platform.createNotificationChannel(androidChannel);
    }
  }

  Future<void> initPushNotif(BuildContext context) async {
    // Handle the initial notification if the app is opened via a notification.
    fm.getInitialMessage().then(
      (message) {
        handleMessage(context, message);
      },
    );

    // Handle messages when the app is in the foreground.
    FirebaseMessaging.onMessage.listen((message) {
      final notification = message.notification;
      if (notification == null) return;

      developer.log("Foreground notification: ${notification.title}");
      localNotification.show(
        notification.hashCode,
        notification.title,
        notification.body,
        NotificationDetails(
          android: AndroidNotificationDetails(
            androidChannel.id,
            androidChannel.name,
            channelDescription: androidChannel.description,
            icon: '@drawable/notification_icon',
          ),
        ),
        payload: jsonEncode(message.toMap()),
      );
    });

    // Handle messages when the app is reopened via a notification.
    FirebaseMessaging.onMessageOpenedApp.listen((message) {
      handleMessage(context, message);
    });

    // Handle background messages.
    FirebaseMessaging.onBackgroundMessage(handler);
  }
}

这是我的主要

final log = Logger('JumpLogger');
final rootNavigatorKey = GlobalKey<NavigatorState>();

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  //to load production env variables replace '.env.dev' with '.env.prod'
 .....  
runApp(MyApp(sellerOrConsumerActive: sellerOrConsumerActive));
}

Widget getHomePage(String sellerOrConsumerActive) {
  ....
  }
}

Future<String> getBuildCert() async {
  const platform = MethodChannel('com.mazaar.frontend/keys');
  try {
    final String buildCert = await platform.invokeMethod('getBuildCert');
    return buildCert;
  } catch (e) {
    developer.log("Failed to get certs: $e");
    return "";
  }
}

Future<void> fetchAndActivateConfig() async {
  final remoteConfig = FirebaseRemoteConfig.instance;
  await remoteConfig.setConfigSettings(RemoteConfigSettings(
    fetchTimeout: const Duration(minutes: 1),
    minimumFetchInterval: Duration(
        seconds: int.parse(dotenv.env['FIREBASE_REMOTE_CONFIG_DURATION']!)),
  ));

  try {
    // Fetch and activate
    await remoteConfig.fetchAndActivate();
  } catch (e) {
    developer.log('Failed to fetch remote config: $e');
  }
}

class MyApp extends StatelessWidget {
  const MyApp({super.key, required this.sellerOrConsumerActive});
  final String sellerOrConsumerActive;

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: getRouter(sellerOrConsumerActive),
      theme: CustomTheme.themeData,
      debugShowCheckedModeBanner: false,
    );
  }
}

GoRouter getRouter(sellerOrConsumerActive) {
  final Widget homePage = getHomePage(Environment.getSellerOrConsumerActive());
  final shellNavigatorKey = GlobalKey<NavigatorState>();

  return GoRouter(
    navigatorKey: rootNavigatorKey,
    initialLocation: RoutingConstants.root.path,
    routes: <RouteBase>[
      ShellRoute(
        navigatorKey: shellNavigatorKey,
        routes: [
          GoRoute(
            name: RoutingConstants.address.name,
            path: RoutingConstants.address.path,
            pageBuilder: (context, state) => NoTransitionPage<void>(
              key: state.pageKey,
              child: const ChangeAddressPage(),
            ),
          ),
          GoRoute(
              name: RoutingConstants.root.name,
              path: RoutingConstants.root.path,
              pageBuilder: (context, state) => NoTransitionPage<void>(
                    key: state.pageKey,
                    child: homePage,
                  ),
              routes: [
                GoRoute(
                  name: RoutingConstants.item.name,
                  path: RoutingConstants.item.subroute,
                  pageBuilder: (context, state) => NoTransitionPage<void>(
                    key: state.pageKey,
                    child: ItemPage(
                      itemId: state.pathParameters['itemId'].toString(),
                    ),
                  ),
                ),
             ...
        ],
        builder: (context, state, child) {
          return child;
        },
      )
    ],
  );
}

这就是我打电话的地方

FirebaseNotificaitionAPI.initNotif(...)

class ProfilePage extends StatelessWidget {
...
  Future<Map<String, dynamic>> getProfileData(BuildContext context) async {
    await FirebaseNotificationAPI().initNotif(context);
    final user = firebaseAuth.currentUser;
    final storeExists = await checkIfStoreExists(context);
    return {
      'user': user,
      'storeExists': storeExists,
    };
  }


  @override
  Widget build(BuildContext context) {
    return PopScope(
      canPop: false || Environment.isSellerAndConsumer(),
      child: ScaffoldWrapper(
        showBack: false || Environment.isSellerAndConsumer(),
        child: FutureBuilder<Map<String, dynamic>>(
          future: getProfileData(context),
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              return const Center(child: CircularProgressIndicator());
            } else if (snapshot.hasError) {
              return const ErrorDisplay(
                errorMessage: 'Error could not load profile data.',
              );
            } else if (!snapshot.hasData) {
              return const ErrorDisplay(
                errorMessage: 'No data available.',
              );
            } else {
              final user = snapshot.data!['user'] as User?;
              final storeExists = snapshot.data!['storeExists'] as bool;
              final profilePictureUrl = user?.photoURL ?? image;

              return Column(
                children: [
                  Environment.isSellerAndConsumer()
                      ? const HeaderSearchBar(
                          initText: '',
                        )
                      : Container(),
                  Padding(
                    padding: const EdgeInsetsDirectional.fromSTEB(10, 0, 10, 0),
                    child: ListView(
                      ...
                      children: [
                        profilePicture(context, profilePictureUrl),
                        welcomeTitle(user),
                        storeExists
                            ? seeYourStoreButton(context)
                            : businessButton(context),
                        signOutButton(context),
                        manageDataButton(context),
                      ],
                    ),
                  ),
                ],
              );
            }
          },
        ),
      ),
    );
  }

...

还有这个


  @override
  void initState() {
    super.initState();
    _firebaseInit = FirebaseNotificationAPI().initNotif(context);
  }

  @override
  Widget build(BuildContext context) {
    final Uri topBannerAdUrl = Uri.parse('https://flutter.dev');

    return ScaffoldWrapper(
      showBack: false,
      child: Column(
        children: [
          const HeaderSearchBar(
            initText: '',
          ),
          FutureBuilder<void>(
            future: _firebaseInit,
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.waiting) {
                return const Center(child: CircularProgressIndicator());
              } else {
                return Expanded(
                  child: SingleChildScrollView(
                    child: Column(
                      children: [
                        GestureDetector(
                          onTap: () => _launchUrl(topBannerAdUrl),
                          child: const ImageLoader(imageUri: imageUrl),
                        ),
                        const Padding(padding: EdgeInsets.only(top: 10)),
                        Column(
                          children: List.generate(
                            itemTitles.length,
                            (index) {
                              String title = (index < itemTitles.length)
                                  ? itemTitles[index]
                                  : 'Hot Items';
                              return ItemRow(title: title);
                            },
                          ),
                        ),
                      ],
                    ),
                  ),
                );
              }
            },
          ),
        ],
      ),
    );
  }
}
flutter push-notification firebase-cloud-messaging
1个回答
0
投票

核心问题:缺少addPostFrameCallback

点击通知时,您的应用程序会尝试导航到特定页面。但是,如果此导航发生在 Flutter widget 树完全构建之前,则导航器可能尚未准备好。这会导致以下问题:

  • 空导航键:导航键尚未初始化,导致导航尝试失败。
  • 路由中的故障:快速或多个通知可能会触发多个导航事件,从而导致不稳定的行为,例如连续路由或需要多次弹出才能到达所需页面。

解决方案:使用addPostFrameCallback

通过合并 WidgetsBinding.instance.addPostFrameCallback,您可以确保导航逻辑在当前帧渲染后执行。这种方法有几个好处:

  1. 确保导航器准备就绪:
    • 延迟导航,直到小部件树完全构建,确保导航器键正确初始化并可供使用。
  2. 防止导航故障:
    • 避免尝试同时多次导航,这可能导致应用程序进入意外状态或导航不正确。
  3. 稳定导航流程:
    • 保证导航操作在稳定的环境中发生,减少出错的可能性并改善整体用户体验。

实施步骤:

  1. 延迟导航:
    • 将导航逻辑包装在 addPostFrameCallback 中,以确保它在小部件树准备就绪后运行。
  2. 使用全局导航键:
    • 为您的 MaterialApp 或 MaterialApp.router 分配一个全局导航键,以便于从应用程序中的任何位置(包括后台处理程序)进行导航。
  3. 尽早初始化通知:
    • 在应用程序运行之前初始化通知处理程序,以确保导航键已设置并准备好立即处理导航请求。

总结:

  • 添加 addPostFrameCallback: 这可确保仅在小部件树完全构建后才进行导航,从而防止空导航器问题和路由故障。
  • 使用全局导航键: 促进从应用程序的任何部分进行可靠的导航,包括后台进程。
  • 尽早初始化通知:以正确的顺序设置必要的组件,确保点击通知时可以顺利处理导航。

通过使用 addPostFrameCallback 解决导航逻辑的时序并确保导航器正确初始化,您可以实现可靠且无缝的导航来响应通知点击。

© www.soinside.com 2019 - 2024. All rights reserved.