The module model
SMF builds an app from modules. To get something done, like routing or dependency injection, a module names a role ("a router", "a DI container"), and whichever module the user picked provides that role. It names another module only when it builds on it, as the Firebase modules build on firebase_core, or when it has a variant for one provider of a role. The sections below describe the pieces, and module independence explains why the split matters.
Rectangles are modules, and rounded boxes are roles. home and bottom_tabs need a router, and go_router provides it. The analytics role works with a router when the app has one. firebase_analytics depends on firebase_core directly, because it builds on Firebase itself rather than on a role.
Modules
A module is a Dart class that extends SmfModule. It describes itself with a descriptor and returns its contributions to an app, such as files, code for sockets, data for roles and dependencies:
final class HomeModule extends SmfModule {
const HomeModule();
static const id = ModuleId('home');
ModuleDescriptor get descriptor => const ModuleDescriptor(
id: id,
description: 'Start screen with the name of the app',
kind: ModuleKinds.feature,
);
List<Contribution> contribute(ModuleContext context) => [
BrickContribution(homeBundle),
routerRole.data(const RoutesData([/* the route of the screen */])),
];
}
contribute gets a ModuleContext with the name of the app, its organization and its platform identifiers. It says nothing about the other modules of the app. Code that depends on other roles goes into contributions that apply only when those roles are present.
The descriptor says:
| Field | What it means |
|---|---|
id | The id of the module, lower snake_case, as in -m home. |
description | What the module adds, as the questions of smf create show it. |
kind | The kind of the module, which decides the rules it follows. |
providers | The roles the module provides, one provider object per role. |
requires | Roles the module needs. The app gets a provider of each. |
uses | Roles the module works with when they are present, and works without. |
dependsOn | Modules the module builds on directly, such as firebase_core for the Firebase modules. |
variants | Alternative contributions for the providers of one role, such as a feature with a BLoC and a Riverpod variant. |
sockets, socketFamilies | Sockets of the module itself, for the modules that depend on it. |
Kinds
The kind of a module states, as data, the rules its modules follow. The pipeline applies them without knowing any kind.
| Kind | Modules | Rules |
|---|---|---|
| scaffold | flutter_core | Provides the app entry role: main(), start-up, native projects, pubspec.yaml. |
| infrastructure | go_router, get_it, bloc, riverpod, event_bus, the Firebase modules | Sets up a service or a library. Declares no routes, generates nothing in lib/features/, has no variants, and never resolves services itself: it gets them through the factories of its DI registrations. |
| feature | home | A module with screens. Requires the router role, declares at least one route, and keeps its files in lib/features/<id>/. Its optional composition file, lib/features/<id>/<id>_composition.dart, may resolve services; a feature that does requires the DI role. |
| layout | bottom_tabs | Provides the layout role, the main navigation. Keeps its files in lib/core/layout/ and declares no routes. |
In the questions of smf create, the modules that provide no role are grouped by kind: Features, Infrastructure.
Roles
A role is a replaceable part of an app, described by what it offers to the rest of the app. It has:
| Part | Meaning |
|---|---|
| cardinality | Exactly one provider in every app, at most one, or any number. |
| requires, uses | Other roles, whose data and presence its hooks can see. |
| sockets | Named places in its files where other modules put code. |
| interface | The files its own template generates, and the symbols every provider must generate, such as createAppRouter() for the router. |
| data type | The type in which modules describe what they need from it, such as the routes of a feature. |
| options | Options of the command line, such as --start of the router. |
| template, rules | Hooks that run during generation, and checks of the modules and of the generated code. |
A role is defined once, as a constant, in smf_contracts or in any other package. These are the built-in roles:
| Role | Id | Providers in an app | Requires | Uses | Data from modules |
|---|---|---|---|---|---|
| App entry | app_entry | exactly one | none | ||
| Router | router | at most one | Layout | RoutesData: routes | |
| Layout | layout | at most one | Router | none | |
| State management | state_management | at most one | none | ||
| Dependency injection | di | at most one | DiRegistration: services | ||
| Events | events | at most one | Dependency injection | RoleImplementation | |
| Analytics | analytics | any number | Dependency injection, Router | RoleImplementation | |
| Crash reporting | crash_reporting | any number | Dependency injection | RoleImplementation |
The app entry is open to every module, so any module may add start-up code or native settings without declaring the role.
Role templates
Much of a role's code does not depend on its provider, so the role generates it itself, through its template. The router role generates the navigation facade from the routes of all features, the DI role generates the ServiceLocator interface, and the analytics role generates the AnalyticsService interface with a service that forwards every call to all providers. The pipeline treats the template like a module named role:<id>, which owns the files it generates.
Providers
A module provides a role with a provider object in its descriptor. A provider may check the role's data against what it supports, as bottom_tabs does with its limit of five destinations. It may also render the data into its own files: go_router renders the routes into a GoRouter, and get_it renders the registrations into get_it calls. A module whose files and dependencies alone implement the role, such as bloc, uses RoleProvider.plain.
A provider requires and uses what its role requires and uses, so bottom_tabs requires a router because the layout role does.
How modules relate
| Relation | Direction | What it gives |
|---|---|---|
| provides | module → role | The module implements the role. |
| requires | module → role | The app gets a provider of the role, and the module may use its sockets and symbols. |
| uses | module → role | The module works with the role when it is present. Its code that refers to the role's symbols goes into contributions that name the role in when, or inside {{#has_<role>}} in its templates. Its data for the role and its code for the role's sockets apply only when the role is present anyway. |
| depends on | module → module | The module builds on another module directly, and that module comes into the app with it. It may put code into that module's sockets. The id comes from the other module's package, so the dependency is a pub dependency too. |
| variants | module → providers of one role | The module has different contributions for each provider, keyed by the provider's id. It requires the role. |
Every relation points one way. firebase_analytics depends on firebase_core, but firebase_core knows nothing about it. Whenever a role describes what a module needs, the module requires the role rather than depending on the module that provides it.
Variants
A module whose code depends on the provider of a role declares variants, one per provider, keyed by the provider's id. A feature with state has a variant for bloc and one for riverpod, and the pipeline adds the contributions of the variant for the provider that the app has. A variant brings the package of its provider, such as flutter_bloc, with the constraint any, so the provider keeps control of the version.
A variant only adds contributions and never changes the descriptor. When the app has a provider that the module has no variant for, the module cannot be part of the app.
Next
- Sockets and contributions shows how modules put code into the app.
- The generation pipeline shows how the pipeline turns modules into an app.