How to manage global events in your Flutter app

Published on

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:

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

global app events flow

But what happens once your app get bigger?

You will have more services and more events…

global app events flow

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…

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

global app events flow

This will completely change the problem

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

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.

Create a 5 stars app using our Flutter templates

Check our flutter boilerplate
kickstarter for flutter apps
Read more
You may also be interested in
How to build a sidebar menu with GoRouter nested navigation in Flutter  blog card image
How to build a sidebar menu with GoRouter nested navigation in Flutter
Published on 2025-01-17
A new Flutter template dashboard for ApparenceKit and Supabase  blog card image
A new Flutter template dashboard for ApparenceKit and Supabase
Published on 2025-01-21
ApparenceKit is a flutter template generator tool by Apparence.io © 2025.
All rights reserved