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

Ola*_*med 5 dart dart-isolates flutter firebase-cloud-messaging flutter-hive

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

现在的问题是,应用程序在后台运行时收到的通知(不是终止/终止)保存在配置单元中,但导航到通知屏幕时屏幕不会更新(看不到配置单元中的更新),直到应用程序终止并且重新运行。

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

这是我到目前为止的实现

push_message.dart 实例保存在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());
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

notification.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'),
      ),
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

后台消息处理程序

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();
}
Run Code Online (Sandbox Code Playgroud)

Ola*_*med 7

我已经找到解决办法了。

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

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

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();
  });
}
Run Code Online (Sandbox Code Playgroud)

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();
  }
Run Code Online (Sandbox Code Playgroud)

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