Skip to content

Events ​

Events let features react to what happens in the app without knowing about each other. The platform emits typed lifecycle events, and features can define and emit their own. Everything flows through the EventPlugin, available as vyuh.event.

How the pieces fit together

Scroll or pinch to zoom · Drag to move · Esc to close

The event plugin ​

ContractEventPlugin
Default implementationDefaultEventPlugin (built on event_bus)
Accessorvyuh.event
PluginDescriptor slotevent
ImplementationsDefaultEventPlugin, or your own

Every app gets a DefaultEventPlugin; you only fill the event slot to swap in your own. It is a preloaded plugin, so it starts before the platform and every other plugin. That is why system events such as SystemInitStartedEvent can be delivered at all.

The contract has three methods:

MethodWhat it does
on<T>(listener)Calls listener for every event of type T (or a subtype). Returns a DisposeFunction that cancels the subscription
once<T>(listener)Calls listener for the next event of type T only, then cancels itself. Returns a DisposeFunction that cancels it before the event arrives
emit<T>(event)Sends event to every listener of its type

All events extend VyuhEvent<T>, which has a name, an optional data payload of type T, and a timestamp set when the event is created.

Delivery is asynchronous: emit returns straight away and listeners run shortly after (see Delivery). DefaultEventPlugin.init() creates a fresh bus and dispose() destroys it; calling on or emit before init() throws. Because the plugin is preloaded, it keeps running when the platform restarts. The platform cancels feature handlers on a restart, but a subscription you made with on stays until you cancel it.

If the event plugin fails to start, the app shows its startup failure screen instead of crashing, and Restart tries the plugin again.

Declare handlers on a feature ​

The recommended way to listen is declaratively, with FeatureDescriptor.events. Each EventHandler<T> listens to events of type T:

dart
final account = FeatureDescriptor(
  name: 'account',
  title: 'Account',
  events: [
    EventHandler<SystemReadyEvent>((_) => restoreSession()),
    EventHandler<RouteFailureEvent>((e) => log('Route failed: ${e.failure}')),
    EventHandler<UserSignedOutEvent>((_) => clearCaches()),
  ],
  routes: () => [...],
);

The platform owns these subscriptions:

  • It subscribes a feature's handlers before the feature's init runs, so a feature hears events that other features emit while they initialize.
  • It cancels them when the feature is disposed or the platform restarts. A restart never leaves duplicate listeners behind.
  • A lazy feature gets the same treatment. Its handlers start when it activates, and they are removed again if activation fails and rolls back.

You never store or cancel a subscription yourself.

System events ​

The platform emits these events. All of them extend SystemEvent, so EventHandler<SystemEvent> observes every one.

EventWhenPayload
SystemInitStartedEventPlatform init starts, including on restartisRestart
FeatureLoadedEventAn eager feature finished init, or a lazy feature activatedfeatureName, isLazy
FeatureLoadFailedEventA lazy feature failed to load, or its flag turned it offfeatureName, failure
SystemReadyEventPlugins, features and routes are readynone
SystemInitFailedEventPlatform init failedfailure
RouteFailureEventA route could not be shown, reported once per failurefailure

failure is a VyuhFailure: the same object the error views show, with a kind, a user-facing title and message, the location, and the underlying error.

A load cancelled by a restart or dispose is not a failure, so it emits no FeatureLoadFailedEvent.

The auth plugin also emits a built-in event, UserChangedEvent, every time the current user changes, including sign-out. It is a VyuhEvent<User>, not a SystemEvent. See authentication.

Define and emit your own events ​

Extend VyuhEvent<T>, where T is the payload type (void for none). Use a reverse-domain name:

dart
final class UserSignedOutEvent extends VyuhEvent<void> {
  UserSignedOutEvent() : super(name: 'com.example.account.signedOut');
}

vyuh.event.emit(UserSignedOutEvent());

Feature events are the glue between features. The account feature emits UserSignedOutEvent; a cart feature handles it, and so can a redirect that sends signed-out users to the login page. Neither feature imports the other.

Listen imperatively ​

For listeners with a shorter life than a feature, such as a widget or a controller, subscribe with vyuh.event.on<T>() and cancel with the returned function:

dart
late final DisposeFunction _cancel;

@override
void initState() {
  super.initState();
  _cancel = vyuh.event.on<CartChangedEvent>((e) => setState(() {}));
}

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

vyuh.event.once<T>() listens to the next event of that type only. It cancels itself before it calls your listener. It also returns a DisposeFunction; call it to stop waiting if the event never comes:

dart
final cancel = vyuh.event.once<SystemReadyEvent>((_) => showWelcome());

// Later, if you no longer need it:
cancel();

Delivery ​

Events are delivered asynchronously. emit returns at once, and listeners run shortly after, in subscription order. Don't rely on a listener having run by the line after emit. In widget tests, pump the tester before asserting.

A listener that throws does not stop other listeners from running.

Testing ​

In a widget test, run the app and wait with getReady from vyuh_test. Because delivery is asynchronous, pump (or await pumpEventQueue()) after emit before you assert:

dart
import 'package:flutter_test/flutter_test.dart';
import 'package:vyuh_core/vyuh_core.dart';
import 'package:vyuh_test/vyuh_test.dart';

void main() {
  tearDown(() async {
    if (VyuhBinding.instance.initialized) await vyuh.dispose();
  });

  testWidgets('handles sign-out', (tester) async {
    var signedOut = 0;
    runApp(
      features: () => [
        FeatureDescriptor(
          name: 'cart',
          title: 'Cart',
          events: [EventHandler<UserSignedOutEvent>((_) => signedOut++)],
        ),
      ],
    );
    await vyuh.getReady(tester);

    vyuh.event.emit(UserSignedOutEvent());
    await tester.pump();

    expect(signedOut, 1);
  });
}

For a unit test without an app, create a DefaultEventPlugin() and call await plugin.init() before using it.

Write your own ​

Extend EventPlugin and implement on, once, emit, init and dispose, then pass it in the event slot. Keep it preloaded (the base class already mixes in PreloadedPlugin), match listeners by type including subtypes, so EventHandler<SystemEvent> still sees every system event, and make sure the functions returned by on and once really cancel. Both return a DisposeFunction. See writing a plugin.

See also ​