Flutter - 使用本地通知和警报的 FCM

3

这是我第一次使用Flutter测试FCM。我查看了一些来自GitHub的文档和SO问题。

我能够发送通知,并且当应用程序未运行时它们可以被传送。

如果应用程序正在运行或在后台,则消息不可见。

我已将代码添加到main.dart文件中,但不确定这是否是正确的方法。

编辑:这段代码用于onResume:

{notification: {}, data: {badge: 1, collapse_key: com.HT, google.original_priority: high, google.sent_time: 1623238, google.delivered_priority: high, sound: default, google.ttl: 2419200, from: 71374876, body: Body, title: Title, click_action: FLUTTER_NOTIFICATION_CLICK, google.message_id: 0:50a56}}

在以下代码中,我正尝试使用FCM与本地通知。
class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
  FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
      new FlutterLocalNotificationsPlugin();

  @override
  void initState() {
    var initializationSettingsAndroid =
        new AndroidInitializationSettings('@mipmap/ic_launcher');
    var initializationSettingsIOS = new IOSInitializationSettings();
    var initializationSettings = new InitializationSettings(
        android: initializationSettingsAndroid, iOS: initializationSettingsIOS);
    flutterLocalNotificationsPlugin.initialize(initializationSettings,
        onSelectNotification: onSelectNotification);
    _firebaseMessaging.configure(
      onMessage: (Map<String, dynamic> message) async {
        showNotification(
            message['notification']['title'], message['notification']['body']);
        print("onMessage: $message");
      },
      onLaunch: (Map<String, dynamic> message) async {
        print("onLaunch: $message");
        // Navigator.pushNamed(context, '/notify');
        ExtendedNavigator.of(context).push(
          Routes.bookingQRScan,
        );
      },
      onResume: (Map<String, dynamic> message) async {
        print("onResume: $message");
      },
    );
  }

  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      home: AnimatedSplashScreen(), //SplashScreen()

      builder: ExtendedNavigator.builder<a.Router>(router: a.Router()),
    );
  }

  Future onSelectNotification(String payload) async {
    showDialog(
      context: context,
      builder: (_) {
        return new AlertDialog(
          title: Text("PayLoad"),
          content: Text("Payload : $payload"),
        );
      },
    );
  }

  void showNotification(String title, String body) async {
    await _demoNotification(title, body);
  }

  Future<void> _demoNotification(String title, String body) async {
    var androidPlatformChannelSpecifics = AndroidNotificationDetails(
        'channel_ID', 'channel name', 'channel description',
        importance: Importance.max,
        playSound: false, //true,
        //sound: 'sound',
        showProgress: true,
        priority: Priority.high,
        ticker: 'test ticker');

    //var iOSChannelSpecifics = IOSNotificationDetails();
    var platformChannelSpecifics =
        NotificationDetails(android: androidPlatformChannelSpecifics);
    await flutterLocalNotificationsPlugin
        .show(0, title, body, platformChannelSpecifics, payload: 'test');
  }
}

错误

This is when my app is running on foreground. E/FlutterFcmService(14434): Fatal: failed to find callback
W/FirebaseMessaging(14434): Missing Default Notification Channel metadata in AndroidManifest. Default value will be used.
W/ConnectionTracker(14434): Exception thrown while unbinding
W/ConnectionTracker(14434): java.lang.IllegalArgumentException: Service not registered: lu@fb04880
Notification is visible in notification center. Now i am clicking on it and app get terminated.
and new instance of app is running and below is the return code. I/flutter (14434): onResume: {notification: {}, data: {badge: 1, collapse_key: com.HT, google.original_priority: high, google.sent_time: 1607733798, google.delivered_priority: high, sound: default, google.ttl: 2419200, from: 774876, body: Body, title: Title, click_action: FLUTTER_NOTIFICATION_CLICK, google.message_id: 0:1607573733816296%850a56}}
E/FlutterFcmService(14434): Fatal: failed to find callback
W/ConnectionTracker(14434): Exception thrown while unbinding

编辑2: 我进行了更深入的挖掘,并得出了以下代码。

final FirebaseMessaging _fcm = FirebaseMessaging();

  FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
      FlutterLocalNotificationsPlugin();

  var initializationSettingsAndroid;
  var initializationSettingsIOS;
  var initializationSettings;

  void _showNotification() async {
    //await _buildNotification();
  }

  Future<dynamic> myBackgroundMessageHandler(Map<String, dynamic> message) {
    if (message.containsKey('data')) {
      // Handle data message
      final dynamic data = message['data'];
    }

    if (message.containsKey('notification')) {
      // Handle notification message

      final dynamic notification = message['notification'];
    }

    // Or do other work.
  }

  Future<void> _createNotificationChannel(
      String id, String name, String description) async {
    final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
    var androidNotificationChannel = AndroidNotificationChannel(
      id,
      name,
      description,
      importance: Importance.max,
      playSound: true,
      // sound: RawResourceAndroidNotificationSound('not_kiddin'),
      enableVibration: true,
    );
    await flutterLocalNotificationsPlugin
        .resolvePlatformSpecificImplementation<
            AndroidFlutterLocalNotificationsPlugin>()
        ?.createNotificationChannel(androidNotificationChannel);
  }

  Future<void> _buildNotification(String title, String body) async {
    var androidPlatformChannelSpecifics = AndroidNotificationDetails(
        'my_channel', 'Channel Name', 'Channel Description.',
        importance: Importance.max,
        priority: Priority.high,
        //  playSound: true,
        enableVibration: true,
        //  sound: RawResourceAndroidNotificationSound('not_kiddin'),
        ticker: 'noorderlicht');
    //var iOSChannelSpecifics = IOSNotificationDetails();
    var platformChannelSpecifics =
        NotificationDetails(android: androidPlatformChannelSpecifics);

    await flutterLocalNotificationsPlugin
        .show(0, title, body, platformChannelSpecifics, payload: 'payload');
  }

  @override
  void initState() {
    super.initState();

    initializationSettingsAndroid =
        AndroidInitializationSettings('@mipmap/ic_launcher');
    initializationSettingsIOS = IOSInitializationSettings(
        onDidReceiveLocalNotification: onDidReceiveLocalNotification);

    initializationSettings =
        InitializationSettings(android: initializationSettingsAndroid);
    // initializationSettingsAndroid, initializationSettingsIOS);

    _fcm.requestNotificationPermissions();

    _fcm.configure(
      onMessage: (Map<String, dynamic> message) async {
        print(message);
        flutterLocalNotificationsPlugin.initialize(initializationSettings,
            onSelectNotification: onSelectNotification);

        //_showNotification();
        Map.from(message).map((key, value) {
          print(key);
          print(value);
          print(value['title']);
          _buildNotification(value['title'], value['body']);
        });
      },
      onLaunch: (Map<String, dynamic> message) async {
        print("onLaunch: $message");
      },
      onResume: (Map<String, dynamic> message) async {
        print("onResume: $message");
        print(message['data']['title']);
        //AlertDialog(title: message['data']['title']);
        ExtendedNavigator.of(context).push(
          Routes.bookingQRScan,
        );
        //_showNotification();
      },
    );
  }

  Future onDidReceiveLocalNotification(
      int id, String title, String body, String payload) async {
    // display a dialog with the notification details, tap ok to go to another page
    showDialog(
      context: context,
      builder: (BuildContext context) => CupertinoAlertDialog(
        title: Text(title),
        content: Text(body),
        actions: [
          CupertinoDialogAction(
            isDefaultAction: true,
            child: Text('Ok'),
            onPressed: () {},
          )
        ],
      ),
    );
  }

  Future onSelectNotification(String payload) async {
    if (payload != null) {
      debugPrint('Notification payload: $payload');
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      home: AnimatedSplashScreen(), //SplashScreen()

      builder: ExtendedNavigator.builder<a.Router>(router: a.Router()),
    );
  }

使用以上代码,我可以在通知栏中看到通知,但是在onResume部分,我想要重定向,但这并没有起作用。不确定原因。

另外,我希望在onmessage和onresume事件中显示警报框。


请发布您的有效载荷。 - Farhana Naaz Ansari
问题中已经添加了。 - Roxx
您的有效载荷有误,我经验过通知键必须具有标题和正文参数,数据 JSON 对象也应包含标题和正文键。 - Farhana Naaz Ansari
好的,感谢您的评论。我根据您的建议进行了一些修改,并进行了测试。还有一件事,如果应用程序在后台运行,那么当我点击通知时,在应用程序中是否会收到警报,或者它只会打开应用程序? - Roxx
你可以将其重定向到任何屏幕,如果不设置任何条件,则只会打开应用程序。 - Farhana Naaz Ansari
你能给我展示一些重定向的例子吗?我需要在所有页面上定义FCMNotification还是只需要在main.dart上配置它? - Roxx
2个回答

3

您的有效载荷必须正确,有效载荷内的notificationdata对象必须包含titlebody键。当应用程序在notification键中关闭时,您将获得titlebody为空的情况,此时您应该在数据键中具有标题和正文。

{notification: {title: title, body: test}, data: {notification_type: Welcome, body: body, badge: 1, sound: , title: farhana mam, click_action: FLUTTER_NOTIFICATION_CLICK, message: H R U, category_id: 2, product_id: 1, img_url: }}

不要将标题和正文设置为空。
void showNotification(Map<String, dynamic> msg) async {
    //{notification: {title: title, body: test}, data: {notification_type: Welcome, body: body, badge: 1, sound: , title: farhana mam, click_action: FLUTTER_NOTIFICATION_CLICK, message: H R U, category_id: 2, product_id: 1, img_url: }}
    print(msg);
    print(msg['data']['title']);
    var title = msg['data']['title'];
    var msge = msg['data']['body'];

    var android = new AndroidNotificationDetails(
        'channel id', 'channel NAME', 'CHANNEL DESCRIPTION',
        priority: Priority.High, importance: Importance.Max);
    var iOS = new IOSNotificationDetails();
    var platform = new NotificationDetails(android, iOS);
    await flutterLocalNotificationsPlugin.show(0, title, msge, platform,
        payload: msge);
  }

用于重定向

FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
    var android = new AndroidInitializationSettings('@mipmap/ic_launcher');
    var iOS = new IOSInitializationSettings();
    var initSetttings = new InitializationSettings(android, iOS);
    flutterLocalNotificationsPlugin.initialize(initSetttings, onSelectNotification: onSelectNotification);
    firebaseCloudMessaging_Listeners();

  Future onSelectNotification(String payload) async {
    if (payload != null) {
      debugPrint('notification payload:------ ${payload}');
      await Navigator.push(
        context,
        new MaterialPageRoute(builder: (context) => NotificationListing()),
      ).then((value) {});
    }

  }

在'onSelectNotification'函数中,可以将条件作为字符串参数传递,并且可以进行重定向。
(可选,但推荐)如果希望在您的应用程序中(通过onResume和onLaunch,请参见下文)收到系统托盘中的通知点击事件,请在android/app/src/main/AndroidManifest.xml文件的标签中包含以下intent-filter:

我需要在哪里定义_fcm.configure,只需要在onmessage期间显示通知吗?此外,在onResume期间重定向的一些建议。 - Roxx
我更新了代码,你可以使用'onSelectNotification'方法进行重定向,并且你可以在主屏幕上配置fcm。 - Farhana Naaz Ansari
让我看一下你的代码。如果我有困难,我会告诉你的。谢谢你的帮助,Farhana。 - Roxx
你遇到了什么问题?你是否已经在Firebase控制台上测试过发送虚拟通知? - Farhana Naaz Ansari
我已经更新了问题中的新代码。数据来自Firebase。我可以看到通知,但是当我点击它时,我的应用程序会被终止。 - Roxx
显示剩余4条评论

1

看一下你上面(最后编辑的)代码,我认为首先你要确定是使用了本地通知还是默认的fcm通知。由于你的myBackgroundMessageHandler什么都没做,我假设是后者。尝试暂时用一个固定的字符串(例如“这是一个本地通知”)替换标题以确保。

其次,myBackgroundMessageHandler只会对数据消息调用。如果你使用了开始写的有效载荷,那么应该没问题。无论如何,请确保不直接将标题、正文、样式信息等放在有效载荷中。如果需要,请将它们放在数据节点中。

这是我正在使用的代码:

calling the notificationService init() method in main.dart

notification-service.dart

import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:app/models/data-notification.dart';
import 'dart:async';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
import 'dart:io';

FlutterLocalNotificationsPlugin notificationsPlugin =
    FlutterLocalNotificationsPlugin();

//Function to handle Notification data in background. 
Future<dynamic> backgroundMessageHandler(Map<String, dynamic> message) {
  print("FCM backgroundMessageHandler $message");
  showNotification(DataNotification.fromPushMessage(message['data']));
  return Future<void>.value();
}

//Function to handle Notification Click.
Future<void> onSelectNotification(String payload) {
  print("FCM onSelectNotification");
  return Future<void>.value();
}

//Function to Parse and Show Notification when app is in foreground
Future<dynamic> onMessage(Map<String, dynamic> message) {
  print("FCM onMessage $message");
  showNotification(DataNotification.fromPushMessage(message['data']));
  return Future<void>.value();
}

//Function to Handle notification click if app is in background
Future<dynamic> onResume(Map<String, dynamic> message) {
  print("FCM onResume $message");
  return Future<void>.value();
}

//Function to Handle notification click if app is not in foreground neither in background
Future<dynamic> onLaunch(Map<String, dynamic> message) {
  print("FCM onLaunch $message");
  return Future<void>.value();
}

void showNotification(DataNotification notification) async {
  final AndroidNotificationDetails androidPlatformChannelSpecifics =
      await getAndroidNotificationDetails(notification);

  final NotificationDetails platformChannelSpecifics =
      NotificationDetails(android: androidPlatformChannelSpecifics);

  await notificationsPlugin.show(
    0,
    notification.title,
    notification.body,
    platformChannelSpecifics,
  );
}

Future<AndroidNotificationDetails> getAndroidNotificationDetails(
    DataNotification notification) async {
  switch (notification.notificationType) {
    case NotificationType.NEW_INVITATION:
    case NotificationType.NEW_MEMBERSHIP:
    case NotificationType.NEW_ADMIN_ROLE:
    case NotificationType.MEMBERSHIP_BLOCKED:
    case NotificationType.MEMBERSHIP_REMOVED:
    case NotificationType.NEW_MEMBERSHIP_REQUEST:
      return AndroidNotificationDetails(
          'organization',
          'Organization management',
          'Notifications regarding your organizations and memberships.',
          importance: Importance.max,
          priority: Priority.high,
          showWhen: false,
          category: "Organization",
          icon: 'my_app_icon_simple',
          largeIcon: DrawableResourceAndroidBitmap('my_app_icon'),
          styleInformation: await getBigPictureStyle(notification),
          sound: RawResourceAndroidNotificationSound('slow_spring_board'));
    case NotificationType.NONE:
    default:
      return AndroidNotificationDetails('general', 'General notifications',
          'General notifications that are not sorted to any specific topics.',
          importance: Importance.max,
          priority: Priority.high,
          showWhen: false,
          category: "General",
          icon: 'my_app_icon_simple',
          largeIcon: DrawableResourceAndroidBitmap('my_app_icon'),
          styleInformation: await getBigPictureStyle(notification),
          sound: RawResourceAndroidNotificationSound('slow_spring_board'));
  }
}

Future<BigPictureStyleInformation> getBigPictureStyle(
    DataNotification notification) async {
  if (notification.imageUrl != null) {
    print("downloading");
    final String bigPicturePath =
        await _downloadAndSaveFile(notification.imageUrl, 'bigPicture');

    return BigPictureStyleInformation(FilePathAndroidBitmap(bigPicturePath),
        hideExpandedLargeIcon: true,
        contentTitle: notification.title,
        htmlFormatContentTitle: false,
        summaryText: notification.body,
        htmlFormatSummaryText: false);
  } else {
    print("NOT downloading");
    return null;
  }
}

Future<String> _downloadAndSaveFile(String url, String fileName) async {
  final Directory directory = await getApplicationDocumentsDirectory();
  final String filePath = '${directory.path}/$fileName';
  final http.Response response = await http.get(url);
  final File file = File(filePath);
  await file.writeAsBytes(response.bodyBytes);
  return filePath;
}

class NotificationService {

  FirebaseMessaging _fcm = FirebaseMessaging();

  void init() async {
    final AndroidInitializationSettings initializationSettingsAndroid =
        AndroidInitializationSettings('app_icon');

    final IOSInitializationSettings initializationSettingsIOS =
        IOSInitializationSettings();

    final InitializationSettings initializationSettings =
        InitializationSettings(
      android: initializationSettingsAndroid,
      iOS: initializationSettingsIOS,
    );

    await notificationsPlugin.initialize(initializationSettings,
        onSelectNotification: (value) => onSelectNotification(value));

    _fcm.configure(
      onMessage: onMessage,
      onBackgroundMessage: backgroundMessageHandler,
      onLaunch: onLaunch,
      onResume: onResume,
    );
  }
}

data-notification.dart

import 'package:enum_to_string/enum_to_string.dart';

class DataNotification {
  final String id;
  final String title;
  final String body;
  final NotificationType notificationType;
  final String imageUrl;
  final dynamic data;
  final DateTime readAt;
  final DateTime createdAt;
  final DateTime updatedAt;

  DataNotification({
    this.id,
    this.title,
    this.body,
    this.notificationType,
    this.imageUrl,
    this.data,
    this.readAt,
    this.createdAt,
    this.updatedAt,
  });

  factory DataNotification.fromPushMessage(dynamic data) {
    return DataNotification(
      id: data['id'],
      title: data['title'],
      body: data['body'],
      notificationType: EnumToString.fromString(
          NotificationType.values, data['notification_type']),
      imageUrl: data['image_url'] ?? null,
      data: data,
      readAt: null,
      createdAt: null,
      updatedAt: null,
    );
  }
}

enum NotificationType {
  NONE,
  NEW_INVITATION,
  NEW_MEMBERSHIP,
  NEW_ADMIN_ROLE,
  MEMBERSHIP_BLOCKED,
  MEMBERSHIP_REMOVED,
  NEW_MEMBERSHIP_REQUEST
}

你可以忽略DataNotification模型部分,自己解析通知,我只是在后端使用它进行了一些额外的交互。对我来说这很有效,但是如果你想为“onSelectNotification”或类似事件显示警报,则需要找到一种获取上下文的方法。还不确定如何做到这一点。
编辑: 你可以在main.dart中这样调用它。
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  NotificationService().init();

  runApp(
    MyApp()
  );
}

请注意,当前存在与background messaging和热重载相关的问题: https://github.com/FirebaseExtended/flutterfire/issues/4316


谢谢Chris。我会阅读你的代码并告诉你我的想法。还有一件事我想知道。如果我在不同的页面上,我能收到通知吗?还是需要在该页面上定义或创建某种服务来实现通知功能? - Roxx
1
不确定我是否正确理解了您的意思。在特定页面上初始化fcm并不是必要的。您可以在main.dart中或通过上述服务进行操作。 在页面上这样做可能会导致状态等方面出现不必要的问题,但我不确定。 至少,您在backgroundMessageHandler中使用的所有函数显然都必须是全局或静态的 - 尽管我目前无法找到我读到这一点的地方。 - Chris
嗨,Chris,抱歉我离开了几天。现在我回来了,只是为了测试目的使用你的代码,稍后会根据需要进行更改。只有一件事,我想知道如何在 main.dart 中调用 notificationService 的 init() 方法。你是在谈论 NotificationService() 吗,还是其他什么东西。 - Roxx
谢谢更新,Chris。我已经在应用程序的Kotlin文件中使用了io.flutter.plugins.firebasemessaging.FirebaseMessagingPlugin.registerWith(registry?.registrarFor("io.flutter.plugins.firebasemessaging.FirebaseMessagingPlugin"))。 - Roxx
嗯,你按照文档建议做了吗?(比如意图过滤器,在清单文件中引用application.kt等)也许你可能漏掉了某个步骤? - Chris
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接