Skip to content

A Flutter app in one command

Pick what the app needs, such as a router, tabs at the bottom, sign-in, dependency injection, a state manager and Firebase. smf create generates a Flutter project in which these parts already work together.

The project does not depend on SMF at run time, so its code is yours from the first commit.

dart pub global activate smf_flutter_cli
smf create --explain
$ smf create my_app --org com.example -m home,bottom_tabs,get_it,event_bus --explainApp my_app of com.example  Android application id: com.example.my_app  iOS bundle id: com.example.my-app  Directory: /Users/you/projects/my_app Modules  home: requested  bottom_tabs: requested  get_it: requested  event_bus: requested  flutter_core: the only provider of the app entry role, which every app needs  go_router: the only provider of the router role, which home requires Roles  Layout: bottom_tabs  Dependency injection: get_it  Events: event_bus  App entry: flutter_core  Router: go_router

--explain prints the plan and stops before it writes anything. This is the start of its report, from the documentation.

SMF generates apps for Flutter 3.44 or newer and Dart 3.12 or newer. It is tested on macOS, Linux and Windows.

How it works

The elements of an app

SMF builds an app from roles, such as the router or dependency injection, and modules fill them. Pick a module for each role the app needs and leave out the rest. A module that needs a role works with every module that provides it, so you can swap one for another and the app still compiles.

  1. Choose the modules

    Pick the features and a module for each role the app needs, in the questions of smf create or with -m.

    -m home,bottom_tabs,get_it,bloc

  2. Run smf create

    SMF adds the modules that your choice needs, generates the project, and applies dart fix and dart format.

    smf create my_app -m home,bottom_tabs,get_it,bloc

  3. Run the app

    You get a regular Flutter project in which the parts already work together. Start it with flutter run.

    cd my_app && flutter run

Router

An app has none or one.

The routes of the screens that features declare, and a typed way to reach them. A screen navigates with context.nav.home.home().go() rather than through the router, so its code works with any router. With a layout, the router also builds the main navigation.

Your code uses

  • context.nav
  • AppRouter

Modules that provide it

  • go_router

    Routes and a typed navigation facade, with go_router.

How to choose

Choose it with -m go_router or in the questions of smf create. A feature or a layout needs a router, so SMF adds go_router by itself while it is the only router.

Navigation

The tiles are some of the roles of an app. Features, such as the start screen home or the screens of sign_in, and infrastructure, such as firebase_core, provide no role. The documentation has every module, with the role that it fills.All modules

The app

What the app looks like

The video and the stills show an app as smf create generates it, before anyone edits a line. It opens with an onboarding, then asks the user to sign in or to create an account with an email address and a password. Past that are a start screen that lists what to do next, a screen of the account, and settings that change the theme and the language. It comes in a light and a dark theme, in English and Ukrainian.

A walk through the app in 43 seconds, without sound: the two pages of the onboarding, the sign-in, a new account, the start screen, then the settings with the account, the dark theme and Ukrainian, and the start screen once more. A script recorded it on an iPhone simulator. The sign-up is real: the app creates the account in a Firebase project, and the script deletes it when the walk ends.
  1. The first page of the onboarding: a white tile with the letters Ma, drawn as an element of the periodic table, above the name My App, the line "Welcome! We are glad you are here." and the buttons Skip and Next.
    The first launch opens with the onboarding.
  2. The sign-in in the light theme: the tile with the letters Ma among the cells of a table, the title "Sign in", the fields Email and Password, the link "Forgot password?", the button Sign in and, below it, Create account.
    Then the app asks the user to sign in.
  3. The start screen in the light theme: a greeting, the name My App, a dark green card that says "Your app is ready", and the next steps "Make this screen yours" and "Add a feature". A bar at the bottom has the tabs Home and Settings.
    With an account, the user reaches the start screen.
  4. The settings screen in the light theme: the row Account with the address ada.lovelace@example.com, the theme, with Light selected among System, Light and Dark, and the language, which is English.
    The settings have the account, the theme and the language.
  5. The screen of the account in the light theme: the title Account, the line "You are signed in to this account:", the address ada.lovelace@example.com, the button Sign out and, in red, Delete account.
    The screen of the account signs out or deletes the account.
  6. The start screen in the dark theme, with every text in Ukrainian: the greeting, the card that says the app is ready, the next steps and the two tabs.
    The start screen again, dark and in Ukrainian.

The command for this app

smf create my_app -m home,onboarding,sign_in,bottom_tabs,material_theme,gen_l10n,get_it,bloc

onboarding, sign_in and home are the screens, bottom_tabs is the bar with the tabs, material_theme brings the two themes and gen_l10n the texts in both languages. get_it is dependency injection and bloc the state manager. smf create adds what these need: go_router, the settings screen, shared_preferences and, for the accounts, firebase_auth with firebase_core.

The accounts are in Firebase Authentication, so this app needs a Firebase project before it starts. In a terminal, smf create asks whether to register the app in your project, and then whether to enable sign-in with an email address and a password there. Until the app is registered, it compiles but stops at start-up.

Sign-in and its Firebase projectTour of a generated app

The code

Every file has one author

Some files come from a module and others from a role, whichever module provides it. That split lets modules replace each other.

Routes and typed navigation
With a router, every screen of a feature has a route. Screens navigate with context.nav rather than through go_router, so their code works with any router.
Navigation
Tabs that keep their stacks
With bottom_tabs, the routes that features mark as destinations become the tabs of a bar at the bottom, and each tab keeps its own stack.
bottom_tabs
Services behind the state
With get_it, the app registers the services of the modules before its first frame. Screens reach them only through their state, a Cubit or a Riverpod provider.
Services and state
Start-up in the right order
bootstrap() runs the start-up code of the modules in phases, so Firebase and crash reporting are ready before the services of the app.
Start-up
Firebase and its setup
SMF checks for the Firebase CLI, a login and the FlutterFire CLI, and offers to set up what it can. After generation it asks whether to run flutterfire configure.
Firebase
Code you own
SMF applies dart fix and dart format to the new app. The app has no SMF package in its dependencies, and nothing in it has to be generated again.
It is your code now

my_app · home, bottom_tabs, get_it, event_bus

The files of my_app and who wrote each
FileWritten by
my_app/
AGENTS.mdapp entry role
CLAUDE.mdapp entry role
README.mdflutter_core
analysis_options.yamlflutter_core
pubspec.yamlflutter_core
android/flutter_core
ios/flutter_core
lib/
main.dartflutter_core
bootstrap.dartflutter_core
app.dartflutter_core
core/
app/fallback_start_screen.dartflutter_core
di/service_locator.dartDI role
di/dependencies.dartget_it
events/communication_service.dartevents role
events/event_bus_communication_service.dartevent_bus
layout/destination.dartlayout role
layout/app_shell.dartbottom_tabs
router/app_router.dartrouter role
router/navigation.dartrouter role
router/app_router_factory.dartgo_router
features/home/home_screen.darthome
features/home/assets/home
test/flutter_core
Navigating from a screen
context.nav.home.home().go();

For teams

For teams and module authors

SMF rests on one principle: modules do not know each other. Roles, sockets and the checks that smf create runs keep it that way, so a team can swap the provider of a role or add modules of its own.

Modules depend on roles
A module names only the modules it depends on, as the Firebase modules name firebase_core. For everything else it names a role, such as a router or a DI container, and whichever module the user picked provides it. A screen needs a router, and analytics follows the screens of whichever router the app has.
The module model
Sockets in shared files
A module does not edit files. Shared files such as bootstrap.dart have sockets, named places where modules put code, and smf create orders and merges what the modules contribute.
Sockets and contributions
Modules of your own
A module is an ordinary Dart package built on smf_contracts. A command of your own, built on smf_flutter_cli, offers your modules next to the built-in ones, with every option of smf create.
Extending SMF
A contract harness
The harness in smf_pipeline renders every app that matters for a module in memory and checks it against the rules of the module model. The built-in modules test themselves with it.
Test a module
Combinations checked in CI
CI generates apps from the modules in many combinations, each module with each provider of the roles it requires and every module together, and runs flutter analyze on each.
Module independence
Ready for scripts
With --no-input a run asks nothing, and with --strict it stops rather than leave a module out. --explain prints the plan without writing a file, and exit codes tell a script what happened.
Scripts and CI
The module home
final class HomeModule extends SmfModule {
  const HomeModule();

  static const id = ModuleId('home');

  @override
  ModuleDescriptor get descriptor => const ModuleDescriptor(
        id: id,
        description: 'Start screen with the name of the app',
        kind: ModuleKinds.feature,
      );

  @override
  List<Contribution> contribute(ModuleContext context) => [
        BrickContribution(homeBundle),
        routerRole.data(const RoutesData([/* the route of the screen */])),
      ];
}
The contract test of a counter module, from the docs
final harness = ContractHarness(
  ModuleRegistry(const [
    FlutterCoreModule(),
    GoRouterModule(),
    GetItModule(),
    BlocModule(),
    RiverpodModule(),
    CounterModule(),
  ]),
);
final results = await harness.checkAll();
for (final result in results) {
  expect(result.errors, isEmpty, reason: '${result.contractCase}');
}

Free and open source

SMF is developed in the open under the Apache License 2.0. Bug reports and ideas go to GitHub Issues, and questions are welcome on Discord.

Release notes by email

Get an email when SMF or its modules have a new release.

We use Resend. You can unsubscribe at any time.

Contact

Ask questions on Discord, and report bugs or ideas on GitHub. For anything else, write to us here.

DiscordGitHub Issues

Write to us