Skip to main content

Services and state

A generated app can have three services, each from a role: events, analytics and crash reporting. 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 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 all of them.

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, and other modules register services of their own. registerDependencies() does this in bootstrap(), before the first frame.

The generated code follows one rule, and your own code will be easier to change if it follows it too: 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.

resolveWith<T>(param1, [param2]) returns a new service from a factory that takes up to two values from the caller. In tests, GetIt.instance.reset() removes the services again and disposes of those that were created, in the reverse order of their registration.

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.
});

With event_bus, an event goes to everyone listening to its type or to a type it extends or implements, so on<AppEvent>() gets every event. Listeners get an event after the code that fires it has completed, and only the events fired after they started listening.

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 prints nothing itself, 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.