Skip to main content

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 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:

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.

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.

RuleChecked 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>}}. 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 imports only the files its module may use: its own, those of the modules it depends on directly, and those of its roles. The template and the providers of a role may also import the files of the modules that give the role data, such as the screens that a router routes. 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 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.