在过去的几天里,我一直在尝试进行设置,以便我收到的 Firebase FCM 通知可以将用户重新路由到有效负载中设置的正确页面。我尝试了一些方法但都失败了。
尝试使用导航键。 我有一个
rootNavigatorKey
但是当我尝试通过 main.rootNavigatorKey
或通过传递它的思想方法来访问它时。 rootNavigatorKey
的值为空。
尝试使用上下文。 我在上下文中传递,但有时它会出现故障。这意味着它将不断地路由到第 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 和去抖器,但都不起作用,代码通过了检查。
尝试使用
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);
},
),
),
],
),
),
);
}
},
),
],
),
);
}
}
核心问题:缺少addPostFrameCallback
点击通知时,您的应用程序会尝试导航到特定页面。但是,如果此导航发生在 Flutter widget 树完全构建之前,则导航器可能尚未准备好。这会导致以下问题:
解决方案:使用addPostFrameCallback
通过合并 WidgetsBinding.instance.addPostFrameCallback,您可以确保导航逻辑在当前帧渲染后执行。这种方法有几个好处:
实施步骤:
总结:
通过使用 addPostFrameCallback 解决导航逻辑的时序并确保导航器正确初始化,您可以实现可靠且无缝的导航来响应通知点击。