Skip to main content

Tour of a generated app

This tour uses the app from Create your first app:

smf create my_app --org com.example -m home,bottom_tabs,get_it,event_bus --no-input

The app has six modules: the four in -m, go_router, which SMF added because home needs a router, and flutter_core, which every app has.

The files​

my_app/
├── .gitignore
├── .metadata
├── README.md
├── analysis_options.yaml
├── pubspec.yaml
├── android/ Android project of flutter create
├── ios/ iOS project of flutter create
├── lib/
│ ├── main.dart
│ ├── bootstrap.dart
│ ├── app.dart
│ ├── core/
│ │ ├── app/fallback_start_screen.dart
│ │ ├── di/
│ │ │ ├── service_locator.dart
│ │ │ └── dependencies.dart
│ │ ├── events/
│ │ │ ├── communication_service.dart
│ │ │ └── event_bus_communication_service.dart
│ │ ├── layout/
│ │ │ ├── destination.dart
│ │ │ └── app_shell.dart
│ │ └── router/
│ │ ├── app_router.dart
│ │ ├── navigation.dart
│ │ └── app_router_factory.dart
│ └── features/
│ └── home/home_screen.dart
└── test/
└── core/app/fallback_start_screen_test.dart

flutter pub get adds pubspec.lock and .dart_tool/, as in any Flutter project.

Every file has exactly one author. Some files come from a module, and others come from a role, whichever module provides it. A role describes what the rest of the app can count on, such as a router with typed navigation, and generates that part itself. The module that provides the role fills in the rest.

FilesWritten by
main.dart, bootstrap.dart, app.dart, core/app/, test/, pubspec.yaml, analysis_options.yaml, README.md, .gitignore, .metadata, android/, ios/the module flutter_core, which provides the app entry
core/router/app_router.dart, core/router/navigation.dartthe router role
core/router/app_router_factory.dartthe module go_router, which provides the router
core/layout/destination.dartthe layout role
core/layout/app_shell.dartthe module bottom_tabs, which provides the layout
core/di/service_locator.dartthe dependency injection role
core/di/dependencies.dartthe module get_it, which provides dependency injection
core/events/communication_service.dartthe events role
core/events/event_bus_communication_service.dartthe module event_bus, which provides the events
features/home/home_screen.dartthe module home, a feature

This split lets modules replace each other. Code that navigates uses navigation.dart of the router role rather than go_router, and code that needs a service uses service_locator.dart rather than get_it.

Start-up​

lib/main.dart has the same three steps in every app:

lib/main.dart
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await bootstrap();
runApp(const App());
}

bootstrap() runs the start-up code of modules and roles before the first frame. They add their code to one of four phases, which run in this order: code that must come first, platform services such as Firebase, the services of the app, and code that needs those services. In this app, only the dependency injection role adds start-up code. It calls registerDependencies(), which get_it generates:

lib/bootstrap.dart
Future<void> bootstrap() async {
await registerDependencies();
}

With firebase_crashlytics, the platform phase would initialize Firebase and then install the crash reporting, before registerDependencies(). Modules can also wrap the root widget. With Riverpod, for example, runApp gets ProviderScope(child: const App()).

lib/app.dart holds the root widget. With a router it is a MaterialApp.router with the router's configuration. Without one, it is a MaterialApp that shows the fallback start screen of core/app/.

The builder of the root widget, (context, child) => child!, does nothing in this app. Modules wrap the content of every route there through the socket appBuilder, so the template keeps the builder even when no module uses it.

Router and navigation​

The router role generates two files. app_router.dart has the AppRouter interface and the instance appRouter, which createAppRouter() creates on first use. navigation.dart has the typed navigation: a location class for every route, such as HomeHomeLocation for /home, and context.nav, so a screen navigates with context.nav.home.home().go().

go_router implements createAppRouter() in app_router_factory.dart with a GoRouter that has every route of every feature, the redirect of / to the start screen and, with a layout, the tabs. The routes of each feature live under its id, so the route / of home is /home. See Navigation.

Layout​

The layout role generates Destination, which holds a label and an icon. bottom_tabs generates AppShell, a Scaffold with the screen of the selected tab and a NavigationBar with a tab for each destination. The router puts the destination of every feature into it, in the order of the features, and keeps the stack of each tab while another one is selected. home declares one destination, so the bar stays hidden until a second feature adds another.

Dependency injection​

The dependency injection role generates service_locator.dart with the ServiceLocator interface and the functions resolve<T>() and resolveWith<T>(...). get_it generates dependencies.dart, whose registerDependencies() registers the services of all modules in get_it:

lib/core/di/dependencies.dart
Future<void> registerDependencies() async {
final getIt = GetIt.instance;
getIt.registerLazySingleton<di0.CommunicationService>(
() => di0.createCommunicationService(),
);
}

The events role registered its service here because the app has a container. Without one, the app would get the service from createCommunicationService(). See Services and state.

Events​

The events role generates communication_service.dart with AppEvent, the base class of events, and the CommunicationService interface. Its fire(event) sends an event, and on<T>() streams the events of a type. event_bus implements the interface on the EventBus of the event_bus package.

The feature​

home adds features/home/home_screen.dart, a screen with the name of the app in its app bar. It declares one route, /home, which is a destination labelled Home and a screen that the app can start on. Features keep their files in lib/features/<id>/.

The rest of the project​

  • pubspec.yaml has the dependencies of all modules: get_it, go_router and event_bus here, next to Flutter's own. The app needs Flutter 3.44 or newer and Dart 3.12 or newer.
  • analysis_options.yaml has the lints of a new Flutter app, flutter_lints, and leaves build/ out of the analysis. On macOS, Flutter copies the Swift packages of plugins there, with their examples, and flutter analyze would report those otherwise.
  • android/ and ios/ are the projects of flutter create of Flutter 3.44, with the app's identifiers: com.example.my_app on Android and com.example.my-app on iOS. The minimum iOS version is 15.0.
  • README.md has the name of the app. Modules can add sections to it, as firebase_core does with the setup of Firebase.
  • test/ has a widget test of the fallback start screen.

It is your code now​

SMF generates the app once, and the result is a plain Flutter project. It has no SMF package in its dependencies, and no command updates it later, so change any file as you would in a project you wrote yourself. To add screens later, add routes to app_router_factory.dart as you would in any go_router app.

Next​