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>}}. Data for the role and code for its sockets apply only when the role is present, so they need no when.
  • 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.

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.
  • 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 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, 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.
  • 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 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.
  • 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, 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.