当 Hive 数据在 FireBaseMessage 的 onBackgroundMessage 处理程序中更新时,如何更新 UI 小部件?

How can I update the UI widget when Hive data get update inside onBackgroundMessage handler of FireBaseMessage in flutter?

我正在开发一个应用程序,它可以接收来自 firebase 云消息传递的通知。我在收到消息后将其保存在 Hive 中。我有一个通知屏幕,显示从配置单元读取的通知,收到通知后会立即更新。这一直运作良好。

现在的问题是,当应用程序在后台 运行(而不是 kill/terminate)时收到的通知被保存在配置单元中,但是当导航到通知屏幕时屏幕没有更新(更新在在应用程序终止并重新运行之前,看不到配置单元。

我读到这是因为 onBackgroundMessage 处理程序运行在不同的 isolate 上,而 isolate 不能共享存储。看来我需要一种方法将 Hive 通知更新从 onBackgroundMessage 处理程序传递到主隔离区。

这是我目前的实现

push_message.dart通知class实例保存在hive

import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:app_name/helpers/helpers.dart';
part 'push_message.g.dart';

@HiveType(typeId: 1)
class PushMessage extends HiveObject {
  @HiveField(0)
  int id = int.parse(generateRandomNumber(7));
  @HiveField(1)
  String? messageId;
  @HiveField(2)
  String title;
  @HiveField(3)
  String body;
  @HiveField(4)
  String? bigPicture;
  @HiveField(5)
  DateTime? sentAt;
  @HiveField(6)
  DateTime? receivedAt;
  @HiveField(7)
  String? payload;
  @HiveField(8, defaultValue: '')
  String channelId = 'channel_id';
  @HiveField(9, defaultValue: 'channel name')
  String channelName = 'channel name';

  @HiveField(10, defaultValue: 'App notification')
  String channelDescription = 'App notification';

  PushMessage({
    this.id = 0,
    this.messageId,
    required this.title,
    required this.body,
    this.payload,
    this.channelDescription = 'App notification',
    this.channelName = 'channel name',
    this.channelId = 'channel_id',
    this.bigPicture,
    this.sentAt,
    this.receivedAt,
  });

  Future<void> display() async {
    AndroidNotificationDetails androidPlatformChannelSpecifics =
        AndroidNotificationDetails(
      channelId,
      channelName,
      channelDescription: channelDescription,
      importance: Importance.max,
      priority: Priority.high,
      ticker: 'ticker',
      icon: "app_logo",
      largeIcon: DrawableResourceAndroidBitmap('app_logo'),
    );
    IOSNotificationDetails iOS = IOSNotificationDetails(
      presentAlert: true,
    );
    NotificationDetails platformChannelSpecifics =
        NotificationDetails(android: androidPlatformChannelSpecifics, iOS: iOS);
    final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
        FlutterLocalNotificationsPlugin();
    await flutterLocalNotificationsPlugin
        .show(id, title, body, platformChannelSpecifics, payload: payload);
  }

  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'messageId': messageId,
      'title': title,
      'body': body,
      'bigPicture': bigPicture,
      'sentAt': sentAt?.millisecondsSinceEpoch,
      'receivedAt': receivedAt?.millisecondsSinceEpoch,
      'payload': payload,
    };
  }

  factory PushMessage.fromMap(map) {
    return PushMessage(
      id: map.hashCode,
      messageId: map['messageId'],
      title: map['title'],
      body: map['body'],
      payload: map['payload'],
      bigPicture: map['bigPicture'],
      sentAt: map['sentAt'] is DateTime
          ? map['sentAt']
          : (map['sentAt'] is int
              ? DateTime.fromMillisecondsSinceEpoch(map['sentAt'])
              : null),
      receivedAt: map['receivedAt'] is DateTime
          ? map['receivedAt']
          : (map['receivedAt'] is int
              ? DateTime.fromMillisecondsSinceEpoch(map['receivedAt'])
              : null),
    );
  }

  factory PushMessage.fromFCM(RemoteMessage event) {
    RemoteNotification? notification = event.notification;
    Map<String, dynamic> data = event.data;
    var noti = PushMessage(
      id: event.hashCode,
      messageId: event.messageId!,
      title: notification?.title ?? (data['title'] ?? 'No title found'),
      body: notification?.body! ?? (data['body'] ?? 'Can not find content'),
      sentAt: event.sentTime,
      receivedAt: DateTime.now(),
      bigPicture: event.notification?.android?.imageUrl,
    );
    return noti;
  }

  Future<void> saveToHive() async {
    if (!Hive.isBoxOpen('notifications')) {
      await Hive.openBox<PushMessage>('notifications');
    }
    await Hive.box<PushMessage>('notifications').add(this);
  }

  String toJson() => json.encode(toMap());

  factory PushMessage.fromJson(String source) =>
      PushMessage.fromMap(json.decode(source));

  Future<void> sendToOne(String receiverToken) async {
    try {
      await Dio().post(
        "https://fcm.googleapis.com/fcm/send",
        data: {
          "to": receiverToken,         
          "data": {
            "url": bigPicture,
            "title": title,
            "body": body,
            "mutable_content": true,
            "sound": "Tri-tone"            
          }
        },
        options: Options(
          contentType: 'application/json; charset=UTF-8',
          headers: {
            "Authorization":
                "Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
          },
        ),
      );
    } catch (e) {
      debugPrint("Error sending notification");
      debugPrint(e.toString());
    }
  }
}

notifications.dart 通知屏幕

import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:app_name/custom_widgets/drawer_sheet.dart';
import 'package:app_name/custom_widgets/notification_expandable.dart';
import 'package:app_name/models/config.dart';
import 'package:app_name/models/push_message.dart';

class Notifications extends StatelessWidget {
  const Notifications({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    if (!Hive.isBoxOpen('notifications')) {
      Hive.openBox<PushMessage>('notifications');
    }
    return Scaffold(
      appBar: AppBar(
        title: const Text("Notifications"),
        centerTitle: true,
      ),
      body: ValueListenableBuilder<Box<PushMessage>>(
        valueListenable: Hive.box<PushMessage>('notifications').listenable(),
        builder: (context, Box<PushMessage> box, widget) {
          return box.isEmpty
              ? const Center(child: Text('Empty'))
              : ListView.builder(
                  itemCount: box.length,
                  itemBuilder: (BuildContext context, int i) {
                    int reversedIndex = box.length - i - 1;
                    return NotificationExpandable((box.getAt(reversedIndex))!);
                  },
                );
        },
      ),
      drawer: !useDrawer
          ? null
          : const DrawerSheet(
              currentPage: "notifications",
            ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Hive.box<PushMessage>('notifications').clear();
        },
        child: const Text('Clear'),
      ),
    );
  }
}

backgroundMessageHandler

Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  if (!Hive.isAdapterRegistered(1)) {
    Hive.registerAdapter(PushMessageAdapter());
  }
  await Hive.initFlutter();
  if (!Hive.isBoxOpen('notifications')) {
    await Hive.openBox<PushMessage>('notifications');
  }
  await Firebase.initializeApp();
  print('Handling a background message ${message.messageId}');
  PushMessage msg = PushMessage.fromFCM(message);
  await msg.saveToHive();
  msg.display();
  Hive.close();
}

我找到了解决方法。

无法从 firebase 云消息传递的 onBackgroundMessage 处理程序与主隔离区进行通信,因为该函数在不同的隔离区上运行,我不知道它是在哪里触发的。 FCM 文档还说,任何 UI 影响逻辑都不能在此处理程序中完成,但 io 过程(如将消息存储在设备存储中)是可能的。

我将背景消息保存在与我保存前景消息的位置不同的配置单元框中。因此,在通知屏幕的初始状态,我首先从背景框获取消息,将其附加到前景框,然后启动一个在隔离中运行的函数,每 1 秒检查一次表单背景消息。当该函数获得后台消息时,它会将主隔离区作为映射发送给它,在那里它被转换为前景消息的实例并附加到前景消息框。每当前景通知框的值发生变化时,小部件就会更新,因为我使用了一个 ValueListenableBuilder 来侦听前景通知的配置单元框。然后隔离在屏幕的处置方法中终止。 这是代码。

import 'dart:async';
import 'dart:isolate';

import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:app_name/custom_widgets/drawer_sheet.dart';
import 'package:app_name/custom_widgets/notification_expandable.dart';
import 'package:app_name/models/background_push_message.dart';
import 'package:app_name/models/config.dart';
import 'package:app_name/models/push_message.dart';

class Notifications extends StatefulWidget {
  const Notifications({Key? key}) : super(key: key);

  @override
  State<Notifications> createState() => _NotificationsState();
}

class _NotificationsState extends State<Notifications> {
  bool loading = true;
  late Box<PushMessage> notifications;

  @override
  void dispose() {
    stopRunningIsolate();
    super.dispose();
  }

  @override
  void initState() {
    super.initState();
    startIsolate().then((value) {
      setState(() {
        loading = false;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Notifications"),
        centerTitle: true,
      ),
      body: ValueListenableBuilder<Box<PushMessage>>(
        valueListenable: Hive.box<PushMessage>('notifications').listenable(),
        builder: (context, Box<PushMessage> box, widget) {
          return box.isEmpty
              ? const Center(child: Text('Empty'))
              : ListView.builder(
                  itemCount: box.length,
                  itemBuilder: (BuildContext context, int i) {
                    int reversedIndex = box.length - i - 1;
                    return NotificationExpandable((box.getAt(reversedIndex))!);
                  },
                );
        },
      ),
      drawer: fromSchool
          ? null
          : const DrawerSheet(
              currentPage: "notifications",
            ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Hive.box<PushMessage>('notifications').clear();
        },
        child: const Text('Clear'),
      ),
    );
  }
}

Isolate? isolate;

Future<bool> startIsolate() async {
  if (!Hive.isAdapterRegistered(2)) {
    Hive.registerAdapter(BackgroundPushMessageAdapter());
  }
  Hive.initFlutter();
  if (!Hive.isBoxOpen('notifications')) {
    await Hive.openBox<PushMessage>('notifications');
  }
  if (Hive.isBoxOpen('background_notifications')) {
    await Hive.box<BackgroundPushMessage>('background_notifications').close();
  }
  await Hive.openBox<BackgroundPushMessage>('background_notifications');
  Iterable<BackgroundPushMessage> bgNotifications =
      Hive.box<BackgroundPushMessage>('background_notifications').values;
  List<PushMessage> bgMsgs =
      bgNotifications.map((e) => PushMessage.fromMap(e.toMap())).toList();

  Hive.box<PushMessage>('notifications').addAll(bgMsgs);
  await Hive.box<BackgroundPushMessage>('background_notifications').clear();
  await Hive.box<BackgroundPushMessage>('background_notifications').close();
  ReceivePort receivePort = ReceivePort();
  String path = (await getApplicationDocumentsDirectory()).path;
  isolate = await Isolate.spawn(
      backgroundNotificationCheck, [receivePort.sendPort, path]);
  receivePort.listen((msg) {
    List<Map> data = msg as List<Map>;
    List<PushMessage> notis =
        data.map((noti) => PushMessage.fromMap(noti)).toList();

    Hive.box<PushMessage>('notifications').addAll(notis);
  });
  return true;
}

void stopRunningIsolate() {
  isolate?.kill(priority: Isolate.immediate);
  isolate = null;
}

Future<void> backgroundNotificationCheck(List args) async {
  SendPort sendPort = args[0];
  String path = args[1];
  if (!Hive.isAdapterRegistered(1)) {
    Hive.registerAdapter(PushMessageAdapter());
  }
  Hive.init(path);
  Timer.periodic(const Duration(seconds: 1), (Timer t) async {
    if (!Hive.isAdapterRegistered(2)) {
      Hive.registerAdapter(BackgroundPushMessageAdapter());
    }
    if (Hive.isBoxOpen('background_notifications')) {
      await Hive.box<BackgroundPushMessage>('background_notifications').close();
    }
    await Hive.openBox<BackgroundPushMessage>('background_notifications');
    if (Hive.box<BackgroundPushMessage>('background_notifications')
        .isNotEmpty) {
      List<Map> notifications =
          Hive.box<BackgroundPushMessage>('background_notifications')
              .values
              .map((noti) => noti.toMap())
              .toList();
      sendPort.send(notifications);
      await Hive.box<BackgroundPushMessage>('background_notifications').clear();
    }
    await Hive.box<BackgroundPushMessage>('background_notifications').close();
  });
}

onBackgroundNotification 处理程序

if (!Hive.isAdapterRegistered(2)) {
    Hive.registerAdapter(BackgroundPushMessageAdapter());
  }
  final documentDirectory = await getApplicationDocumentsDirectory();
  Hive.init(documentDirectory.path);
  if (Hive.isBoxOpen('background_notifications')) {
    await Hive.box<BackgroundPushMessage>('background_notifications').close();
  }
  await Hive.openBox<BackgroundPushMessage>('background_notifications');
  await Firebase.initializeApp();
  print('Handling a background message ${message.messageId}');
  BackgroundPushMessage msg = BackgroundPushMessage.fromFCM(message);
  msg.display();
  Hive.box<BackgroundPushMessage>('background_notifications').add(msg);
  print("Notification shown for $msg");
  print(
      "Notification length ${Hive.box<BackgroundPushMessage>('background_notifications').length}");
  if (Hive.isBoxOpen('background_notifications')) {
    Hive.box<BackgroundPushMessage>('background_notifications').close();
  }

请注意,我对后台通知对象和前台通知对象有不同的 class,因为配置单元无法在不同的框中注册相同 class 的两个实例。因此,我必须为每个 class 注册适配器。不过,我让一个 class 扩展了另一个以避免重复代码。