Skip to main content

Services and state

A generated app can have services for events, analytics and crash reporting, each from a role. The code of the app gets them from a DI container when the app has one, and from plain functions when it does not. Either way, the screens reach them only through their state.

The preferences, which remember the settings of the app, come from a role too. The code of the app gets them when the app starts, or from the container, and Preferences and settings describes how.

Sign-in comes from a role as well, but not as a service of the container. The code of the app reaches it through appSession, a top-level variable; see Sign-in.

The services​

RoleServiceFileModule
EventsCommunicationServicelib/core/events/communication_service.dartevent_bus
AnalyticsAnalyticsServicelib/core/analytics/analytics_service.dartfirebase_analytics
Crash reportingCrashReporterlib/core/crash_reporting/crash_reporter.dartfirebase_crashlytics

Each role generates the interface of its service and a function that returns the app's service: createCommunicationService(), createAnalyticsService() and createCrashReporter(). The function returns the same instance every time. An app can have several modules for analytics or crash reporting, and its service then forwards every call to each of them on its own. A module whose service throws or fails keeps no other from the call, and its failure reaches neither the code that called nor the handlers of uncaught errors. In debug mode, the service of the app prints the failure. Each analytics module gets its own copy of the parameters of a call. The app leaves out a service that fails to be created or to start, and works with the others.

The code of the app uses the interface and never the module behind it, so you can replace a module without touching that code.

With a DI container​

With get_it, each service role registers its service in the container as a lazy singleton, the preferences role too, and other modules register services of their own. registerDependencies() does this in bootstrap(), before the first frame.

The generated code follows one rule, and the guide for coding agents of the app gives it to agents for the code that they add: only the composition file of a feature takes services from the container. The composition file, lib/features/<id>/<id>_composition.dart, creates what the screens of the feature need, such as a Cubit, with resolve<T>() of lib/core/di/service_locator.dart. Everything else gets its services as parameters, and the screens talk only to their state.

With BLoC, the composition file creates the Cubit:

lib/features/counter/counter_composition.dart
import 'package:flutter_bloc/flutter_bloc.dart';

import '../../core/di/service_locator.dart';
import 'counter_store.dart';

/// Creates the cubit of the counter screen with the services it needs.
CounterCubit createCounterCubit() => CounterCubit(resolve<CounterStore>());

and the screen provides it with BlocProvider(create: (_) => createCounterCubit(), ...) and reads it with context.read<CounterCubit>().

With Riverpod, the composition file bridges the container to a provider:

lib/features/counter/counter_composition.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../core/di/service_locator.dart';
import 'counter_store.dart';

/// The store of the counter, from the service locator of the app.
final counterStoreProvider = Provider<CounterStore>(
(_) => resolve<CounterStore>(),
);

and the notifiers of the feature read it with ref. Write a feature module builds this feature from start to end.

A value known only at run time, such as the id of the item that a screen shows, is a parameter of the function in the composition file, as in DetailsCubit createDetailsCubit(int id). Elsewhere, a factory class creates objects with such values: it takes its services in its constructor, creates the objects in a method such as call(int id), and is registered like any other service, so the code that needs it gets it as a parameter.

In tests, resetDependencies() of lib/core/di/dependencies.dart disposes of the services that the container created, in the reverse order of their registration, and removes them all, so that registerDependencies() can register them again.

Without a container​

Without a DI container, call the function of the service where you need it, such as in the code that creates a Cubit:

final analytics = createAnalyticsService();

It returns the same service every time, so every part of the app shares it. Don't create another one from the module's own implementation, such as createEventBusCommunicationService(), because the listeners of one event service do not get the events fired into another.

This is for the code that you write in the app. In the code that modules generate, only the DI container calls these functions, and the contract harness checks that. A feature resolves its services in its composition file instead, which is why a feature with services requires the DI role.

Events​

The events role lets parts of the app that do not know each other exchange events. An event is a class that extends AppEvent:

final class ItemAdded extends AppEvent {
const ItemAdded(this.id);

final int id;
}

fire sends an event, and on<T>() is the stream of the events of a type:

events.fire(const ItemAdded(5));

final subscription = events.on<ItemAdded>().listen((event) {
// An item was added somewhere in the app.
});

An event goes once to every listener of its type that is listening when the event is fired, and the events arrive in the order they were fired. Whether it also goes to the listeners of a type that it extends or implements depends on the module, and it never goes to a listener of any other type. The stream of on<T>() has only the events fired after it is listened to, so an event fired between the call of on<T>() and listen is not in it. A cancelled subscription gets no more events. With event_bus, an event also goes to the listeners of the types it extends or implements, so on<AppEvent>() gets every event, and listeners get an event after the code that fires it has completed.

Analytics​

AnalyticsService logs events and describes the user: logEvent, logSignIn, logSignUp, setUserId, setUserProperty and setAnalyticsCollectionEnabled. In an app with a router, screen views need no code: firebase_analytics logs one for each screen the user sees. See firebase_analytics.

Crash reporting​

bootstrap() calls installCrashReporting() of the role once the reporting services are ready. It reports as fatal the errors of the main isolate that the app does not handle, through two handlers. FlutterError.onError gets the errors that Flutter catches, such as in a build or a layout, and still presents them as Flutter does by default. PlatformDispatcher.instance.onError gets every other uncaught error, such as one in a Future or a Timer, and in debug mode it also leaves the error unhandled, so the engine still prints it.

Report other errors with recordError, add context to the next report with log, and set the user with setUserId. The reporter does not print what it reports, so an error that the app reports is not printed either.

The handlers cover the main isolate. compute() and Isolate.run() throw the error of their isolate to the code that awaits them, so it reaches the handlers unless that code catches it. For an isolate that the app spawns itself, report its errors in its error listener, such as the onError port of Isolate.spawn.

State​

The state management role holds the state of screens. bloc adds flutter_bloc, and riverpod adds flutter_riverpod and wraps the app in a ProviderScope. A feature that keeps state brings its Cubits or providers in a variant for each state manager, so the same feature works with either.

With either state manager, the widgets of a screen talk only to their state, a Cubit through context.read or a provider through ref, and the state talks to the services. Widgets never reach into the DI container.

The screens of sign_in are written this way with both. A screen provides or watches its state and passes plain values and callbacks to a view, a widget that imports no package of a state manager. State that several screens read, such as who uses the app, is provided around the root of the app with bloc, and is a provider of its own with riverpod.