Я создал службу уведомлений в памяти для использования в своих базах кода Dart/Flutter. Я начал использовать шаблон Observer, но быстро обнаружил, что подписка на изменения в глубоко вложенных моделях представлений стала болезненной и запутанной.
Цель состоит в том, чтобы создать одноэлементную службу, которая сможет публиковать уведомления строго типизированным способом. Потребители могут подписаться на целевые уведомления, а также получать полезную нагрузку строго типизированным способом.
У меня есть два разных уведомления, которые я написал, которые фактически предоставляют одну и ту же полезную нагрузку. Первый работает, а второй выдает следующую ошибку времени выполнения.
Unhandled exception:
type '(ExampleNotification) => void' is not a subtype of type '(PublishedNotification) => dynamic'
#0 NotificationService.publish (package:console/notification_service.dart:42:24)
#1 main (package:console/console.dart:15:23)
#2 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#3 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)
Каждое уведомление расширяет базовый класс PublishedNotification
.
abstract class PublishedNotification {
String qualifier;
PublishedNotification(this.qualifier);
@override
String toString() {
return 'PublishedNotification{qualifier: $qualifier}';
}
}
import 'package:console/published_notification.dart';
class HelloWorldNotification extends PublishedNotification {
HelloWorldNotification() : super('Hello World Message');
}
import 'package:console/published_notification.dart';
class ExampleNotification extends PublishedNotification {
ExampleNotification() : super('This is an example');
}
Каждое уведомление может быть опубликовано кем угодно, и уведомление получат только подписчики. Когда объект подписывается на уведомление, создается подписка, содержащая обратный вызов для вызова подписчика, а также тип уведомления, на которое нацелена подписка.
import 'dart:mirrors';
import 'package:console/published_notification.dart';
class Subscription<T extends PublishedNotification> {
final Function(T) subscriberCallback;
Type targetNotification = reflectClass(T).reflectedType;
Subscription(this.subscriberCallback);
}
Подписка, публикация и отписка осуществляется через NotificationService
. При вызове метода subscribe
подписчик передает обратный вызов, который будет вызываться каждый раз при публикации уведомления. Он также возвращает сам Subscription
, чтобы вызывающий абонент мог отказаться от подписки и выполнить очистку во время удаления.
Когда вызывается publish
, он принимает аргумент, расширяющий PublishedNotification
. Некоторое отражение используется для определения типа, а затем я извлекаю все подписки из коллекции подписчиков и вызываю их обратные вызовы с уведомлением.
import 'dart:mirrors';
import 'package:console/published_notification.dart';
import 'package:console/subscription.dart';
class NotificationService {
// List of subscribers grouped by notification type.
final Map<Type, List<Subscription>> subscriptions = <Type, List<Subscription>>{};
// Subscribe to a notification.
// notification is a required named parameter.
// A secondary 'condition' parameter will be added to support conditional pushing a notification to a subscriber based on if the condition passes or not.
Subscription<T> subscribe<T extends PublishedNotification>({required Function(T payload) notification}) {
// Wrap notification callback into a subscriber.
final newSubscriber = Subscription(notification);
ClassMirror notificationClass = reflectClass(T);
Type notificationType = notificationClass.reflectedType;
// Store the subscriber into our subscriptions collection.
late List<Subscription> subscribers;
if (subscriptions[notificationType] == null) {
subscribers = List.empty(growable: true);
subscriptions[notificationType] = subscribers;
}
subscribers.add(newSubscriber);
// Return so that it can be provided during an unsubscribe request.
return newSubscriber;
}
// Publish a notification to all subscribers
void publish<T extends PublishedNotification>(T notification) {
Type notificationType = notification.runtimeType;
if (subscriptions[notificationType] == null) {
return;
}
List<Subscription> subscribers = subscriptions[notificationType]!;
// Invoke the callback delegate we have stored within each subscriber.
for (Subscription targetSubscriber in subscribers) {
targetSubscriber.subscriberCallback(notification);
}
}
// Unsubscribe the
void unsubscribe(Subscription subscriber) {
subscriptions[subscriber.targetNotification]?.remove(subscriber);
}
}
У меня есть пример консольного приложения Dart, которое я создал, чтобы воспроизвести проблему, с которой я столкнулся. Я подписываюсь на два разных уведомления, публикую уведомления, а затем отказываюсь от подписки. Когда HelloWorldNotification
публикуется, мой обратный вызов вызывается без каких-либо проблем. Когда ExampleNotification
публикуется, я получаю исключение, которое показано выше.
import 'package:console/example_notification.dart';
import 'package:console/hello_world_notification.dart';
import 'package:console/notification_service.dart';
import 'package:console/published_notification.dart';
import 'package:console/subscription.dart';
void main() {
var notificationService = NotificationService();
Subscription helloWorldSubcription =
notificationService.subscribe<HelloWorldNotification>(notification: handleHelloWorldNotification);
Subscription exampleSubscription =
notificationService.subscribe<ExampleNotification>(notification: handleExampleNotification);
notificationService.publish(HelloWorldNotification());
notificationService.publish(ExampleNotification());
notificationService.unsubscribe(helloWorldSubcription);
notificationService.unsubscribe(exampleSubscription);
// These should not hit the handlers below.
notificationService.publish(HelloWorldNotification());
notificationService.publish(ExampleNotification());
}
void handleHelloWorldNotification(PublishedNotification payload) {
print(payload.qualifier);
}
void handleExampleNotification(ExampleNotification payload) {
print(payload.qualifier);
}
Я не понимаю, почему первое уведомление работает нормально, а второе нет. Они оба одинаковы с точки зрения реализации, и обратные вызовы ничем не отличаются. Я предполагаю, что это проблема с тем, как я делаю выводы об дженериках внутри NotificationService
, но я не уверен на 100%, почему это так.
Я могу выполнить приведение в цикле for для каждого Subscription
, и проблема исчезнет, и все будет работать как положено.
// Publish a notification to all subscribers
void publish<T extends PublishedNotification>(T notification) {
Type notificationType = notification.runtimeType;
if (subscriptions[notificationType] == null) {
return;
}
List<Subscription> subscribers = subscriptions[notificationType]!;
// Invoke the callback delegate we have stored within each subscriber.
for (Subscription targetSubscriber in subscribers) {
var s = targetSubscriber as Subscription<T>; // Cast here
s.subscriberCallback(notification);
}
}
Есть ли что-то явно неправильное в моей реализации метода publish
и в том, как я храню обратные вызовы подписки? У меня есть исправление, но я хочу понять причину, по которой первая публикация работает, а вторая нет, если я не принудительно приведу актеров. Будет ли в конечном итоге требоваться приведение типов или есть способ немного провести рефакторинг, чтобы приведение типов не требовалось?
Спасибо!
Хорошо, похоже, что dart:mirrors
не поддерживается во Flutter. Мне пришлось искать другой способ сбора информации о типах, и я обнаружил, что мне на самом деле не нужно использовать reflactClass
. Когда я обновил свой NotificationService
, чтобы исключить использование dart:mirrors
, исключения перестали возникать.
import 'dart:mirrors';
import 'package:console/published_notification.dart';
import 'package:console/subscription.dart';
class NotificationService {
// List of subscribers grouped by notification type.
final Map<Type, List<Subscription>> subscriptions = <Type, List<Subscription>>{};
// Subscribe to a notification.
@override
Subscription<T> subscribe<T extends PublishedNotification>({required Function(T payload) notification}) {
// Wrap notification callback into a subscriber.
final newSubscriber = Subscription(notification);
// Store the subscriber into our subscriptions collection.
late List<Subscription> subscribers;
if (subscriptions[T] == null) {
subscribers = List.empty(growable: true);
subscriptions[T] = subscribers;
}
subscribers.add(newSubscriber);
// Return so that it can be provided during an unsubscribe request.
return newSubscriber;
}
// Publish a notification to all subscribers
@override
void publish<T extends PublishedNotification>(T notification) {
if (subscriptions[T] == null) {
return;
}
List<Subscription> subscribers = subscriptions[T]!;
// Invoke the callback delegate we have stored within each subscriber.
for (Subscription targetSubscriber in subscribers) {
targetSubscriber.notify(notification);
}
}
// Unsubscribe the
@override
void unsubscribe(Subscription subscriber) {
subscriptions[subscriber.targetNotification]?.remove(subscriber);
}
}
Это изменение также коснулось моего класса Subscription
.
class Subscription<T extends PublishedNotification> {
final Function(T) _subscriberCallback;
Type targetNotification = T;
Subscription(this._subscriberCallback);
void notify(T notification) {
_subscriberCallback(notification);
}
}
Актерский состав мне больше не нужен, и кажется, все работает так, как ожидалось. Мне все еще интересно понять, почему reflectClass.reflectedType
вызвало проблемы, но просто перейти T
прямо ко всему - нет.
Вы проводите его с актерами, которые я показываю внизу, или без них? У меня не возникает исключения, когда я бегу с актерским составом, но я обходлюсь без него.
Не вижу исключения с приведением as Subscription<T>
или без него.
Хорошо, я удалил приведение, и теперь оно тоже компилируется. Я провел некоторый рефакторинг (и обновил свой пост, чтобы отразить это). Если я вернусь к использованию Type targetClass = reflectClass(T).reflectedType
вместо нового способа доступа к типу, это выдаст исключение. Я верну свой обновленный пост к оригиналу, а затем опубликую ответ, показывающий устранение использования dart:mirrors
, решенное для него. Однако мне хотелось бы знать, почему dart:mirrors
выдает исключение.
Хорошо @mmcdon20 — я вернул свое сообщение к оригиналу, поэтому теперь случаются исключения. Я также отредактировал его, чтобы показать внесенные мной изменения (передавая везде T
вместо reflectClass(T).reflectedType
и как это решило проблему.
Глядя на ваш код, видно, что изменение между T
и reflectClass(T).reflectedType
не устранило проблему, вместо этого вы ввели новую функцию notify
в свой класс Subscription
, и это, похоже, изменение, которое остановило возникновение исключения.
Другими словами, targetSubscriber.subscriberCallback(notification);
вызывает исключение, а targetSubscriber.notify(notification);
— нет. Я не совсем понимаю, почему на данный момент.
Кроме того, это несвязанная проблема: вы неправильно используете late
в функции подписки. Это вызовет исключение, если вы попытаетесь подписаться на один и тот же тип дважды. Вместо этого вам, вероятно, нужен тип, допускающий значение NULL.
Спасибо что подметил это. Я вернулся, провел повторное тестирование и теперь тоже это вижу. Я предполагаю, что это связано с тем, что когда вызывается обратный вызов, он по какой-то причине не знает, что такое T (несмотря на то, что он доступен в методе publish
), тогда как когда я вызываю обратный вызов из метода notify
, T известен. Думаю, я бы в какой-то степени понял это, если бы не тот факт, что первая публикация может это понять, а вторая — нет. Возможно, T захватывается как HelloWorldNotification
внутри этого контекста во время выполнения и не освобождается при поступлении второго вызова?
Ну, в первом примере обратный вызов принимает PublishedNotification
в качестве аргумента, а во втором примере — более конкретный ExampleNotification
. Если вы обновите первый обратный вызов, чтобы принять HelloWorldNotification
, то ни то, ни другое не сработает.
Ах хорошо. Я совершенно не заметил, что у моих обратных вызовов была другая подпись. Спасибо что подметил это. Я также заменил использование late
нулевым оператором присваивания - List<Subscription> subscribers = subscriptions[notificationType] ??= List.empty(growable: true);
Так что, в конце концов, это связано с тем фактом, что T неизвестен, когда обратный вызов вызывается из метода publish
. Даже если я изменю код примера кода, включив <T>, например service.publish<ExampleNotification>(ExampleNotification());
, он все равно не сможет определить T в методе publish
.
Думаю, я разобрался в проблеме. Я опубликую ответ ниже.
Рассмотрим следующий код:
class PublishedNotification {}
class Subscription<T extends PublishedNotification> { }
class GenericClass<T> {}
void main() {
print(GenericClass().runtimeType); // GenericClass<dynamic>
print(Subscription().runtimeType); // Subscription<PublishedNotification>
}
Как вы можете видеть выше, поскольку вы ограничили универсальный тип предложением T extends PublishedNotification
, когда вы используете Subscription
в качестве необработанного типа, он теперь эквивалентен Subscription<PublishedNotification>
, а не Subscription<dynamic>
, как вы могли бы ожидать.
Имея это в виду, следующий код
for (Subscription targetSubscriber in subscribers) {
targetSubscriber.subscriberCallback(notification);
}
действительно,
for (Subscription<PublishedNotification> targetSubscriber in subscribers) {
targetSubscriber.subscriberCallback(notification);
}
И если вы наведете указатель мыши на targetSubscriber
в редакторе, он сообщит вам, что это тип Subscription<PublishedNotification>
.
Теперь вы используете вышеизложенное со следующими обратными вызовами:
void handleHelloWorldNotification(PublishedNotification payload) {
print(payload.qualifier);
}
void handleExampleNotification(ExampleNotification payload) {
print(payload.qualifier);
}
С первым обратным вызовом проблем нет, поскольку типы совпадают с PublishedNotification
, но со вторым обратным вызовом возникает проблема. Subscription<PublishedNotification>
должен иметь возможность использовать обратный вызов для любого PublishedNotification
, но фактический обратный вызов ограничен только принятием ExampleNotification
, поэтому и возникает исключение.
Ах, это имеет смысл, если увидеть это таким образом. Итак, это сводится к проблеме дисперсии — с моим использованием дженериков. Это объясняет, почему вызов notify
в классе Subscription
решает эту проблему, поскольку T явно является дочерним классом в этом контексте. Спасибо!
Я не получаю исключения при попытке запустить ваш код.