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>}}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 nowhen. - 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
authModeof 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 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.
selectedByDefaultis for a module with no role that a new app should have, such as a start screen. A run in a terminal offers it selected among the modules of its kind. It says nothing for a module that provides a role, where Enter takes the first provider, and a run without a terminal never takes a module that it was not given.- 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. ItsAppShellkeepsdestinations,currentIndexandonSelectas public fields or getters, which code that knows only the role reads. It shows the label of a destination withdestination.label(context)in thebuildof 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
CompositionFilein itsroleRules, 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 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. - 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
appDestinationsof 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 aMaterialApp.routerwhen the app has a router, so that every screen finds the theme andMaterialLocalizationsabove it. It creates the root in thebuildof a widget thatmain()runs inside the root wrappers, so that the arguments of the root can read the context below them. Its templates have the tags ofappBuilderin thebuilderof the root in every app, since the app entry role puts a widget there itself, andsmf createstops 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.welcomewhen the flow needs to know nothing of the user, andGuardStage.identitywhen 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
GuardedNavigationof the role, with the routes of the pages that the location is asked for on top of, and tells it of its pages whenguardChangesnotifies. It does what the class answers,ShowInstead,ShowOver,ShowNothingorClosePages, 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 neithercreateAuthService()norinitAuth()norappSession.start(), and it does not import the file ofAuthService. - 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.allowsAppandappSession.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
AuthImplementationofAuthService, 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 ofAuthService: a widget of a provider signs in throughappSessiontoo. - 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
dartruns, reads it from the text of the file of the session, withAuthRole.modeDeclaration. It stops when the file does not tell the mode. - A way to sign in comes from a module that requires or provides the auth role, as a
SignInMethodwith a label, platforms, an issuer of an https URL, and an icon and a function in files of the module. Each issuer is the way to sign in of one module. The function returns aFutureof a credential that may benull, takes no arguments and does not useappSession. The icon is a class with aconstunnamed constructor without arguments. - A provider of the auth role reports in
validatea way to sign in whose identity provider its backend does not take, and its service fails such a credential withnotConfigured.
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 callscreateAppPreferences()orinitPreferences(). - 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
constunnamed constructor that requires no arguments. A module that only uses the settings screen role generates that file in a brick withwhen: {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)andcreateDarkTheme(context), and keeps no theme mode of its own. In an app with the theme role, no module setstheme,darkThemeorthemeModeof the root.
Onboarding, notifications and push messages
- A step of the onboarding comes from a module that uses the onboarding screen role, as an
OnboardingStepwith a lowerCamelCase name of its own among the steps of the module and an icon. ItsrunandshowIfare top-level functions of files of the module:runtakes theBuildContextof the onboarding screen and returns aFuture, andshowIftakes no arguments and returns aboolat once, without side effects. See Add a step to the onboarding. - A provider of the onboarding screen names one route of its own that needs no values as the screen, and a file of it reads
appOnboardingSteps. - Code reaches notifications only through
appNotifications, and push messages only throughappPush. It calls neithercreateNotificationsService()norcreatePushService(), norinitNotifications(),initPush()or thestart()of either. - A notification of the app has an id from 0 up. A notification with an owner names it under
notificationOwnerKeyand has a negative id. - A provider of the push role never asks for the permission to show notifications.
Purchases and values in .env
- Code reaches the purchases only through
appPurchases. It calls neithercreatePurchasesService()norinitPurchases()norappPurchases.start(), and only the files of the implementation of the provider import the file ofPurchasesService(purchases.factory_calls,purchases.start_calls). - A provider of the purchases role contributes exactly one
RoleImplementationofPurchasesService, whose function is in its file and takes no arguments (purchases.implementation_factories). Every call of its service fails only with aPurchasesFailure, and without what it needs, such as a key, withnotConfiguredand a hint. - A route whose screen is only for a user with premium access asks for the condition
PurchasesRole.premium, and its module lists the purchases role. A module that requires the role stands for the condition with a guard of the stageidentitywhose function returnsappPurchases.hasPremium. - A key of
.envis anEnvKeyof the module whose code reads it, or of the template of a role for a value that the template reads. Its name is in UPPER_SNAKE_CASE, of two words or more, starts with the id of the module or the role in upper case and an underscore, and never starts withFLUTTER_. Its description is one line of at most 100 characters without a backslash. Each key has a getter of its own (env.keys). - The files of a module read only its own keys and those of the modules it depends on, and the files of the template of a role read only the keys of that template (
env.key_access). A provider of the role declares a getter ofAppEnvfor every key (env.keys_rendered), and only a provider gives the role tool arguments, at most once. - No secret goes into
.env: every value ends up in the app, where anyone can read it.
REST API, CI, the app icon and the splash screen
- Code reaches the REST API only through
appRest. No module callscreateRestTransport(), and only the files of the implementation of the provider import the file ofRestTransport(rest.factory_calls). No module callsappRest.use(), which is for tests (rest.use_calls). - A provider of the REST role contributes one
RoleImplementationofRestTransport, created with the app, whose function is in its file and takes no arguments (rest.implementation_factories). - A provider of the CI role has, in its files other than Dart files and outside their comments, each command of the checks and of the preparation of a build at the end of a line, the command of the build and the file that it keeps, each with something that stands for the environment or once for every environment, and each environment and each key as a whole word (
ci.pipeline_rendered). A value reaches a command only as a variable of the process. - A provider of the app icon role or of the splash role generates the two images of the role and has a
PostGenStepthat makes the result and that a run without a terminal runs too: neither external nor interactive, with no notice, and continuing no other step (app_icon.step,splash.step).
Flavors
- A module reads the names and the ids of the flavors from
AppEntryRole.flavors, and writes none out. - A provider of the app entry has those flavors in each native project of the app, and
default-flavorin the pubspec names one of them (app_entry.flavors). - In the template of
ios/Runner.xcodeproj/project.pbxprojof a provider, each build configuration of the project has the tag of the minimum iOS version as itsIPHONEOS_DEPLOYMENT_TARGET, and one of a target has that tag or no such setting (app_entry.ios_deployment_target_tag). - The README of a provider has the tag of
AppEntryRole.readmeSections, since the template of the role writes the section on the flavors there. - A step that has to follow every run of a tool, for every flavor, is a Dart file of a directory that a script of the app runs, and not a
PostGenStep, which runs once. See Continuing a step.
Warnings
- A role reports a warning about what the app is made of with a code of its own,
<role id>.<name>, which it publishes as a constant. Achoosehook reports it withRoleChoiceContext.warn, never through the logger, and a structural rule reports no warning.
Texts
- A text that a user sees is a
LocalizedTextthat its module gives the localization role, and a template reads it through a variable oflocalizationRole.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 contextbelow the root of the app is in scope, and not inside aconstexpression. - 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 "Withgo_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, the notifications role, the push role and the purchases role register nothing in the container: code reaches them throughappSession,appNotifications,appPushandappPurchases.
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 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 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.
- A variable with a
RoleVarholds 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 ortopLevel, imports neithermaterialnorcupertino. - A brick with a condition on a choice,
whenChosen, holds no socket tag. - A component of the Android manifest is declared once, by one module. The Swift statements of
iosAppDelegateLaunchtake no imports and are indented by four spaces, and the components by eight.
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
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. - 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 isskippable, 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 nowhenof its own. Theidof 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.bathas no^,<,>,%,&,|or". - A step that only the developer of the app can do is a
ManualStepof the module, published as a constant of its module class, or of a role when contributors that do not know each other share it. Its id never changes, and its title is one short line. A step that depends on the data of a role comes from themanualStepshook, withoutwhen. An entitlement that only a paid Apple Developer team signs goes in withAppEntryRole.paidTeamEntitlement. See Steps by hand.
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.hookandresult.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 withroleClassNameProblemsthat its code takes the names of the classes of roles from the roles.
See Test a module.