iOS 14+ 上的 Flutter FCM 9+

Flutter FCM 9+ on iOS 14+

如何实施 FCM 9+ 以在 IOS 版本 14+ 上正常工作?

我之前关于 Flutter FCM 7 implementation 的回答很有帮助,所以我决定为新的 FCM 9+ 版本编写相同的说明,并在几分钟内展示如何在我们的 Flutter 应用程序中实现流畅的消息传递。

迁移到零安全和 FCM 版本 9+ (IOS 14+) 后情况看起来并没有好转。我们遇到了同样的问题,但在一个新的包装器中 :)。

下面描述的说明可以帮助 FCM 9+ 实现并提供一些代码示例。也许这些说明可以帮助某人并防止浪费时间。

XCode 设置

AppDelegate.swift

import UIKit
import Flutter
import Firebase
import FirebaseMessaging

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    FirebaseApp.configure()
    GeneratedPluginRegistrant.register(with: self)
    if #available(iOS 10.0, *) {
        UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
    }

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

Info.plist

<key>FirebaseAppDelegateProxyEnabled</key>
<false/>
<key>FirebaseScreenReportingEnabled</key>
<true/>

消息示例(可调用函数)

您的消息必须使用以下选项发送:

{
   mutableContent: true,
   contentAvailable: true,
   apnsPushType: "background"
}

只是在可调用函数中使用的示例

exports.sendNotification = functions.https.onCall(
    async (data) => {
        console.log(data, "send notification");
        var userTokens = [USERTOKEN1,USERTOKEN2,USERTOKEN3];
        var payload = {
            notification: {
                title: '',
                body: '',
                image: '',
            },
            data: {
                type:'',
            },
        };
        
        for (const [userToken,userUID] of Object.entries(userTokens)) {
            admin.messaging().sendToDevice(userToken, payload, {
                mutableContent: true,
                contentAvailable: true,
                apnsPushType: "background"
            });
        }
        
        return {code: 100, message: "notifications send successfully"};
    });

Flutter 消息服务

import 'dart:convert' as convert;
import 'dart:io' show Platform;

import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_app_badger/flutter_app_badger.dart';
import 'package:octopoos/entities/notification.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:uuid/uuid.dart';

class MessagingService {
  final Box prefs = Hive.box('preferences');
  final FirebaseMessaging fcm = FirebaseMessaging.instance;
  static final instance = MessagingService._();

  bool debug = true;

  /// Private Singleton Instance
  MessagingService._();

  /// Set FCM Presentation Options
  Future<void> setPresentationOptions() async {
    await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
      alert: true,
      badge: true,
      sound: true,
    );
  }

  /// Check PUSH permissions for IOS
  Future<bool> requestPermission({bool withDebug = true}) async {
    NotificationSettings settings = await fcm.requestPermission(
      alert: true,
      announcement: false,
      badge: true,
      carPlay: false,
      criticalAlert: false,
      provisional: false,
      sound: true,
    );

    // if (withDebug) debugPrint('[ FCM ] Push: ${settings.authorizationStatus}');
    bool authorized = settings.authorizationStatus == AuthorizationStatus.authorized;
    return (Platform.isIOS && authorized || Platform.isAndroid) ? true : false;
  }

  /// Initialize FCM stream service
  Future<void> initializeFcm() async {
    final String? currentToken = await fcm.getToken();
    final String storedToken = prefs.get('fcmToken', defaultValue: '');

    /// Refresh Device token & resubscribe topics
    if (currentToken != null && currentToken != storedToken) {
      prefs.put('fcmToken', currentToken);
      /// resubscribeTopics();
    }

    if (debug) {
      debugPrint('[ FCM ] token: $currentToken');
      debugPrint('[ FCM ] service initialized');
    }
  }

  /// Store messages to Hive Storage 
  void store(RemoteMessage message) async {
    final FirebaseAuth auth = FirebaseAuth.instance;
    final Map options = message.data['options'] != null && message.data['options'].runtimeType == String
        ? convert.json.decode(message.data['options'])
        : message.data['options'];

    final AppNotification notificationData = AppNotification(
      id: const Uuid().v4(),
      title: message.data['title'] ?? '',
      body: message.data['body'] ?? '',
      image: message.data['image'] ?? '',
      type: message.data['type'] ?? 'notification',
      options: options,
      createdAt: DateTime.now().toString(),
    );

    late Box storage;
    switch (message.data['type']) {
      default:
        storage = Hive.box('notifications');
        break;
    }

    try {
      String id = const Uuid().v4();
      storage.put(id, notificationData.toMap());
      updateAppBadge(id);

      if (debug) debugPrint('Document $id created');
    } catch (error) {
      if (debug) debugPrint('Something wrong! $error');
    }
  }

  /// Update app badge
  Future<void> updateAppBadge(String id) async {
    final bool badgeIsAvailable = await FlutterAppBadger.isAppBadgeSupported();

    if (badgeIsAvailable && id.isNotEmpty) {
      final int count = Hive.box('preferences').get('badgeCount', defaultValue: 0) + 1;
      Hive.box('preferences').put('badgeCount', count);
      FlutterAppBadger.updateBadgeCount(count);
    }
  }

  /// Subscribe topic
  Future<void> subscribeTopic({required String name}) async {
    await fcm.subscribeToTopic(name);
  }

  /// Unsubscribe topic
  Future<void> unsubscribeTopic({required String name}) async {
    await fcm.unsubscribeFromTopic(name);
  }

  /// Resubscribe to topics
  Future<int> resubscribeTopics() async {
    final List topics = prefs.get('topics', defaultValue: []);
    if (topics.isNotEmpty) {
      for (String topic in topics) {
        subscribeTopic(name: topic);
      }
    }

    return topics.length;
  }
}

AppNotification 模型

class AppNotification {
  String id;
  String title;
  String body;
  String image;
  String type;
  Map options;
  String createdAt;

  AppNotification({
    this.id = '',
    this.title = '',
    this.body = '',
    this.image = '',
    this.type = 'notification',
    this.options = const {},
    this.createdAt = '',

  });

  AppNotification.fromMap(Map snapshot, this.id)
      : title = snapshot['title'],
        body = snapshot['body'],
        image = snapshot['image'],
        type = snapshot['type'] ?? 'notification',
        options = snapshot['options'] ?? {},
        createdAt = (DateTime.parse(snapshot['createdAt'])).toString();

  Map<String, dynamic> toMap() => {
        "id": id,
        "title": title,
        "body": body,
        "image": image,       
        "type": type,
        "options": options,
        "createdAt": createdAt,
      };
}

main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:provider/provider.dart';
import 'package:octopoos/services/messaging.dart';
import 'package:timezone/data/latest.dart' as tz;

Future<void> fcm(RemoteMessage message) async {
    MessagingService.instance.store(message);

 /// Show foreground Push notification
 /// !!! Flutter Local Notification Plugin REQUIRED
 await notificationsPlugin.show(
      0,
      message.data['title'],
      message.data['body'],
      NotificationDetails(android: androidChannelSpecifics, iOS: iOSChannelSpecifics),
    );
}

Future<void> main() async {
  /// Init TimeZone
  tz.initializeTimeZones();

  /// Init Firebase Core Application
  await Firebase.initializeApp();

  /// FCM Permissions & Background Handler
  MessagingService.instance.setPresentationOptions();
  FirebaseMessaging.onBackgroundMessage(fcm);
  
  runApp(
    MultiProvider(
      providers: kAppProviders,
      child: App(),
    ),
  );
}

app.dart


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

  Future<void> initFcmListeners() async {
    MessagingService.instance.initializeFcm();
    FirebaseMessaging.instance.getInitialMessage().then((message) {
      if (message != null) _handleMessage(message);
    });

    FirebaseMessaging.onMessage.listen(_handleMessage);
    FirebaseMessaging.onMessageOpenedApp.listen(_handleMessage);
  }

  void _handleMessage(RemoteMessage message) {
   MessagingService.instance.store(message);
  }
  

就是这样。不要忘记在真实的 IOS 设备上进行测试。 FCM 无法在 IOS 模拟器上运行。

下面是如何在flutter中实现FCM的全过程

首先按照以下步骤在 firebase 控制台上设置您的应用 link Add Firebase to your Flutter app

添加依赖

firebase_core: ^1.12.0
firebase_messaging: ^11.2.6

将配置添加到应用程序端。

Android

加入你的Applicationclass

import io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingBackgroundService;

public class Application extends FlutterApplication implements PluginRegistrantCallback {
  // ...
  @Override
  public void onCreate() {
    super.onCreate();
    FlutterFirebaseMessagingBackgroundService.setPluginRegistrant(this);
  }

  @Override
  public void registerWith(PluginRegistry registry) {
    GeneratedPluginRegistrant.registerWith(registry);
  }
  // ...
}

FlutterFirebaseMessagingBackgroundService 调用应用程序 onCreate 方法的回调。

iOS 整合

iOS 遵循此文档 setup iOS or macOS with Firebase Cloud Messaging.

添加功能

这是您的 main.dart 文件并将整个代码替换为以下内容:

import 'dart:async';
import 'dart:convert';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:firebase_messaging_example/firebase_config.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

import 'message.dart';
import 'message_list.dart';
import 'permissions.dart';
import 'token_monitor.dart';

/// Define a top-level named handler which background/terminated messages will
/// call.
///
/// To verify things are working, check out the native platform logs.
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  // If you're going to use other Firebase services in the background, such as Firestore,
  // make sure you call `initializeApp` before using other Firebase services.
  await Firebase.initializeApp(options: DefaultFirebaseConfig.platformOptions);
  print('Handling a background message ${message.messageId}');
}

/// Create a [AndroidNotificationChannel] for heads up notifications
late AndroidNotificationChannel channel;

/// Initialize the [FlutterLocalNotificationsPlugin] package.
late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: const FirebaseOptions(
      apiKey: 'AIzaSyAHAsf51D0A407EklG1bs-5wA7EbyfNFg0',
      appId: '1:448618578101:ios:0b11ed8263232715ac3efc',
      messagingSenderId: '448618578101',
      projectId: 'react-native-firebase-testing',
    ),
  );

  // Set the background messaging handler early on, as a named top-level function
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  if (!kIsWeb) {
    channel = const AndroidNotificationChannel(
      'high_importance_channel', // id
      'High Importance Notifications', // title
      'This channel is used for important notifications.', // description
      importance: Importance.high,
    );

    flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();

    /// Create an Android Notification Channel.
    ///
    /// We use this channel in the `AndroidManifest.xml` file to override the
    /// default FCM channel to enable heads up notifications.
    await flutterLocalNotificationsPlugin
        .resolvePlatformSpecificImplementation<
            AndroidFlutterLocalNotificationsPlugin>()
        ?.createNotificationChannel(channel);

    /// Update the iOS foreground notification presentation options to allow
    /// heads up notifications.
    await FirebaseMessaging.instance
        .setForegroundNotificationPresentationOptions(
      alert: true,
      badge: true,
      sound: true,
    );
  }

  runApp(MessagingExampleApp());
}

/// Entry point for the example application.
class MessagingExampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Messaging Example App',
      theme: ThemeData.dark(),
      routes: {
        '/': (context) => Application(),
        '/message': (context) => MessageView(),
      },
    );
  }
}

// Crude counter to make messages unique
int _messageCount = 0;

/// The API endpoint here accepts a raw FCM payload for demonstration purposes.
String constructFCMPayload(String? token) {
  _messageCount++;
  return jsonEncode({
    'token': token,
    'data': {
      'via': 'FlutterFire Cloud Messaging!!!',
      'count': _messageCount.toString(),
    },
    'notification': {
      'title': 'Hello FlutterFire!',
      'body': 'This notification (#$_messageCount) was created via FCM!',
    },
  });
}

/// Renders the example application.
class Application extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _Application();
}

class _Application extends State<Application> {
  String? _token;

  @override
  void initState() {
    super.initState();
    FirebaseMessaging.instance
        .getInitialMessage()
        .then((RemoteMessage? message) {
      if (message != null) {
        Navigator.pushNamed(
          context,
          '/message',
          arguments: MessageArguments(message, true),
        );
      }
    });

    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      RemoteNotification? notification = message.notification;
      AndroidNotification? android = message.notification?.android;
      if (notification != null && android != null && !kIsWeb) {
        flutterLocalNotificationsPlugin.show(
          notification.hashCode,
          notification.title,
          notification.body,
          NotificationDetails(
            android: AndroidNotificationDetails(
              channel.id,
              channel.name,
              channel.description,
              // TODO add a proper drawable resource to android, for now using
              //      one that already exists in example app.
              icon: 'launch_background',
            ),
          ),
        );
      }
    });

    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
      print('A new onMessageOpenedApp event was published!');
      Navigator.pushNamed(
        context,
        '/message',
        arguments: MessageArguments(message, true),
      );
    });
  }

  Future<void> sendPushMessage() async {
    if (_token == null) {
      print('Unable to send FCM message, no token exists.');
      return;
    }

    try {
      await http.post(
        Uri.parse('https://api.rnfirebase.io/messaging/send'),
        headers: <String, String>{
          'Content-Type': 'application/json; charset=UTF-8',
        },
        body: constructFCMPayload(_token),
      );
      print('FCM request for device sent!');
    } catch (e) {
      print(e);
    }
  }

  Future<void> onActionSelected(String value) async {
    switch (value) {
      case 'subscribe':
        {
          print(
            'FlutterFire Messaging Example: Subscribing to topic "fcm_test".',
          );
          await FirebaseMessaging.instance.subscribeToTopic('fcm_test');
          print(
            'FlutterFire Messaging Example: Subscribing to topic "fcm_test" successful.',
          );
        }
        break;
      case 'unsubscribe':
        {
          print(
            'FlutterFire Messaging Example: Unsubscribing from topic "fcm_test".',
          );
          await FirebaseMessaging.instance.unsubscribeFromTopic('fcm_test');
          print(
            'FlutterFire Messaging Example: Unsubscribing from topic "fcm_test" successful.',
          );
        }
        break;
      case 'get_apns_token':
        {
          if (defaultTargetPlatform == TargetPlatform.iOS ||
              defaultTargetPlatform == TargetPlatform.macOS) {
            print('FlutterFire Messaging Example: Getting APNs token...');
            String? token = await FirebaseMessaging.instance.getAPNSToken();
            print('FlutterFire Messaging Example: Got APNs token: $token');
          } else {
            print(
              'FlutterFire Messaging Example: Getting an APNs token is only supported on iOS and macOS platforms.',
            );
          }
        }
        break;
      default:
        break;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Cloud Messaging'),
        actions: <Widget>[
          PopupMenuButton(
            onSelected: onActionSelected,
            itemBuilder: (BuildContext context) {
              return [
                const PopupMenuItem(
                  value: 'subscribe',
                  child: Text('Subscribe to topic'),
                ),
                const PopupMenuItem(
                  value: 'unsubscribe',
                  child: Text('Unsubscribe to topic'),
                ),
                const PopupMenuItem(
                  value: 'get_apns_token',
                  child: Text('Get APNs token (Apple only)'),
                ),
              ];
            },
          ),
        ],
      ),
      floatingActionButton: Builder(
        builder: (context) => FloatingActionButton(
          onPressed: sendPushMessage,
          backgroundColor: Colors.white,
          child: const Icon(Icons.send),
        ),
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            MetaCard('Permissions', Permissions()),
            MetaCard(
              'FCM Token',
              TokenMonitor((token) {
                _token = token;
                return token == null
                    ? const CircularProgressIndicator()
                    : Text(token, style: const TextStyle(fontSize: 12));
              }),
            ),
            MetaCard('Message Stream', MessageList()),
          ],
        ),
      ),
    );
  }
}

/// UI Widget for displaying metadata.
class MetaCard extends StatelessWidget {
  final String _title;
  final Widget _children;

  // ignore: public_member_api_docs
  MetaCard(this._title, this._children);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      margin: const EdgeInsets.only(left: 8, right: 8, top: 8),
      child: Card(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            children: [
              Container(
                margin: const EdgeInsets.only(bottom: 16),
                child: Text(_title, style: const TextStyle(fontSize: 18)),
              ),
              _children,
            ],
          ),
        ),
      ),
    );
  }
}

遵循此文档:Cloud Messaging