Write an infrastructure module
An infrastructure module sets up a library or a service without screens of its own. It declares no routes, keeps nothing in lib/features/, has no variants, and never takes services from the DI container itself. It puts its code and settings into the app entry, which every module may use, and into the sockets of the roles and modules it declares.
The examples on this page come from two small modules that were generated into an app and checked with flutter analyze: logging, which sets up the logging package, and git, which makes the new app a Git repository.
Start-up code
bootstrap() runs the start-up code of all modules in four phases: early, platform, DI and late. A module adds a fragment of code to a phase, with the imports it needs:
List<Contribution> contribute(ModuleContext context) => [
const PubspecContribution.hosted('logging', '^1.3.0'),
const SocketContribution.code(
AppEntryRole.bootstrapEarly,
Fragment(
r"Logger.root.onRecord.listen((record) => debugPrint('${record.level.name}: ${record.message}'));",
imports: [
ImportRef('package:flutter/foundation.dart', show: ['debugPrint']),
ImportRef('package:logging/logging.dart', show: ['Logger']),
],
),
),
];
The pipeline adds the imports to lib/bootstrap.dart, merges them with those of other modules, and formats the file:
import 'package:flutter/foundation.dart' show debugPrint;
import 'package:logging/logging.dart' show Logger;
Future<void> bootstrap() async {
Logger.root.onRecord.listen(
(record) => debugPrint('${record.level.name}: ${record.message}'),
);
}
Pick the phase by what the code needs: early for what must come first, platform for platform services such as Firebase.initializeApp, DI for the registration of the services, and late for code that needs the services. Code that must be declared at the top level of lib/bootstrap.dart, such as a handler of background messages, goes into AppEntryRole.topLevel. lib/bootstrap.dart imports neither material nor cupertino, so start-up code does not depend on a design library.
Wrappers around the app
AppEntryRole.rootWrappers wraps the root widget in main(), and AppEntryRole.appBuilder wraps the content of every route, in the builder of the root MaterialApp. A wrapper has an opening and a closing part. This is how riverpod adds its ProviderScope:
const SocketContribution.wrap(
AppEntryRole.rootWrappers,
Fragment.wrap(
'ProviderScope(child: ',
')',
imports: [
ImportRef(
'package:flutter_riverpod/flutter_riverpod.dart',
show: ['ProviderScope'],
),
],
),
),
The first contribution is the outermost wrapper, as the order of contributions decides. AppEntryRole.appArgs sets theme, darkTheme, localizationsDelegates and supportedLocales of the root MaterialApp.
Dependencies
PubspecContribution adds to the pubspec.yaml of the app:
PubspecContribution.hosted('logging', '^1.3.0')depends on a package of pub.dev, anddev: truemakes it a dev dependency.PubspecContribution.sdk('flutter_localizations')depends on a package of the Flutter SDK.PubspecContribution.flutter(assets: [...], fonts: [...], generate: true)adds to theflutter:section.PubspecContribution.environment(sdk: ..., flutter: ...)constrains the Dart and Flutter versions of the app.
The pipeline intersects the constraints of a package that several modules add, and an empty intersection is an error. Declare environment only when your package needs a newer Dart or Flutter than the app has anyway. flutter_core sets Dart ^3.12.0 and Flutter >=3.44.0, and smf create checks the Flutter SDK against the merged constraints before it generates anything.
A package that the provider of a role brings, such as flutter_riverpod of riverpod, is that provider's. Another module adds it only in its variant for the provider, with the constraint any, or when it depends on the provider. A package that no provider brings is shared, and every module that needs it adds it. A provider brings a package when it adds the package outside a variant with a constraint other than any. In a provider, add a shared package, such as collection, with any. See The packages of a provider.
Native settings
The app entry has sockets for the native files, so a module never edits them:
AppEntryRole.iosDeploymentTarget.value('16.0'),
AppEntryRole.androidManifestPermissions.key('android.permission.INTERNET'),
AppEntryRole.androidManifestApplicationMeta.entry(
'com.example.sdk.CHANNEL',
const AndroidMetaData.value('stable'),
),
AppEntryRole.infoPlist.entry(
'UIBackgroundModes',
const PlistStringArray(['fetch']),
),
AppEntryRole.gradleSettingsPlugins.entry('io.github.ben-manes.versions', '0.64.0'),
AppEntryRole.gradleAppPlugins.key('io.github.ben-manes.versions'),
AppEntryRole.gradleAppDependencies.entry('androidx.annotation:annotation', '1.9.1'),
- The minimum iOS version is the highest that any module asks for, and
flutter_coregives 15.0. - Equal entries of several modules merge, and different values of one key are an error that names both modules. Arrays of strings in
Info.plistare united, and Gradle versions take the highest. - A key that the template of
flutter_corehas already, such asCFBundleName, cannot be added again. The Gradle sockets do not take the plugins that the template declares or applies, or the Kotlin plugin, which the Flutter Gradle plugin applies itself. AppEntryRole.mainActivityIntentFilterstakes<intent-filter>elements of the main activity as XML, such as deep links.
README sections
AppEntryRole.readmeSections adds a section to the README.md of the app, under a heading of its own, in Markdown:
AppEntryRole.readmeSections.entry(
'Logging',
'The app logs with the [logging](https://pub.dev/packages/logging) '
'package: create a `Logger` per class. `bootstrap()` prints the '
'records at `Logger.root.level` and above, which is INFO by default.',
),
Use it for what someone who opens the app needs to know about your module, such as how to set up a service again. firebase_core explains there how to configure Firebase.
Building on another module
When your module builds on another module itself, and not on a role, depend on it with the id constant of its package:
dependsOn: {FirebaseCoreModule.id},
The other module comes into the app with yours, and the constant makes its package a dependency of yours. Your module may then put code into the sockets of that module. A module can declare sockets of its own, such as SocketRef.module(id, 'setup', CodeSocket()), which only the modules that depend on it directly may fill. Prefer a role whenever one describes what you need, so that the user can pick its provider.
Code generation
A module that needs build_runner contributes a CodegenRequest, and the builders it needs as dev dependencies:
const PubspecContribution.hosted('json_annotation', '^4.9.0'),
const PubspecContribution.hosted('json_serializable', '^6.9.0', dev: true),
const CodegenRequest(description: 'JSON code of FixtureModel'),
However many modules ask, the pipeline runs dart run build_runner build --force-jit once, after flutter pub get, and adds build_runner to the dev dependencies itself. A library that the builders generate and that code of the app imports, such as lib/core/di/dependencies.config.dart, goes into outputs, so that the pipeline and the harness know about it. Part files, such as .g.dart, need no mention.
Checking the machine
A module that needs a tool on the machine contributes a Preflight with checks. A check has an id, unique in its module, and a description, which the report of --explain and the warnings show:
/// Checks that Git is installed.
final class GitCheck extends PreflightCheck {
const GitCheck();
String get id => 'git';
String get description => 'Git';
Future<PreflightStatus> check(SmfEnvironment environment) async {
if (await environment.findExecutable('git') != null) {
return const PreflightPassed();
}
return const PreflightMissing(
instructions: 'Install Git from https://git-scm.com/downloads.',
);
}
}
checkonly reads the machine, because--explainruns it too. It does not install anything, log in, ask the user or change files other than its temporary files. It returnsPreflightPassed,PreflightMissingwith instructions, orPreflightFailedwhen the check itself could not run.- When the check finds something other than what it looks for, such as an older version of the tool, it says what it found in
PreflightMissing(found: ...), as a clause such as'git 2.20.0 is installed'. The pipeline then says that the tool is needed, but git 2.20.0 is installed, rather than that the tool is missing. - A missing tool that the check can install is
PreflightMissing(installable: true). In a terminal, unless the run skips external setup, the pipeline asks the user, showing what the check found and its instructions, callsinstall, and runscheckagain.installreturns aToolInstallwith the directories of what it installed, which the pipeline adds to thePATHof later checks and commands. - A failed check is a warning by default. Set
requiredonly when the app cannot work without the tool. A failed required check leaves the module out of the app, or stops the run with--strictor when no app can be made without the module. - Never show the output of a command that can hold a secret, such as a token of a login. The login check of
firebase_corereports only the error of the command. - Run commands with the
processRunnerof the environment, and find them withfindExecutable, which also looks in the directories of tools installed during the run. The namesflutteranddartstand for the Flutter SDK that the pipeline checked. On Windows, a batch file such asdart.batcannot take arguments with^,<,>,%,&,|or", becausecmd.exewould read them as its own. - A command that may wait a long time, such as one that asks a service over the network, gets a
timeoutinprocessRunner.run. The runner then stops the command along with the processes it started, and the result says so withtimedOut. The login check offirebase_coregivesfirebase projects:list40 seconds. - The pipeline shows the progress "Checking the machine" while the checks run, and another progress for a check that runs again after an installation, so a check does not need to show one itself.
- Read an environment variable of
smfwithenvironmentVariable, such asSSH_CONNECTION, which tells the login check offirebase_corethat the session is over SSH. On Windows, the name may have any case.
See the checks of firebase_core for checks that install tools and log in.
Commands after generation
A PostGenStep runs a command in the new app, after flutter pub get and code generation:
const Preflight([GitCheck()]),
PostGenStep(
ToolRef('git'),
['init', '--quiet'],
description: 'Creating a Git repository',
skippable: true,
needs: ['git'],
),
✓ Getting the packages of the app (1.0s)
✓ Creating a Git repository (0.0s)
✓ Cleaning up the imports (1.9s)
| Option | Meaning |
|---|---|
description | What the step does, for its progress and for the instructions when it is left for later. |
interactive | The command talks to the user, so it runs with the terminal attached, and only in a run that can ask. |
external | The command needs something outside the app, such as an account, so a run with --skip-external-setup leaves it for later. |
skippable | The app is complete without it, so the pipeline may leave it for later, printing its command, instead of failing. In a run that can ask, the pipeline asks before it runs the step, so the user can leave it for later too. |
needs | Ids of checks of the module's Preflight. When one has not passed, the step does not run, is not asked about, and is left for later with the check as the reason. |
followUps | Steps that finish this one, such as a fix of a file its tool writes. They run right after it, without a question, once it succeeded. Otherwise they are left for later after it. |
hosts | The operating systems on which the step applies at all, such as macOS for a step that changes the Xcode project. |
A step that is not skippable fails the generation when it fails or cannot run. ToolRef names the executable, arguments that always come first, and environment variables. firebase_core, for example, runs flutterfire as ToolRef('dart', prefixArgs: ['pub', 'global', 'run', 'flutterfire_cli:flutterfire']).
The step runs in the temporary directory of the app, which then moves to its place, so it must not write the absolute path of its working directory into the app. The pipeline writes the files where Flutter records such paths again in the app's directory.
Services
An infrastructure module that creates services registers them as data of the DI role, and gets the services they need through the factory, as in FactoryRef('createApiClient', import: ..., deps: [ServiceRef(TypeRef('HttpClient', import: ...))]). The container calls the factory with those services, and the module never calls resolve. See registering services.
A module that implements analytics, crash reporting or events provides the role instead; see Provide a role.