Rules for modules
Use this checklist when you write or review a module. The pipeline or the contract harness checks most of these rules. The others keep modules independent, which is what lets any combination of them work.
Independence
- A module knows only the modules in its
dependsOn, and the roles it provides, requires or uses. It never learns which other modules the app has. - A module never refers to what another module generates, such as a class, a file or a package, unless it depends on that module. Prefer a role whenever one describes what you need.
contributedepends only on theModuleContext. What depends on another role goes into contributions withwhen, and what depends on the provider of a role goes into variants.- Code that refers to the symbols of a role the module only uses goes into contributions that name the role in
when, and into templates only inside{{#has_<role>}}. Data for the role and code for its sockets apply only when the role is present, so they need nowhen. - Generated widgets talk only to their state, a Cubit or a provider, and never to the DI container or another service directly.
The descriptor
- The id is lower snake_case, unique among the modules of a command, different from the id of every role, and not
pipeline. As a Dart name in lowerCamelCase, as incontext.nav.<id>, it must not be a member ofObject, such ashashCode. - Declare the id once, as a
static constof the module class, and let other modules refer to that constant independsOn, so that the dependency is a pub dependency too. dependsOnpoints one way. No module depends on itself, and the dependencies do not form a cycle.- A module has one provider object per role it provides, and does not also require or use a role it provides.
- A module of a kind that must provide a role, such as a layout, provides it.
- Variants are for a role that an app has at most one of, never for a role the module provides, and every key is the id of a module of the command that provides that role. Infrastructure has no variants.
Kinds
- A feature keeps its files in
lib/features/<id>/, declares at least one route, and resolves services only in its composition file,lib/features/<id>/<id>_composition.dart. A feature that resolves services requires the DI role. - Infrastructure declares no routes, generates nothing in
lib/features/, has no variants, and never resolves services. Its registrations list the services their factories take inFactoryRef.deps. - A layout provides the layout role, keeps its files in
lib/core/layout/and declares no routes.
Routes and screens
- Paths are lowercase segments or
:<name>parameters. A top-level path starts with/and is relative to the feature, and a child's path is relative to its parent. - Route and parameter names are lowerCamelCase. No route or parameter takes the name of a member of
Object, such ashashCode, or of adart:coretype written in lowercase, such asint, and no parameter is namedkey,path,parent,chainorrouteName. - Declare a route with a fixed segment before a route with a parameter in its place, so that the parameter does not take its locations.
- The unnamed constructor of every screen is
constand takes each parameter of its route as a named parameter of the same name. Its template has the tag of the screen's annotations before the class, and the tag of each parameter's annotations before the parameter. - Every screen is an UpperCamelCase class in a file of the app, which the route imports with
ImportRef.app, without a prefix orshow. A screen belongs to one route. - A feature navigates only to its own routes and to those of the features it depends on.
Services
- A registration's factory is a top-level function of its file that takes the services in
deps, then the parameters of the registration. A dispose function takes the service. - A provider of a service role contributes exactly one
RoleImplementation, whose function takes no arguments. Only the DI container calls the functions that create the services of roles.
Packages
- The code in
lib/imports only the module model ofsmf_contracts, its own files and the public libraries of its dependencies. It does not importdart:io,dart:isolate,dart:ffiordart:mirrors. - The package of a provider of a role, such as
flutter_riverpodofriverpod, goes into another module only through its variant for that provider, with the constraintany, or through a dependency on the provider. - A module that imports or exports the package of a provider adds that package itself, even when it depends on the provider. A provider of a role may import, for a fragment, the package that a module which gives the role data adds.
- A provider brings a package when it adds the package outside a variant with a constraint other than
any, and the package is then the provider's. A package that no provider brings is shared, and every module that needs it adds it. A provider brings only the packages that implement its role and adds a shared package, such ascollection, which Flutter pins, withany. See The packages of a provider. - Declare
PubspecContribution.environmentonly when your package needs a newer Dart or Flutter than the app has anyway. - Dev dependencies between module packages do not form a cycle.
Templates
- Socket tags go in three braces, outside mustache sections, and a socket that carries imports has its tag in one Dart file.
- Code and paths with slashes go in three braces, and no backslash comes right before a line break or a non-ASCII character.
- Bricks have no mason hooks, partials or changes of the delimiters, and every file has one owner.
- Code that goes into
lib/bootstrap.dart, through the phases of start-up ortopLevel, imports neithermaterialnorcupertino.
See Templates for the full list.
The machine and commands
- A preflight check only reads the machine. It never installs anything, logs in, asks the user or changes files other than its temporary files, and it never shows output that may hold a secret.
- A check is
requiredonly when the app cannot work without it. - A step after generation does not write the absolute path of its working directory into the app. It is
skippablewhen the app is complete without it,interactivewhen it talks to the user,externalwhen it needs something outside the app, and itneedsthe checks without which it would fail. - On Windows, an argument of a batch file such as
dart.bathas no^,<,>,%,&,|or".
Tests
- The tests of the module run the contract harness over a registry with
flutter_core, the modules it depends on, a provider of every role it requires or uses, and every provider it has a variant for. - When the module adds the package of a provider of a role, the registry has that provider too, and
checkAll()then checks an app of the module with it. - The tests check the package with
ModulePackage.
See Test a module.