我的Android应用在被终止时会重复显示两次通知

移动开发 2026-07-11

以下是我在Flutter上开发的应用。iOS运作正常,但Android应用在处于终止状态时会显示两次通知。

我排查后发现,只有Google Play Store应用会显示两次通知,我在本地的调试或发行版构建中都无法重现这种情况。

非常感谢任何帮助或解答。

以下是我的Flutter代码

main.dart

GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Future.delayed(const Duration(milliseconds: 300));
  await Firebase.initializeApp();
  FlutterError.onError = (details) {
    FirebaseCrashlytics.instance.recordFlutterFatalError(details);
  };
  PlatformDispatcher.instance.onError = (error, stack) {
    FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
    return true;
  };
  print("notification main.dart called");
  // FirebaseMessaging.onBackgroundMessage(handleBackgroundMessage);
  await PushNotificationService().initNotifications();
  await Get.putAsync(() => StorageService().init());
  Get.put(ScrollControllerHelper());
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
    DeviceOrientation.portraitDown,
  ]).then((_) {
    runApp(MyApp());
  });
}

class MyApp extends StatelessWidget {
  final CheckAuthController userController = Get.put(CheckAuthController());

  MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    ScreenUtil.init(context);
    return GetMaterialApp(
      navigatorKey: navigatorKey,
      debugShowCheckedModeBanner: false,
      title: Strings.appTitle,
      initialRoute: userController.isOnBoardingDisplayed == true ? RouteConstants.dashboard : RouteConstants.onboarding,
      initialBinding: userController.isOnBoardingDisplayed == true ? DashboardBinding() : OnboardingBindings(),
      getPages: Pages.getPages,
      theme: ThemeData(
        scaffoldBackgroundColor: ColorConstants.appBackgroundColor,
      ),
      defaultTransition: Transition.rightToLeft,
      translations: AppTranslation(),
      locale: const Locale(Constants.languageGujarati),
      fallbackLocale: const Locale(Constants.languageGujarati),
      supportedLocales: const [
        Locale(Constants.languageEnglish),
        Locale(Constants.languageGujarati),
        Locale(Constants.languageHindi),
      ],
      localizationsDelegates: const [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
    );
  }

}

push_notification_service.dart

import 'dart:convert';
import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:prime_debut_flutter/routes/routes.dart';
import 'package:prime_debut_flutter/views/dashboard/dashboard_controller.dart';

class PushNotificationService {
  final Set<String> _handledMessageIds = {};
  PushNotificationService._internal();
  static final PushNotificationService _instance =
  PushNotificationService._internal();

  factory PushNotificationService() => _instance;

  final _firebaseMessaging = FirebaseMessaging.instance;
  bool _isInitialized = false;


  final androidChannel = const AndroidNotificationChannel(
      'android_channel', 'android_notification_channel',
      description: 'notification', importance: Importance.high);

  final _localNotifications = FlutterLocalNotificationsPlugin();

  void handleMessage(RemoteMessage? message) async {
    //when app is minimized and notification come and tap
    print("notification in push service detail = $message");
    var messageData = message?.data;
    print("notification in push service detail = $messageData");
    Map? notificationPayload = messageData;
    print("notification in push service detail = $notificationPayload");
    Map? dataPayload = notificationPayload;
    print("notification in push service detail = $dataPayload");
    String? postId = dataPayload?['id'];
    String? notificationId = dataPayload?['notification_id'];
    print("notification in push service postId = $postId");
    print("notification in push service notificationId = $notificationId");
    if (message == null) return;
    var controller = Get.find<DashboardController>();
    controller.postId = postId ?? "";
    controller.notificationId = notificationId;
    controller.getHomePost(isFromNotification: true,isFromReload: false,isForceRefresh: false);
    controller.apiReadNotification();
  }
  void handleMessageMinimize(RemoteMessage? message) async {
    //when app is minimized and notification come and tap
    print("notification in push service detail = $message");
    var messageData = message?.data;
    print("notification in push service detail = $messageData");
    Map? notificationPayload = messageData;
    print("notification in push service detail = $notificationPayload");
    Map? dataPayload = notificationPayload;
    print("notification in push service detail = $dataPayload");
    String? postId = dataPayload?['id'];
    String? notificationId = dataPayload?['notification_id'];
    print("notification in push service postId = $postId");
    if (message == null) return;
    var controller = Get.find<DashboardController>();
    controller.postId = postId ?? "";
    controller.notificationId = notificationId;
    controller.getHomePost(isFromNotification: true, isFromReload: false,isForceRefresh: false);
    controller.apiReadNotification();
    Routes.goToDashboardView(isFromNotification: true, postId: postId,notificationId: notificationId);
  }

  Future initLocalNotifications() async {
    const android = AndroidInitializationSettings('app_logo');
    const ios = DarwinInitializationSettings();
    const settings = InitializationSettings(android: android, iOS: ios);


    await _localNotifications.initialize(
      settings,
      onDidReceiveNotificationResponse: _onReceived,
      onDidReceiveBackgroundNotificationResponse: _onReceived,
    );

    final platform = _localNotifications.resolvePlatformSpecificImplementation<
        AndroidFlutterLocalNotificationsPlugin>();
    await platform?.createNotificationChannel(androidChannel);

    _localNotifications.resolvePlatformSpecificImplementation<
        AndroidFlutterLocalNotificationsPlugin>()?.requestNotificationsPermission();
  }

  Future initPushNotifications() async {
    await FirebaseMessaging.instance
        .setForegroundNotificationPresentationOptions(
        alert: true, badge: true, sound: true);
    FirebaseMessaging.instance.getInitialMessage().then((message){
      print("notification in push service getInitialMessage = $message");
      handleMessageMinimize(message);
    });
    FirebaseMessaging.onMessageOpenedApp.listen((message){
      print("notification in push service onMessageOpenedApp = $message");
      //when app is minimized and notification come and tap
      handleMessageMinimize(message);
    });
    FirebaseMessaging.onBackgroundMessage(handleBackgroundMessage);
    print("notification in FirebaseMessaging.onMessage");
    FirebaseMessaging.onMessage.listen((message) {
      final data = message.data;
      final dedupeKey = (data['notification_id'] ??
          message.messageId ??
          '${data['id']}_${message.sentTime?.millisecondsSinceEpoch ?? ''}')
          .toString();

      if (dedupeKey.isNotEmpty && _handledMessageIds.contains(dedupeKey)) return;
      if (dedupeKey.isNotEmpty) _handledMessageIds.add(dedupeKey);

      final nidStr = (data['notification_id'] ?? '').toString();
      final int notifId = int.tryParse(nidStr)
          ?? (nidStr.isNotEmpty
              ? nidStr.hashCode
              : DateTime.now().millisecondsSinceEpoch.remainder(100000));

      print("========== FCM ==========notifId = $notifId");
      print("messageId: ${message.messageId}");
      print("from: ${message.from}");
      print("sentTime: ${message.sentTime}");
      print("notification in local notification ${message.notification?.title}");
      final notification = message.notification;
      if (notification != null) {
        _localNotifications.show(
            notifId,
            notification.title,
            notification.body,
            NotificationDetails(
                android: AndroidNotificationDetails(
                  androidChannel.id, androidChannel.name,
                  channelDescription: androidChannel.description,
                  icon: 'app_logo',showWhen: false,)),
            payload: jsonEncode(message.toMap()));
      }

    });
  }

  Future<void> initNotifications() async {
    print("notification initNotifications called");
    if (_isInitialized) {
      print("PushNotificationService already initialized");
      return;
    }
    _isInitialized = true;
    await _firebaseMessaging.requestPermission();
    String? apnsToken;
    if (Platform.isMacOS || Platform.isIOS) {
      apnsToken = await _firebaseMessaging.getAPNSToken();
    }
    if ((Platform.isIOS && apnsToken != null) || Platform.isAndroid) {
      generateFcmToken();
      _firebaseMessaging.subscribeToTopic('local_news_set_up');
      print("topic subscribed");
    }
    await initLocalNotifications();
    await initPushNotifications();
  }
  generateFcmToken() async{
    final fcmToken = await _firebaseMessaging.getToken();
    print("Fcm token --- ${fcmToken}");
  }
  void unSubscribe(){
    _firebaseMessaging.unsubscribeFromTopic('local_news_set_up');
  }
}





void _onReceived(NotificationResponse details) async {
  //when app is opened at that time navigation occurred
  print("notification 1 detail = $details");
  var messageData = details.payload;
  Map notificationPayload = (jsonDecode(messageData!));
  Map dataPayload = (notificationPayload["data"]);
  var postId = dataPayload['id'];
  String? notificationId = dataPayload['notification_id'];
  print("Raw payload: $postId and notificationId = $notificationId");
  var controller = Get.find<DashboardController>();
  controller.postId = postId;
  controller.notificationId = notificationId;
  controller.homePostListPage = 1;
  controller.getHomePost(isFromNotification: true, isFromReload: false,isForceRefresh: false);
  controller.apiReadNotification();

}

Future<void> handleBackgroundMessage(RemoteMessage message) async {
  print("notification 2 detail = $message");
  print("message back-- ${message.notification}");
}

解决方案

// ✅ ROOT CAUSE
// Duplicate notifications happen when:
// 1. FCM "notification" payload is present → Android shows it automatically
// 2. You ALSO show notification manually using flutter_local_notifications
// Result = 2 notifications (especially in terminated state)


// ❌ PROBLEM (Your current approach)
FirebaseMessaging.onMessage.listen((message) {
  final notification = message.notification;

  if (notification != null) {
    // This creates duplicate notification
    _localNotifications.show(
      0,
      notification.title,
      notification.body,
      ...
    );
  }
});


// ✅ SOLUTION 1 (RECOMMENDED - Use DATA ONLY payload)

// 🔹 Send this from server (NO "notification" block)
{
  "to": "<FCM_TOKEN>",
  "data": {
    "title": "Hello",
    "body": "World",
    "id": "123"
  }
}


// 🔹 Handle in Flutter
FirebaseMessaging.onMessage.listen((message) {
  final data = message.data;

  _localNotifications.show(
    0,
    data['title'],
    data['body'],
    const NotificationDetails(
      android: AndroidNotificationDetails(
        'channel_id',
        'channel_name',
        importance: Importance.max,
        priority: Priority.high,
      ),
    ),
  );
});


// ✅ SOLUTION 2 (If you want to keep "notification" payload)

// 🔹 Then DO NOT show manually
FirebaseMessaging.onMessage.listen((message) {
  // ❌ REMOVE manual show()
  // Android will automatically display notification
});


// ⚠️ IMPORTANT NOTES

// 1. In TERMINATED state:
//    - "notification" payload → handled by Android system (auto display)
//    - Your Dart code does NOT run

// 2. In FOREGROUND:
//    - You must show notification manually (if using data-only)

// 3. Deduplication logic WILL NOT WORK for terminated state
//    because system notifications bypass Dart layer


// ✅ FINAL RECOMMENDATION (Production)
// ✔ Use ONLY "data" payload
// ✔ Handle all notifications manually in Flutter
// ✔ Remove "notification" payload completely
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章