Skip to main content

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.
  • contribute depends only on the ModuleContext. What depends on another role goes into contributions with when, 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>}} or as the value of a variable for an app with the role (RoleVar). Data for the role and code for its sockets apply only when the role is present, so they need no when.
  • The contributions of a module are the same whatever a role chose. A module that is no provider reads the mode of a role, such as authMode of the auth role, from the constant that the role generates, when the app runs.
  • 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 in context.nav.<id>, it must not be a member of Object, such as hashCode.
  • Declare the id once, as a static const of the module class, and let other modules refer to that constant in dependsOn, so that the dependency is a pub dependency too.
  • dependsOn points 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 in FactoryRef.deps.
  • A layout provides the layout role, keeps its files in lib/core/layout/ and declares no routes. Its AppShell keeps destinations, currentIndex and onSelect as public fields or getters, which code that knows only the role reads. It shows the label of a destination with destination.label(context) in the build of the widget that shows it, and keeps nothing of what it returned.
  • A kind of your own lists the file where its modules may resolve services as a CompositionFile in its roleRules, and a module that resolves services there requires the DI role. The modules of a kind without one resolve no services.

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 as hashCode, or of a dart:core type written in lowercase, such as int, and no parameter is named key, path, parent, chain or routeName.
  • 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 const and 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 or show. A screen belongs to one route.
  • The label of a destination is a LocalizedText. A module that lists the localization role gives the role the same text, and a module that does not gives the label its English text alone.
  • A provider of the router gives the shell of the layout appDestinations of the layout role, and renders neither a label nor an icon of a destination.
  • A feature navigates only to its own routes and to those of the features it depends on.
  • A provider of the app entry builds the root of the app as a MaterialApp, or as a MaterialApp.router when the app has a router, so that every screen finds the theme and MaterialLocalizations above it. It creates the root in the build of a widget that main() runs inside the root wrappers, so that the arguments of the root can read the context below them. Its templates have the tags of appBuilder in the builder of the root in every app, since the app entry role puts a widget there itself, and smf create stops on a provider without them.

Guards​

  • A guard has a lowerCamelCase name of its own among the guards of its module. Its function is a top-level function of a file of the app that takes no arguments and returns a ValueListenable<bool>.
  • The value of a guard is known once bootstrap() has run, and it changes outside the build of a frame.
  • The target of a guard is a top-level route of its module that needs no values and is outside the main navigation. No route of its flow is a start candidate, and none asks for a condition.
  • A guard has the stage that its flow belongs to: GuardStage.welcome when the flow needs to know nothing of the user, and GuardStage.identity when it asks or needs to know who the user is. A guard that needs to know who the user is without asking it itself, as that of a subscription, allows while it cannot tell: the app may ask it before the guard that asks.
  • A guard whose next user should not come back to where the last one was, as after a sign-out, sets resumes: false.
  • A guard without a condition is a gate over the whole app. A guard with a condition keeps the user only from the routes that ask for it. Its module requires or provides the role of the condition and declares one guard for it at most.
  • Two guards with one flow, such as a gate and a guard of a condition that show the same target, read one notifier.
  • The routes of a flow show only while a guard with that flow does not allow. No code navigates into the flow of a gate: its module changes what the guard reads, and the router shows the target.
  • A screen neither closes itself nor navigates after it changed what a guard reads. The router has done both by then.
  • A route asks only for conditions of the roles that its module requires, uses or provides. Such a route is outside the main navigation and the flows of the guards of the module, and neither it nor a route below it is a start candidate.
  • A provider of the router asks the guards of an app that has them through the GuardedNavigation of the role, with the routes of the pages that the location is asked for on top of, and tells it of its pages when guardChanges notifies. It does what the class answers, ShowInstead, ShowOver, ShowNothing or ClosePages, and keeps the request that waits for a flow.
  • The answer depends on those pages. So a provider of the router never asks with no pages about a location that it shows over a page, nor about one that it goes to for a request that it asked about with the pages. A router whose redirect runs for its own navigation keeps those calls from the class.
  • A link to a page of the flow of a guard that stands for a condition opens that page over the screen that the app starts on. A screen opens such a page with push().

Sign-in​

  • The code of a module, and of the template of another role, reaches sign-in only through appSession. It calls neither createAuthService() nor initAuth() nor appSession.start(), and it does not import the file of AuthService.
  • The mode of the app gates nothing on its own. A module with the screens of sign-in declares the guards, with functions of its own that return appSession.allowsApp and appSession.hasAccount.
  • A route whose screen is only for the user of an account asks for the condition AuthRole.account, and its module lists the auth role.
  • A provider of the auth role contributes exactly one RoleImplementation of AuthService, whose function is in its file and takes no arguments. Of its files, only those of the class and of the function of that implementation import the file of AuthService: a widget of a provider signs in through appSession too.
  • Every call of the service of a provider fails only with an AuthFailure, and what the service keeps to is the same in every mode of the app.
  • A tool of the app that follows the mode, such as a script of a provider that dart runs, reads it from the text of the file of the session, with AuthRole.modeDeclaration. It stops when the file does not tell the mode.

Settings and preferences​

  • The preferences are for settings, not for secrets. A token, a password, an API key or an encryption key never goes there: a secret belongs in the secure storage of the platform.
  • A module gets the preferences through a restorer that it gives PreferencesRole.restorers, or, in an app with a DI container, as a dependency of the factory of a service. It never calls createAppPreferences() or initPreferences().
  • A restorer is synchronous and awaits nothing. It keeps the current value when nothing is saved under its key, keeps the preferences for the writes of its module, and may run more than once.
  • A key is <module id>.<setting>.
  • Widgets read and change a setting through the state of its module, never through the preferences.
  • The widget of a settings entry is a class in a file of its contributor, with a const unnamed constructor that requires no arguments. A module that only uses the settings screen role generates that file in a brick with when: {settingsScreenRole}.
  • A provider of the settings screen names one route of its own that needs no values as the screen. The screen shows every entry once, in the order of the role, and the provider creates the widget of each through an import of its file with a prefix of its own.
  • A provider of the theme generates createLightTheme(context) and createDarkTheme(context), and keeps no theme mode of its own. In an app with the theme role, no module sets theme, darkTheme or themeMode of the root.

Texts​

  • A text that a user sees is a LocalizedText that its module gives the localization role, and a template reads it through a variable of localizationRole.varsOf.
  • The name of a text is a lowerCamelCase identifier of its own among the texts of the module. A text has an English text, translations by the code of their language, and no braces, and neither the English text nor a translation is blank.
  • A text in the data of another role, such as the label of a destination, is a text of the module too. Two such texts of one name are the same text: the app reads a text of a module by its name.
  • A template reads a text variable in three braces, outside mustache sections, where a BuildContext context below the root of the app is in scope, and not inside a const expression.
  • Code reads only the texts of its module and of the modules it depends on. The template of a role reads its own texts, and the texts in the data that the modules give its role or a role that it requires or uses, such as the labels of the destinations.
  • A module brings no delegate for texts of its own and adds no language to the root of the app.
  • A provider of the localization role renders a getter for every text of the app, in the languages of the app, with the English text where a translation is missing.

Notes for coding agents​

  • A note tells what the code of the app does not show. A module that provides a role adds its note under the description of the role, and a module without a role under a heading of its own.
  • A note is true in every app that has it. It names a file of the app by its path from the root of the app, and only a file that the app has. What it tells of a file that only some apps have is a contribution of its own, with the role of that file in its when.
  • Only the template of a role contributes AgentNote.ofRole, and such a note names no provider and no package. A provider that wraps a package or a tool opens its note with a line that names it, such as "With go_router:".

Services​

  • A registration's factory is a top-level function of its file that takes the services in deps. A dispose function takes the service.
  • A provider of the DI role renders every registration: its files call or tear off the factory of each registration and its dispose function. The contract harness checks this in the apps of every provider.
  • 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. The auth role registers nothing in the container: code reaches sign-in through appSession.

Packages​

  • The code in lib/ imports only the module model of smf_contracts, its own files and the public libraries of its dependencies. It does not import dart:io, dart:isolate, dart:ffi or dart:mirrors.
  • The package of a provider of a role, such as flutter_riverpod of riverpod, goes into another module only through its variant for that provider, with the constraint any, 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 or in a file that its render hook generates, 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 as collection, which Flutter pins, with any. See The packages of a provider.
  • Declare PubspecContribution.environment only 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.
  • A variable with a RoleVar holds code for an app with a role and code for one without it. A template reads it in three braces, outside mustache sections.
  • A file that a render hook generates has a path inside the app that every machine can write, and no brick or other hook generates the same file.
  • Code that goes into lib/bootstrap.dart, through the phases of start-up or topLevel, imports neither material nor cupertino.

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 may need a tool that a check of a module that its module depends on installs, since those checks run first. It counts on no check of any other module.
  • A check is required only 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 skippable when the app is complete without it, interactive when it talks to the user, external when it needs something outside the app, and it needs the checks without which it would fail.
  • A step that does more than its description says tells the user in its notice, one line that reads on its own. Such a step is skippable, since the user may decline it.
  • A module continues, with followUpOf, only its own steps and those of the modules it depends on directly, which publish the ids of their steps as constants of their module classes. A step that continues another has no when of its own. The id of a step names the module that contributes it, and no other step of the app has that id.
  • On Windows, an argument of a batch file such as dart.bat has no ^, <, >, %, &, | or ".

Tests​

  • The tests of the module run the contract harness over a registry with flutter_core, the modules it depends on, every provider of a role it requires or uses that it should be checked with, and every provider it has a variant for. uncheckedProviders() of the harness is empty.
  • 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 what the module gives a role through the role, in result.hook and result.app!.socketOrders, and not in the files that the provider of another role renders. That holds for its routes, its services, its settings entries and its texts alike.
  • The tests check the package with ModulePackage, and with roleClassNameProblems that its code takes the names of the classes of roles from the roles.

See Test a module.