# Provide a role

A module provides a role with a provider object in its descriptor. What the provider has to do depends on the role. Some roles need only a package, and others give the provider their data to render. The examples come from the built-in modules.

## A plain provider

A module whose files and dependencies alone implement the role uses `RoleProvider.plain`. This is `bloc`, which adds its package and its note for coding agents:

```dart
final class BlocModule extends SmfModule {
  /// Creates the module.
  const BlocModule();

  /// The id of the module, by which other modules key their variants for
  /// BLoC.
  static const id = ModuleId('bloc');

  @override
  ModuleDescriptor get descriptor => const ModuleDescriptor(
        id: id,
        description: 'BLoC with flutter_bloc',
        kind: ModuleKinds.infrastructure,
        providers: [RoleProvider.plain(stateManagementRole)],
      );

  @override
  List<Contribution> contribute(ModuleContext context) => [
        const PubspecContribution.hosted('flutter_bloc', '^9.1.1'),
        AppEntryRole.agentSections.entry(
          stateManagementRole.description,
          AgentNote(agentNote),
        ),
      ];
}
```

A provider of the state management role brings its package with a constraint of its own, which makes the package belong to the provider. Features add it only in their variant for this provider, with the constraint `any`. The provider's id is the key of those variants, so choose it with care. `riverpod` also adds its `ProviderScope` to the root wrappers.

### The packages of a provider

A provider brings a package when it adds the package outside a variant, with a constraint other than `any`. The package then belongs to the provider, unless a module that the provider depends on brings it too. In an app with the provider, every other module that adds the package, with any constraint, must take it from the provider: in its variant for the provider, with `any`, or through a dependency on the provider. `smf create` reports any other module that adds it. By default it leaves that module out of the app, and with `--strict` it stops.

So bring only the packages that implement your role. A package that many modules add, such as `collection`, stays shared as long as no provider brings it, so add such a package with `any`. If your provider brought it with a constraint of its own, every module that adds it without a variant for your provider or a dependency on it would be reported in the apps that have your provider. Two providers that bring the same package, neither depending on the other, are both reported in an app that has both.

A module that imports the package of a provider adds the package too, even when it depends on the provider, so that the check above sees it. The contract harness reports code that imports or exports such a package without adding it. Your provider may import, for a fragment or in a file that its render hook generates, the package that a module which gives your role data adds, since the code of the data may need it.

## A service: events, analytics, crash reporting

The events, analytics and crash reporting roles generate the interface of their service and everything around it: the function that returns the app's service, a service that forwards every call to all providers when the role takes several, the registration in the DI container, and start-up code. A provider only implements the interface and contributes its implementation as data of the role. This is `event_bus`:

```dart
final class EventBusModule extends SmfModule {
  /// Creates the module.
  const EventBusModule();

  /// The id of the module.
  static const id = ModuleId('event_bus');

  static const _file = ImportRef.app(
    'core/events/event_bus_communication_service.dart',
  );

  @override
  ModuleDescriptor get descriptor => const ModuleDescriptor(
        id: id,
        description: 'Events between parts of the app with event_bus',
        kind: ModuleKinds.infrastructure,
        providers: [RoleProvider.plain(eventsRole)],
      );

  @override
  List<Contribution> contribute(ModuleContext context) => [
        BrickContribution(eventBusBundle),
        const PubspecContribution.hosted('event_bus', '^2.0.1'),
        eventsRole.data(
          const RoleImplementation(
            type: TypeRef('EventBusCommunicationService', import: _file),
            create: FactoryRef(
              'createEventBusCommunicationService',
              import: _file,
            ),
          ),
        ),
        AppEntryRole.agentSections.entry(
          eventsRole.description,
          AgentNote(agentNote),
        ),
      ];
}
```

Its brick implements the interface of the role, which it imports from the role's file:

```dart title="lib/core/events/event_bus_communication_service.dart"
import 'package:event_bus/event_bus.dart';

import 'communication_service.dart';

CommunicationService createEventBusCommunicationService() =>
    EventBusCommunicationService(EventBus());

final class EventBusCommunicationService implements CommunicationService {
  EventBusCommunicationService(this._eventBus);

  final EventBus _eventBus;

  @override
  void fire(AppEvent event) => _eventBus.fire(event);

  @override
  Stream<T> on<T extends AppEvent>() => _eventBus.on<T>();
}
```

- A provider contributes exactly one implementation, and only a provider of the role contributes one. No module puts code into the socket of the implementations, which the template fills.
- The function that creates the implementation takes no arguments, so the service works without a DI container. The role registers the service in the container when there is one.
- An implementation that starts asynchronously uses `RoleImplementation.async(type: ..., init: ...)`, whose function returns a `Future`. The role awaits it in the platform phase of `bootstrap()`.
- A crash reporter only reports. The template of the crash reporting role installs the handlers of errors, which present the errors as Flutter does, so the reporter prints nothing. A provider whose SDK installs handlers of its own turns them off.

### What the services promise

An events service delivers each event once to every listener of its type that is listening when the event is fired, in the order the events were fired. Whether it also delivers the event to the listeners of a type that the event extends or implements, such as `on<AppEvent>()`, is up to the provider, and `event_bus` does. A listener of any other type gets neither the event nor an error. The stream of `on<T>()` has only the events fired after it is listened to, so an event fired after `on<T>()` returned the stream, but before `listen`, is not in it. A cancelled subscription gets no more events.

An app can have several providers of analytics and of crash reporting, which know nothing of each other, so the service of the role calls each of them on its own. A provider that throws, or whose future fails, keeps no other provider from the call, and its failure reaches neither the code that called nor the handlers of the errors of the app. In debug mode the service prints it, so a provider that does not work shows in the console. Each analytics provider gets its own copy of the map of parameters, and one that changes the map changes nothing for the caller or for the other providers. A provider whose factory throws, or whose asynchronous start fails, is left out, and the app starts and works with the others.

SMF's CI checks these promises in running apps. The test of the events role runs in every app with the role in [the matrix of the built-in modules](https://doc.saymyframe.com/contributing.md#generated-apps). The tests of analytics and crash reporting run in an app with the Firebase providers of both roles next to fixture providers of the same roles.

### Preferences

The preferences role is a service role too: its template generates the `AppPreferences` interface, and a provider contributes one implementation of it. This is what `shared_preferences` contributes:

```dart
@override
List<Contribution> contribute(ModuleContext context) => [
      BrickContribution(sharedPreferencesBundle),
      const PubspecContribution.hosted('shared_preferences', '^2.5.5'),
      preferencesRole.data(
        const RoleImplementation.async(
          type: TypeRef('SharedAppPreferences', import: _file),
          init: FactoryRef('openSharedAppPreferences', import: _file),
        ),
      ),
      AppEntryRole.agentSections.entry(
        preferencesRole.description,
        AgentNote(agentNote),
      ),
    ];
```

The role does more with the implementation than create it. `initPreferences()` of the role opens the preferences with the function of the implementation and then gives them to the [restorers](https://doc.saymyframe.com/concepts/settings.md#preferences) of the modules, and `bootstrap()` awaits it in its platform phase, whether the implementation is created asynchronously or not. A test runs `initPreferences()` again to see what the next launch reads, so the function of the implementation reads what is saved each time it is called.

Every implementation keeps what the interface promises:

- A read is synchronous, from memory, and never throws. It returns `null` when the key has no value of the type it asks for. Whether a number saved as an `int` is read as a `double`, or the other way round, is up to the provider.
- Once the future of a write completes, reads return what it saved, in this run of the app and after its next launch.
- A key has one value. A write replaces what the key had, a value of another type too, and `remove` makes the reads of the key return `null` again.
- The preferences keep a copy of a list they are given, and a read returns a copy of it.

A package may promise less than the role, and the implementation then makes up the difference. The reads of the shared_preferences package cast the value and throw on another type, and the package keeps the list that it is given, so `SharedAppPreferences` checks the type of each value itself and hands the package a copy of each list.

SMF's CI checks these promises in running apps with every module that provides the role, and on devices, where the platform side of the provider is the real one; see [Contributing](https://doc.saymyframe.com/contributing.md#the-roles-at-run-time).

### Notifications and push messages

The notifications role and the push role take one implementation each, of `NotificationsService` and of `PushService`. As with the auth role, the code of the app never calls the service of a provider. It uses `appNotifications` and `appPush`, which the templates generate and which do what is the same with every provider: the permission and what the app remembers of it, the owners of notifications, the address of the device. Neither role registers anything in a DI container, and `bootstrap()` awaits `initNotifications()` and then `initPush()` in its platform phase. This is what `local_notifications` contributes for the role, next to its native settings:

```dart
notificationsRole.data(
  const RoleImplementation.async(
    type: TypeRef('LocalNotificationsService', import: _file),
    init: FactoryRef('openLocalNotifications', import: _file),
  ),
),
```

[Notifications and push messages](https://doc.saymyframe.com/guides/notifications.md#for-a-provider) sums up what a provider of each keeps to, and the doc comments of the two interfaces in the app hold the full contracts.

### Purchases

The purchases role takes one implementation, of `PurchasesService`. The code of the app never calls the service of a provider: it uses `appPurchases`, which the template generates and which does what is the same with every provider, such as whose the purchases are and what the app does while they are not set up. The role registers nothing in a DI container, and `bootstrap()` awaits `initPurchases()` in its platform phase. This is what `revenuecat` contributes for the role:

```dart
purchasesRole.data(
  const RoleImplementation(
    type: TypeRef('RevenueCatPurchasesService', import: _file),
    create: FactoryRef('createRevenueCatPurchasesService', import: _file),
  ),
),
```

[Purchases](https://doc.saymyframe.com/guides/purchases.md#for-a-provider) sums up what a provider keeps to, and the doc comments of the interface in the app hold the full contract.

### Values in .env

A provider of the role of the values in `.env` generates `AppEnv` in `lib/core/env/app_env.dart`, with a getter for each key of `envRole.keysIn(input)`, from its render hook. A provider that needs arguments on the commands of the `flutter` tool that build, run or test the app gives them to the role, as `dart_define` does:

```dart
envRole.data(const EnvToolArguments(['--dart-define-from-file=.env'])),
```

[Values in .env](https://doc.saymyframe.com/guides/env.md#for-a-provider) tells what `AppEnv` keeps to.

### REST API

The REST role takes one implementation, of `RestTransport` in `lib/core/rest/rest_transport.dart`: one exchange on the wire, and nothing else. The template generates `appRest` over it, and everything that the client guarantees is in that code: the join of a path and the address, the failures, the repeats, the redirects, the timeout, the token of the user. So a second provider keeps every guarantee by construction. The role registers nothing in a DI container and adds nothing to `bootstrap()`, so the template refuses an implementation that is created asynchronously. This is what `dio` contributes for the role:

```dart
restRole.data(
  const RoleImplementation(
    type: TypeRef('DioRestTransport', import: _file),
    create: FactoryRef('createDioRestTransport', import: _file),
  ),
),
```

The doc of `RestTransport` holds the contract, which only a running app shows:

- `send()` makes one attempt, and a provider repeats nothing.
- A provider follows no redirect: a 3xx is a reply. `appRest` follows them, since it knows which header may go where.
- Any status is a reply, and it keeps the headers of the reply, with their names in lower case. A provider whose stack decoded a body leaves out `content-encoding` and its `content-length`, which no longer tell of the bytes. A body in a coding that the stack did not decode keeps them.
- A failure to connect, a TLS handshake or a certificate that fails, and a connection that breaks are `RestConnectionFailed`. On `dart:io` those are `SocketException`, `HttpException` and `TlsException`. Any other error is a bug of the provider, which reaches the caller as the reason `unknown`.
- When `RestExchange.stop` completes, a provider closes the connection at once. It needs no timeout of its own, since `appRest` stops every exchange. On `dart:io` the call ends at once, but a TLS handshake on its way cannot be ended, not even by `close(force: true)`: its socket closes when the handshake ends or the server ends it. A provider that shares one `HttpClient` cannot end a TCP connect on its way either: the system ends it.
- On `dart:io`, a provider that keeps an `HttpClient` makes a new one when `HttpOverrides` change, as they do when a test starts or closes a `RestTestServer`.
- It sends the address as it is, with those headers and that body. Its stack may add the headers of the transport itself, such as `host`, `user-agent` and `accept-encoding`. It adds no credential or cookie, and keeps none between two calls.
- It writes no header, no query and no body to a log or a hint.
- Its function takes no arguments, never throws and does not wait, and the implementation works from the first call.

The tests of the role run in every app with a provider, so a new provider gets them; see [Contributing](https://doc.saymyframe.com/contributing.md#the-roles-at-run-time).

### Sign-in

The auth role takes one implementation too: its template generates the `AuthService` interface, and a provider contributes its implementation of it. Unlike the other services, the code of the app never calls this one. It signs in through `appSession`, which the template generates and which does what is the same with every provider. The role registers nothing in a DI container. [Provide sign-in](https://doc.saymyframe.com/extending/provide-auth.md) describes what the service keeps to.

## Following the router

A provider can work with another role through the roles its role uses. The analytics role uses the router, so `firebase_analytics` gives the router a listener of the screen, only when the app has a router:

```dart
const SocketContribution.item(
  RouterRole.screenListeners,
  Fragment(
    'logFirebaseScreenView',
    imports: [
      ImportRef.app(
        'core/analytics/firebase_analytics_service.dart',
        show: ['logFirebaseScreenView'],
      ),
    ],
  ),
  when: {routerRole},
),
```

The listener is a function of the module's own file, which exists only in an app with a router:

```dart
{{#has_router}}
void logFirebaseScreenView(String? route, String location) {
  final name = route ?? (Uri.parse(location).path == '/' ? '/' : null);
  if (name == null) return;
  FirebaseAnalytics.instance.logScreenView(screenName: name).catchError(
    (Object error) => debugPrint('Firebase Analytics: $error'),
    test: (error) => error is PlatformException,
  );
}{{/has_router}}
```

A listener has the type `void Function(String? route, String location)`. It follows the screen the user sees, which is the page of the router on top of the app. Pages that the app shows past the router, such as with `Navigator.push`, and dialogs are not pages of the router. Every provider of the router promises to call each listener once for each change of that screen:

- when the app shows its first screen;
- when another page comes on top: a page that a navigation shows, a page that shows again as the pages above it close, or the page of the tab, or other branch of the main navigation, that the user selects;
- when the page on top shows another location, such as the same route with other values of its parameters.

The router does not call a listener for the pages that a navigation puts below the one on top, such as the parents of a route that `go()` shows, and never twice in a row for the same page at the same location. So a listener hears of the back button of the system only when the button closes a page of the router: when it closes a dialog, the page below stays the screen the user sees.

The listener gets the full name of the route, such as `home.home`, and the location with its query and fragment. For a screen that is not a route of a module, the route is `null` and the location is still that of the screen: `/` for the fallback start screen, or the location that the router could not show for its error screen. The router may call a listener while the app builds, so a listener only takes note of the screen. It does not navigate, rebuild widgets or throw.

The router calls each listener on its own, since the modules of the listeners know nothing of each other. A listener that throws anyway keeps no other listener from hearing of the screen, and what it throws reaches neither the router nor the handlers of the errors of the app. In debug mode the router prints it, so a listener that fails shows in the console.

`RouterRole.observers` takes factories of `NavigatorObserver`s instead, which a router calls once for each navigator it creates. An observer sees the pages of one navigator, not the screen the user sees, so use the listeners to follow screens.

## Registering services

A module that creates services of the app registers them as data of the DI role with `diRole.data(DiRegistration(...))`, and the provider of the role renders them in the form of its container:

| Field | Meaning |
| --- | --- |
| `type` | The type the service is registered as, usually an interface, with the import that declares it. |
| `create` | The top-level function that creates it. Its `deps` are the services it takes, which the container passes in that order. |
| `lifetime` | `lazySingleton`, the default, is created on first use. `singleton` is created while the services are registered, and `factory` makes a new one on every use. |
| `isAsync` | The function returns a `Future`. Only a singleton can be created asynchronously, and `registerDependencies()` waits for it. |
| `dependsOn` | Services that a singleton must wait for although it does not take them. |
| `dispose` | A top-level function that takes the instance of a singleton or lazy singleton and disposes of it when `resetDependencies()` resets the container. |
| `instanceName` | A name that tells this service apart from others of the same type. |

A value known only at run time, such as the id of the item that a screen shows, is not part of a registration. A function in the composition file of a feature takes it as a parameter, as in `DetailsCubit createDetailsCubit(int id)`. Elsewhere, a factory class that takes its services in its constructor creates the objects with such values in a method such as `call(int id)`. It is registered like any other service, and other services take it in their `deps`.

The DI role checks the registrations of the app before anything is generated. A service registered twice is an error, and so are a dependency that nobody registers, a cycle, and a singleton that waits for a service that is neither created asynchronously nor waits itself. The fields beyond the basics need capabilities of the container, which each provider declares, and get_it has them all.

The provider of the role renders every registration: its files call or tear off the factory of each registration, and its dispose function if it has one. The contract harness checks this in the apps of every provider. So the tests of a module that registers services read the registrations with `diRole.graphOf`, not in the files of a provider; see [Test a module](https://doc.saymyframe.com/extending/testing.md#checking-one-app).

The provider also declares `Future<void> resetDependencies()` in `lib/core/di/dependencies.dart`, next to `createServiceLocator()` and `registerDependencies()`. It disposes of the services that the container created, each with its `dispose` function, in the reverse order of their registration. Then it removes every service, so that `registerDependencies()` can register them again. It creates nothing just to dispose of it, so a lazy singleton that nothing resolved is neither created nor disposed of. The app itself never calls `resetDependencies()`, but its tests can, such as a test that registers the services again.

SMF's CI runs the start-up of the apps of every provider of the DI role and checks that every registered service resolves, a singleton or lazy singleton to the same instance each time. It also checks that none resolves after `resetDependencies()` and that all of them resolve again after `registerDependencies()`. The services of the fixtures note what their `dispose` functions do, so CI checks the order of the disposal too.

## A provider that renders its data

A role with data gives it to its provider, which turns it into code. `go_router` renders the routes of all features into the variables of its brick:

```dart
final class _GoRouterProvider extends RoleProvider<RoutesData> {
  const _GoRouterProvider();

  @override
  Role<RoutesData> get role => routerRole;

  @override
  RoleOutput render(RoleHookInput<RoutesData> input) {
    final facade = routerRole.facadeOf(input);
    final routes = GoRoutes.of(
      facade,
      start: routerRole.startIn(input),
      mainNavigation: input.has(layoutRole),
    );
    return RoleOutput(
      fragments: [
        if (routes.hasMainNavigation)
          AppEntryRole.agentSections.entry(
            routerRole.description,
            AgentNote(mainNavigationAgentNote),
          )
        else if (input.has(layoutRole))
          AppEntryRole.agentSections.entry(
            routerRole.description,
            AgentNote(firstDestinationAgentNote),
          ),
      ],
      vars: {
        'initial_location': routes.initialLocation,
        'main_navigation': routes.hasMainNavigation,
        'routes': routes.routes,
        'main_navigation_route': routes.mainNavigation,
        'value_checks': routes.valueChecks,
        // Whether the modules of the app declare guards, which the router
        // then asks.
        'guards': facade.guards.isNotEmpty,
      },
    );
  }
}
```

- `render` gets the data of the role in the order the modules were chosen, with the module that contributed each, and the data of the roles its role requires or uses. `input.has(role)` tells whether such a role is present. The role offers typed views of its data, such as `routerRole.facadeOf(input)`, the routes with their full paths and names, which the provider shares with the facade, so both agree on every path and name.
- `RoleOutput.vars` are variables of the provider's bricks. A variable can be a `Fragment` of code with imports, such as the routes, which a template reads as `{{{routes}}}`, and the pipeline adds its imports to that file. See [Templates](https://doc.saymyframe.com/extending/templates.md#variables-of-render-hooks).
- `RoleOutput.fragments` put code into sockets, with the same access rules as the module's contributions. Here they are the notes of the module for coding agents that depend on the routes of the app; see [Notes for coding agents](https://doc.saymyframe.com/extending/infrastructure-module.md#notes-for-coding-agents).
- `RoleOutput.files` are text files of the provider, each by its path in the app. A brick has the same files in every app, so a file whose path depends on the data of the role, such as one file for each item that the modules give the role, comes from the hook. See [Files of render hooks](https://doc.saymyframe.com/extending/templates.md#files-of-render-hooks).
- `validate` checks the data against what the provider supports and returns problems. `LayoutProvider` does it for the number of destinations, and `bottom_tabs` sets `maxDestinations` to 5.
- `manualSteps` returns the steps of the setup that the developer of the app does by hand and that depend on the data, such as those that `firebase_auth` lists for the ways to sign in of Apple and Google. See [Steps by hand](https://doc.saymyframe.com/extending/infrastructure-module.md#steps-that-depend-on-the-data-of-a-role).

The documentation of each role on pub.dev says what it expects of its providers, meaning the files and symbols they generate and the behavior they promise:

- the app entry, [`AppEntryRole`](https://pub.dev/documentation/smf_contracts/latest/smf_contracts/AppEntryRole-class.html): `main()` in `lib/main.dart`, which initializes the binding, awaits `bootstrap()` and runs the root widget inside the root wrappers; `bootstrap()` in `lib/bootstrap.dart`, with the phases of start-up in their order and without a design library; the root of the app as a `MaterialApp`, created in the `build(BuildContext context)` of a widget, so that the arguments that modules give the root read the context below the wrappers; the fallback start screen, only in an app that no route starts (`RouterRole.noStartRoute`); and the native projects, with the tags of the native sockets and `ios/Runner/Runner.entitlements`, which every build configuration of the app target signs with;
- the router, [`RouterRole`](https://pub.dev/documentation/smf_contracts/latest/smf_contracts/RouterRole-class.html): `createAppRouter()` in `lib/core/router/app_router_factory.dart`, routes named by their full names, the start route at `/`, or the fallback start screen of the app entry there, imported only where no route starts the app, the main navigation around the layout's `AppShell`, the `StateError` of `push()` and `replace()` with a location of the main navigation on a page over it, a `config` created once, the back button of the system, on its error screen too, `push()` and `replace()` on that screen, observers for every navigator, the listeners of the screen, each called on its own, and the [guards](https://doc.saymyframe.com/extending/router-guards.md) of an app that has them;
- the layout, [`LayoutRole`](https://pub.dev/documentation/smf_contracts/latest/smf_contracts/LayoutRole-class.html): `AppShell(destinations:, currentIndex:, onSelect:, body:)` in `lib/core/layout/app_shell.dart`, with `destinations`, `currentIndex` and `onSelect` as public fields or getters;
- dependency injection, [`DiRole`](https://pub.dev/documentation/smf_contracts/latest/smf_contracts/DiRole-class.html): `createServiceLocator()`, `registerDependencies()` and `resetDependencies()` in `lib/core/di/dependencies.dart`, the registrations in the order of `DiGraph.ordered`, a call or a tear-off of the factory of every registration and of its dispose function, and the capabilities of the container;
- the settings screen, [`SettingsScreenRole`](https://pub.dev/documentation/smf_contracts/latest/smf_contracts/SettingsScreenRole-class.html): a route of the provider that shows [the screen](https://doc.saymyframe.com/extending/provide-settings-screen.md), with every entry of the modules once, in the order of the role;
- localization, [`LocalizationRole`](https://pub.dev/documentation/smf_contracts/latest/smf_contracts/LocalizationRole-class.html): the extension `AppTexts` on `BuildContext` in `lib/core/l10n/l10n.dart`, with a getter for [each text](https://doc.saymyframe.com/extending/provide-localization.md) of the app in each of its languages;
- the theme, [`ThemeRole`](https://pub.dev/documentation/smf_contracts/latest/smf_contracts/ThemeRole-class.html): `createLightTheme(context)` and `createDarkTheme(context)` in `lib/core/theme/app_theme.dart`, the [look](https://doc.saymyframe.com/extending/provide-theme.md) of the app;
- the onboarding screen, [`OnboardingScreenRole`](https://pub.dev/documentation/smf_contracts/latest/smf_contracts/OnboardingScreenRole-class.html): a route of the provider that shows the onboarding, named with an `OnboardingScreenRoute`, and a file of the provider that reads `appOnboardingSteps` of `lib/core/onboarding/onboarding_steps.dart` and shows each step that its `showIf` keeps, on a page of its own, in the order of the list; see [Add a step to the onboarding](https://doc.saymyframe.com/extending/onboarding-steps.md#what-the-onboarding-does-with-a-step);
- sign-in, [`AuthRole`](https://pub.dev/documentation/smf_contracts/latest/smf_contracts/AuthRole-class.html): one implementation of `AuthService` of `lib/core/auth/auth_service.dart`, the same in every mode of the app, whose calls fail only with an `AuthFailure`; see [Provide sign-in](https://doc.saymyframe.com/extending/provide-auth.md);
- notifications, [`NotificationsRole`](https://pub.dev/documentation/smf_contracts/latest/smf_contracts/NotificationsRole-class.html): one implementation of `NotificationsService` of `lib/core/notifications/notifications_service.dart`, whose function asks the user nothing, that shows every notification in the channel of the app, schedules at about a time, also after a restart of the device, and opens the settings of the system on a request where the platform has no question; see [Notifications and push messages](https://doc.saymyframe.com/guides/notifications.md#for-a-provider);
- push messages, [`PushRole`](https://pub.dev/documentation/smf_contracts/latest/smf_contracts/PushRole-class.html): one implementation of `PushService` of `lib/core/push/push_service.dart`, whose `address` keeps from one launch to the next and never throws, which never asks for the permission to show notifications, and which tells whether the platform shows a message in the foreground; see [Notifications and push messages](https://doc.saymyframe.com/guides/notifications.md#for-a-provider);
- purchases, [`PurchasesRole`](https://pub.dev/documentation/smf_contracts/latest/smf_contracts/PurchasesRole-class.html): one implementation of `PurchasesService` of `lib/core/purchases/purchases_service.dart`, whose function asks the user nothing and never throws, whose calls fail only with a `PurchasesFailure`, and which keeps the purchases of each user apart; see [Purchases](https://doc.saymyframe.com/guides/purchases.md#for-a-provider);
- the values in `.env`, [`EnvRole`](https://pub.dev/documentation/smf_contracts/latest/smf_contracts/EnvRole-class.html): `AppEnv` in `lib/core/env/app_env.dart`, with a getter for every key that returns the value of `.env`, or `null` for an empty or missing one, before any code of the app runs; see [Values in .env](https://doc.saymyframe.com/guides/env.md#for-a-provider).
- the REST client, [`RestRole`](https://pub.dev/documentation/smf_contracts/latest/smf_contracts/RestRole-class.html): [one implementation](#rest-api) of `RestTransport` of `lib/core/rest/rest_transport.dart`;
- CI, [`CiRole`](https://pub.dev/documentation/smf_contracts/latest/smf_contracts/CiRole-class.html): a file in [the format of its host](#ci) with every command of `ciRole.pipelineIn`;
- the app icon and the splash screen, [`AppIconRole`](https://pub.dev/documentation/smf_contracts/latest/smf_contracts/AppIconRole-class.html) and [`SplashRole`](https://pub.dev/documentation/smf_contracts/latest/smf_contracts/SplashRole-class.html): [two images](#app-icon-and-splash-screen) in `assets/branding/` for each, and a step that makes the result.

When the user presses the back button of the system, a router closes the route on top. It tries `NavigatorState.maybePop` on the navigators that the user sees, the innermost first: a dialog closes before the page below it, and a page of the selected branch of the main navigation before the main navigation, unless a route of the root navigator, such as a dialog, covers the main navigation. With no route to close, the router leaves the button to the system, which closes the app. A press that closes a route is one that the router handled. Otherwise the system takes the button too, and closes the app together with the route.

The error screen of a router is a page like any other for the button. A dialog over it closes, and the button throws nothing there. The screen itself closes only if a page is below it, which is up to the provider. With none, the router has no route to close and leaves the button to the system.

On its error screen, a router still shows the location that `push()` or `replace()` asks for, and the listeners of the screen hear of it once. `replace()` shows it in place of that screen. `push()` shows it over that screen and completes with the value of its page. A provider that has no page there to push over shows it in place of that screen, as `go()` does, and completes the push with `null` at once.

The layout role generates `lib/core/layout/destination.dart` with the class `Destination` and with `appDestinations`, the destinations of the app in the order of the features. The label of a `Destination` is a function of a `BuildContext`, which returns the text of its module in the language of the app, or its English text. So the two providers share the work like this:

- A router gives the shell `LayoutRole.appDestinations` as its `destinations` and has a branch for each, in the same order. It renders neither a label nor an icon. The contract harness checks that, in an app with a layout and destinations, the files of the router read `appDestinations` (the rule `router.destinations_shown`).
- A layout shows a label with `destination.label(context)` in the `build` of the widget that shows it, as a text, in a tooltip or to the semantics, and keeps nothing of what it returned. The widget then builds again with the new label when the language changes.

A layout keeps `destinations`, `currentIndex` and `onSelect` of its `AppShell` public because code that knows only the role reads them, whichever layout provides it. A test of the main navigation does this: it finds a destination among `destinations` by its label and icon, selects it with `onSelect`, as the layout does when the user selects it, and reads the selected one from `currentIndex`. The `body` may stay private, since such code finds what the shell shows in the widget tree.

SMF's CI checks the router and the layout in running apps, with every provider of the two roles that SMF has. One test starts the app and goes with `go()` to each location that needs no values, up to the first 20. Each location must show the page of its route on top of the innermost navigator, named after the route, and the screen of the route, with no `ErrorWidget` and no error from Flutter. Other tests check the listeners of the screen, the observers, the back button, also on the error screen, `push()` and `replace()` from that screen, the `StateError` of `push()` and `replace()`, the `config` created once, the guards and the conditions of the routes, the switches between the destinations of the layout, and that the layout shows no label in a language that the app is no longer in. [Contributing](https://doc.saymyframe.com/contributing.md#the-roles-at-run-time) lists these tests.

## The app entry

A provider of the app entry owns the native projects, so it has the flavors of the role there. The flavors are constants, `AppEntryRole.flavors`, each an `AppFlavor`, and a module or a tool reads their names and ids from them (`applicationIdIn`, `bundleIdIn`, `xcodeConfigurations`). `AppEntryRole.defaultFlavor` is what `default-flavor` in the pubspec names, and `AppEntryRole.releaseFlavor` has the id of the app itself. The structural rule `app_entry.flavors` checks each native project that the app has:

| File | What it has |
| --- | --- |
| `pubspec.yaml` | `default-flavor`, which names a flavor of the role |
| `android/app/build.gradle.kts` | Exactly those flavors in `productFlavors`, each with a dimension and its application id |
| `ios/Runner.xcodeproj/project.pbxproj` | The build configurations of each flavor in every configuration list, by the `name` of the object. In the app target they have the bundle id of the flavor and a base, the same for the flavors of a mode. |
| The scheme of each flavor | It exists, takes those configurations in each action and runs the pre-action of Flutter |

The rule leaves two things to other tests. Only a run of the `flutter` tool shows which flavor is the default, which a test of the role checks in a running app. And a provider may write the name on the device in another form.

The template of the role writes the section "Flavors" of the README of the app, so the README of every provider has the tag of `AppEntryRole.readmeSections`. The provider writes a section of its own, `AppEntryRole.flavorProjectsHeading`, on where each flavor is in its native projects.

The module rule `app_entry.ios_deployment_target_tag` keeps a literal iOS version out of the template of `project.pbxproj`. Each build configuration of the project has the tag of `AppEntryRole.iosDeploymentTarget` as its `IPHONEOS_DEPLOYMENT_TARGET`, and one of a target has that tag or no such setting, so a version that a module raises reaches every configuration.

## CI

The CI role knows what runs and not where. `ciRole.pipelineIn` gives the commands as data, a `CiPipeline`, and a provider writes them in the format of its host, in a file of `RoleOutput.files` of its render hook, since the syntax of such a format may read like a tag of a brick. Each command runs in a POSIX shell at the root of a fresh copy of the repository, on a machine with the Flutter SDK and a JDK, which the provider sets up:

| In `CiPipeline` | What the provider does with it |
| --- | --- |
| The checks | Runs them on each push |
| `CiBuild.environments` | Offers them to a developer, who picks one when a build starts, the first by default |
| `CiBuild.keys` | Passes each to `CiBuild.prepare` as a variable of the process. A value reaches a command only that way, never in the text of a script. |
| `CiBuild.prepare`, `CiBuild.command` | Runs them in that order |
| `CiBuild.artifact` | Keeps the file |

The structural rule `ci.pipeline_rendered` reads the files of the provider other than Dart files as lines of text, without those that start with `#` or `//`. It looks for each command at the end of a line, and for each environment and each key as a whole word. In the place of the environment, the command and the file each have a reference of the host, or every environment. It sees presence and not structure: a command in the wrong job, or a job that never runs, passes it. So a provider keeps a test that parses its format, as `test/workflow_test.dart` of `smf_github_actions` does, and shows the file in a real run.

An app has at most one provider, since a repository lives on one host. The app does not read the role, so the role has no test in a running app. A provider tells in a section of its own in the README of the app how to start a build.

## App icon and splash screen

The app icon role and the splash role have no sockets and no Dart symbols, since no module gives them data and no code of the app reads them. Each holds the paths of two images, which every provider generates and the app does not bundle: `AppIconRole.imageFile` and `foregroundFile`, and `SplashRole.lightImageFile` and `darkImageFile`, all in `assets/branding/`. What the images show, the colours around them and the tool that makes the result belong to the provider. One module may provide both roles.

The native projects are files of the provider of the app entry, and two bricks cannot generate one file, so a provider makes the icon or the screen in a `PostGenStep`. The rules `app_icon.step` and `splash.step` report a provider with no step that a run without a terminal runs too, which is a step that is neither external nor interactive, has no notice and continues no other step. Mark it `skippable`, so that a tool that fails costs a warning and not the app:

```dart
const PostGenStep(
  ToolRef('dart'),
  [updateScript],
  description: 'Making the app icon from assets/branding',
  skippable: true,
),
```

The template of each role writes its section of the README of the app and its note. The section and the note of the provider tell what the images show and give the command that makes the result again after a change. What reached the native projects shows only once the step has run, so neither the rules nor a snapshot see it: the tests of the two roles read the files of the app as `smf create` left them.

## Roles with a page of their own

Four roles, and the guards of the router role, ask more of a provider than fits here:

- [Ask the guards in a router](https://doc.saymyframe.com/extending/router-guards.md): what a provider of the router role calls in an app with guards, and what it does with each answer.
- [Provide the settings screen](https://doc.saymyframe.com/extending/provide-settings-screen.md): the route of the screen, and the entries that the modules give it.
- [Provide the localization](https://doc.saymyframe.com/extending/provide-localization.md): the texts of the app in its languages.
- [Provide the theme](https://doc.saymyframe.com/extending/provide-theme.md): the light and the dark theme, and what the role does with the mode.
- [Provide sign-in](https://doc.saymyframe.com/extending/provide-auth.md): the service of a provider of the auth role, and what the role does with the session and the mode.
