# Module independence

SMF rests on one principle: modules do not know each other. Roles, sockets, variants and the checks of the pipeline all exist to keep it that way.

## What a module knows

A module knows three things:

- the modules in its `dependsOn`, in one direction only, so `firebase_analytics` depends on `firebase_core` and `firebase_core` knows nothing about it;
- the roles it provides, requires or uses, through their sockets, their symbols and their data;
- the app it is part of, meaning its name, its organization, its platforms and its identifiers.

It never learns which modules the user picked. It never refers to what another module generates, such as a class, a file or a package, unless it depends on that module.

## What that looks like

`firebase_analytics` logs a screen view whenever the screen changes, without knowing go_router. It uses the router role and gives the role 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},
),
```

In the module's template, the listener exists only inside `{{#has_router}}`, the presence flag of the router role. An app without a router gets neither the contribution nor the listener.

A feature does not know get_it either. Its composition file calls `resolve<T>()` of the DI role and hands the services to the feature's state, and `get_it`, or any other provider of the role, makes that call work. The feature's screens know even less. They talk only to their state, a Cubit through `context.read` or a provider through `ref`, and never to the container.

```mermaid
flowchart TB
    screen["Screen widget"] -- "context.read or ref" --> state["Cubit or provider"]
    composition["Composition file of the feature"] -- "services" --> state
    composition -- "resolve&lt;T&gt;()" --> locator(["DI role: ServiceLocator"])
    getit["get_it"] -. provides .-> locator
    screen -. "never" .-x getit
```

A setting works the same way. The module that owns a setting gives the settings screen role the widget of its row, and whichever module provides the screen shows it. Neither names the other, so the screen has the rows of exactly the modules that the app has. The setting is saved through the preferences role, and its label is translated through the localization role. The module names neither of the modules that provide them; see [How settings work](https://doc.saymyframe.com/concepts/settings.md).

A screen that needs an account works the same way. Its feature asks for the condition `AuthRole.account` on the route, and whichever module has the screens of sign-in declares the guard that stands for it, among the built-in modules `sign_in`. The two know the auth role and not each other; see [Conditions of routes](https://doc.saymyframe.com/concepts/conditions.md).

`sign_in` itself knows neither the module that keeps the accounts nor the router. It signs in through `appSession` of the auth role, and its screens neither close themselves nor navigate after a sign-in: two guards read the session, and the router shows the sign-in and leaves it.

## What the model enforces

SMF checks these rules in code. `smf create` checks the contributions of the modules before it generates an app, and the contract harness runs the same checks in the tests of every module, together with checks of the generated code.

| Rule | Checked by |
| --- | --- |
| A module puts code only into the sockets of its own roles, of the app entry and of the modules it depends on, and contributes data only to its own roles. The `when` of a contribution names only roles of the module. | `smf create` and the harness |
| A feature keeps its files in `lib/features/<id>/`, and infrastructure declares no routes and has no variants. | `smf create` and the harness |
| The package of a role's provider, such as `flutter_riverpod` of `riverpod`, goes into another module only through its variant for the provider or through a dependency on the provider. | `smf create` and the harness |
| Every file of the app has exactly one owner. | `smf create` and the harness |
| Code that refers to the symbols of a role that the module only uses exists only in apps with the role: a contribution with that code names the role in `when`, and a template puts the code inside `{{#has_<role>}}` or reads it from a variable whose code depends on the role. Data for the role and code for its sockets need neither, because they apply only when the role is present. | the harness, in its app without the role |
| Only the composition file of a feature resolves services. Other code gets them as parameters of its factory function, and only the DI container calls the functions that create them. | the harness |
| Code of a module navigates only to its own routes and to those of the modules it depends on. | the harness |
| Code of a module reads only its own texts and those of the modules it depends on. | the harness |
| A module gets the preferences through a function that it gives the preferences role, or from the DI container. Only the DI container calls `createAppPreferences()`, and only `bootstrap()` calls `initPreferences()`. | the harness |
| Code of a module signs in only through `appSession` of the auth role. It does not call `createAuthService()`, and only `bootstrap()` calls `initAuth()`. | the harness |
| The provider of a role renders what the modules give the role: every registration of a service, every entry of the settings screen and every text. So the tests of a module need not look into the files of a provider. | the harness |
| Code imports only the files its module may use: its own, those of the modules it depends on directly, and those of its roles. What a render hook of a role gives may also import the files of the modules that give the role data, such as the screens that a router routes: a fragment of the hook, or a file that it generates. The template of a brick may not, since it is the same whatever the modules give the role. An import that the pipeline adds for a fragment counts for the module of the fragment. | the harness |
| A module that imports or exports the package of a role's provider adds the package itself, even when it depends on the provider. | the harness |

## What it buys

Any combination of modules works. You can add a module, leave one out or pick another provider of a role, and the app still compiles. Continuous integration generates apps from the modules in many combinations, each module with each provider of the roles it requires or uses and every module together, and runs `flutter analyze` on each of them.

Providers are replaceable. A second router or DI container can join without a change to the features, because the features never named the first one.

Modules can come from anyone. A module by another author follows the same rules and goes through the same checks. See [Extending SMF](https://doc.saymyframe.com/extending.md).
