This is something I’ve seen in most of the apps I’ve been viewing these last years. So today I wanna just share my way of solving this without any external package.
How most apps deals with global events
Imagine your are building a game. At the end of a session you wanna notify an external service that the game has ended.
serviceA.onGameEnd()
Let’s get further now. When our game ends we wants to:
- Check if we have to show a notification
- Check if we have to ask for a review
- Check if we have to ask for a rating
- Increase user experience
- Unlock a new level
So now you start getting into much more trouble You will notify multiple services
serviceA.onGameEnded()
reviewService.onGameEnded()
ratingService.onGameEnded()
experienceService.onGameEnded()
serviceB.onGameEnded()
...
This results in something like this
But what happens once your app get bigger?
You will have more services and more events…
I took an example with page presenter. You can replace this with page notifier if you use riverpod or bloc provider… that’s the idea.
When you start having this You have a big problem of concurrency. You don’t know who is doing something before another. Data flows in a uncontrolable way and when you have a new service that needs to get an event you don’t know where to start.
Plus that’s completely impossible to test this with unit test. Because your code involves too many external things.
Listening for global events
Now that you understand the problem. I am pretty sure you already faced it.
On some little apps with few events. This ain’t a problem. But on many apps with a lot of events…
- Service (or notifier, presenter whatever you call it) should avoid to know each others
- We would like to be able to understand and control the flow of actions (even when we have many more actions)
- Result of events should be easily testable with unit tests
So as a solution we would like to invert the problem. Instead of sending events to everyone Send the event once and let who needs it get it
Real life analogy
If you subscribe to a newspaper or magazine, you no longer need to go to the store to check if there a new edition. Instead, the publisher sends new issues directly to your mailbox right after publication.
How can I notify other services without knowing them?
Instead of notifying directly our different services Our services will listen for events they care about. We will have one global queue to prevent concurrent events
This will completely change the problem
- Services doesn’t know each others
Lets get into real code
Sending events
Now that we understand more the problem and know what we want. We will use the Observer pattern. And good news, Dart has everything already in place to do this easily.
let’s code
class AppEventsDispatcher<T extends AppEvent> {
final StreamController<T> _controller;
late final Stream<T?> _stream;
Stream<T?> get stream => _stream;
final List<T> _onNotificationEventsSubscriber;
AppEventsDispatcher()
: _onNotificationEventsSubscriber = [],
_controller = StreamController() {
_stream = _controller.stream.asBroadcastStream();
}
void dispose() {
_onNotificationEventsSubscriber.clear();
_controller.close();
}
void publish(T event) {
_controller.add(event);
}
}
We use a StreamController to send events. StreamController also has a stream property for those who want to listen. We create the stream and keep it directly when creating the controller here.
We will ensure that AppEventsDispatcher has only one instance So only one queue will be available for our all app
What is asBroadcastStream?
This allow you to have multiple simultaneous listeners. And that’s what we want.
Our AppEvent model will simply be an abstract class
sealed class AppEvent { }
// or
abstract class AppEvent { }
Receiving events
You will have to start listening for events when creating your service
appEventDispatcher.stream.listen(onEvent);
Then just react to events you are interested in
Future<void> onEvent(AppEvent? event) async {
if (event == null) {
return;
}
switch (event) {
case OnStartGameEvent endGameEvent:
// handle startGame
case OnEndGameEvent endGameEvent:
// handle endGameEvent
default:
}
}
If you are using sealed class the switch case will be even more simpler. This is why I personnaly prefer to use sealed class for this kind of things. Also it has the advantage to force me handling any new cases I add.
For example if you are using Riverpod this would look like
part 'lobby_notifier.g.dart';
class LobbyNotifier extends _$LobbyNotifier {
Future<LobbyState> build() async {
...
ref.read(appEventsDispatcherProvider).stream.listen(_onEvent);
return LobbyState(
...
);
}
}
Note: if your notifier is not going to stay alive (keep alive on riverpod) Don’t forget to close the stream subscription
Now our events are dispatched globally and our modules doesn’t have to know each others. So we easily understand how a service react to events. And tests are easy to do as we simply have to publish events
Getting further
- An event can have a priority and be consumed by the first service that needs it
- You can have a queue of events and consume them one by one
- You can have a queue of events and be able to replay them ...
With this kind of design you can easily have a lot of async control on your app. And make sure something will only be done once.
Conclusion
This kind of design pattern is really useful to simplify our code and make it more testable
I hope that it will help you writing better code and make your app more maintainable.