Skip to main content

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:

FieldWhat it means
idThe id of the module, lower snake_case, as in -m home.
descriptionWhat the module adds, as the questions of smf create show it.
kindThe kind of the module, which decides the rules it follows.
providersThe roles the module provides, one provider object per role.
requiresRoles the module needs. The app gets a provider of each.
usesRoles the module works with when they are present, and works without.
dependsOnModules the module builds on directly, such as firebase_core for the Firebase modules.
variantsAlternative contributions for the providers of one role, such as a feature with a BLoC and a Riverpod variant.
sockets, socketFamiliesSockets 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.

KindModulesRules
scaffoldflutter_coreProvides the app entry role: main(), start-up, native projects, pubspec.yaml.
infrastructurego_router, get_it, bloc, riverpod, event_bus, the Firebase modulesSets 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.
featurehomeA 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.
layoutbottom_tabsProvides 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:

PartMeaning
cardinalityExactly one provider in every app, at most one, or any number.
requires, usesOther roles, whose data and presence its hooks can see.
socketsNamed places in its files where other modules put code.
interfaceThe files its own template generates, and the symbols every provider must generate, such as createAppRouter() for the router.
data typeThe type in which modules describe what they need from it, such as the routes of a feature.
optionsOptions of the command line, such as --start of the router.
template, rulesHooks 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:

RoleIdProviders in an appRequiresUsesData from modules
App entryapp_entryexactly onenone
Routerrouterat most oneLayoutRoutesData: routes
Layoutlayoutat most oneRouternone
State managementstate_managementat most onenone
Dependency injectiondiat most oneDiRegistration: services
Eventseventsat most oneDependency injectionRoleImplementation
Analyticsanalyticsany numberDependency injection, RouterRoleImplementation
Crash reportingcrash_reportingany numberDependency injectionRoleImplementation

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​

RelationDirectionWhat it gives
providesmodule → roleThe module implements the role.
requiresmodule → roleThe app gets a provider of the role, and the module may use its sockets and symbols.
usesmodule → roleThe 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 onmodule → moduleThe 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.
variantsmodule → providers of one roleThe 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​