Sockets and contributions
A module does not edit files. It returns contributions, plain data that says what it adds, and the pipeline puts them together. Shared files, such as bootstrap.dart, have sockets: named places where modules put code without knowing who else does.
Contributions
| Contribution | What it adds |
|---|---|
BrickContribution | Template files, as a mason bundle. Every generated file has exactly one owner. |
SocketContribution | Code or a value for a socket. |
RoleData | Data for a role in the role's own language, such as the routes of a feature for the router. |
PubspecContribution | A dependency, an SDK constraint, or a setting of the flutter: section of pubspec.yaml. |
CodegenRequest | A request to run build_runner in the new app. |
Preflight | Checks of the machine before generation, which can offer to set up what is missing. |
PostGenStep | A command to run in the new app, such as flutterfire configure. |
Every contribution can have a when, a set of roles that must all be present for the contribution to apply. A module lists there the roles it only uses, so code that refers to them is generated only in apps that have them. firebase_analytics gives the router its listener of the screen with when: {routerRole}.
Sockets
A socket belongs to a role, to a module or to the pipeline. Its template marks it with a tag in three braces that joins smf_, the owner and the name of the socket, such as {{{smf_app_entry__bootstrap_platform}}}. The pipeline replaces the tag with the rendered contributions. Modules refer to sockets only through typed constants, such as AppEntryRole.bootstrapPlatform, so a misspelled socket does not compile.
This is lib/bootstrap.dart as flutter_core writes it, with the tags of the start-up phases:
{{{smf_app_entry__top_level}}}
Future<void> bootstrap() async {
{{{smf_app_entry__bootstrap_early}}}
{{{smf_app_entry__bootstrap_platform}}}
{{{smf_app_entry__bootstrap_di}}}
{{{smf_app_entry__bootstrap_late}}}
}
The kind of a socket decides what it takes and how its contributions are rendered:
| Kind | Takes | Rendered as | Example |
|---|---|---|---|
CodeSocket | Fragments of code, with the imports they need | One after another | The start-up phases |
ArgsSocket | Named arguments of a call | name: value, lines. A list argument unites its items, and two values of a single argument conflict. | theme and supportedLocales of MaterialApp |
WrapperSocket | A widget around a piece of code, as an opening and a closing part | The first contribution outermost | ProviderScope(child: …) around the root widget |
FactoryListSocket | Functions that the socket's owner calls | List items | Navigator observers and listeners of the screen of the router |
KeyedSocket | Entries with a key | Merged by key with a policy | Android permissions, Info.plist keys, Gradle plugins, README sections |
ValueSocket | One value | Merged with a policy | The minimum iOS version, the highest any module needs |
A fragment of code never imports anything itself. It lists its imports, such as ImportRef('package:firebase_core/firebase_core.dart'), and the pipeline adds them to the file that holds the socket's tag, merged and sorted. ImportRef.app('firebase_options.dart') imports a file of the app without knowing the app's package name.
The sockets of the app entry
The app entry role is open to every module, so any module can use these sockets:
| Socket | Where | What goes in |
|---|---|---|
bootstrapEarly, bootstrapPlatform, bootstrapDi, bootstrapLate | bootstrap() | Start-up code, by phase |
topLevel | lib/bootstrap.dart | Top-level declarations, such as a handler of background messages |
rootWrappers | main() | Widgets around the root widget |
appArgs | the root MaterialApp | theme, darkTheme, localizationsDelegates, supportedLocales |
appBuilder | the builder of the root MaterialApp | Widgets around the content of every route |
iosDeploymentTarget | project.pbxproj | The minimum iOS version, where the highest wins |
androidManifestPermissions | AndroidManifest.xml | <uses-permission> by name |
androidManifestApplicationMeta | AndroidManifest.xml | <meta-data> of the application |
mainActivityIntentFilters | AndroidManifest.xml | <intent-filter>s of the main activity, such as deep links |
infoPlist | Info.plist | Keys with a string, a boolean, an integer or an array of strings, where arrays are united |
gradleSettingsPlugins, gradleAppPlugins, gradleAppDependencies | Gradle files | Plugins with their versions, the plugins the app applies, and dependencies, where the highest version wins |
readmeSections | README.md | Sections by heading, such as the setup of Firebase |
The router role adds observers and screenListeners, and two families of sockets for the annotations of screens and of their parameters, which a router that needs annotations fills.
Who may contribute where
- A socket of a role takes contributions from the modules that provide, require or use the role, and from the templates of the role and of the roles that require or use it. The sockets of the app entry are open to every module.
- A socket of a module takes contributions from the modules that depend on it directly.
- The sockets of the pipeline, which are the sections of
pubspec.yaml, take no contributions. Modules contributePubspecContributions instead, and the pipeline merges them.
A module may contribute data only to its own roles, and only a role's providers contribute its implementations. The pipeline checks all of it before it renders anything.
Order
When several modules put code into one socket, or several steps run after generation, they come in an order that the pipeline derives from the modules alone:
- A module comes after the modules it depends on.
- A module that requires a role, or names it in
when, comes after the role's providers and its template. - A role's template comes after the role's providers, and after the providers and templates of the roles that its role requires or that its contributions name in
when.
A contributor comes after every contributor it reaches through these edges, even through modules that add nothing to the socket. Otherwise contributors are ordered by id. A cycle is an error. smf create --explain prints the order of every socket with more than one contributor, and the edges that decide it.
That is why installCrashReporting() of the crash reporting role runs after Firebase.initializeApp of firebase_core: the role's template comes after its provider, firebase_crashlytics, which depends on firebase_core.
Merging
Keyed and value sockets merge the contributions of several modules with a policy of the socket:
| Policy | How values merge | Used for |
|---|---|---|
| conflict | Equal values agree, and different values are an error that names both modules. | Android permissions, README sections |
| max | The highest version wins. | The minimum iOS version, Gradle plugins |
Info.plist | Arrays of strings are united, and every other value must agree. | Info.plist keys |
The pipeline merges pubspec.yaml itself. It intersects the constraints of a package that several modules add, and an empty intersection is an error. A package that is both a dependency and a dev dependency becomes a dependency.