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.
| Files | Written 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.dart | the router role |
core/router/app_router_factory.dart | the module go_router, which provides the router |
core/layout/destination.dart | the layout role |
core/layout/app_shell.dart | the module bottom_tabs, which provides the layout |
core/di/service_locator.dart | the dependency injection role |
core/di/dependencies.dart | the module get_it, which provides dependency injection |
core/events/communication_service.dart | the events role |
core/events/event_bus_communication_service.dart | the module event_bus, which provides the events |
features/home/home_screen.dart | the 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:
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:
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:
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.yamlhas the dependencies of all modules:get_it,go_routerandevent_bushere, next to Flutter's own. The app needs Flutter 3.44 or newer and Dart 3.12 or newer.analysis_options.yamlhas the lints of a new Flutter app,flutter_lints, and leavesbuild/out of the analysis. On macOS, Flutter copies the Swift packages of plugins there, with their examples, andflutter analyzewould report those otherwise.android/andios/are the projects offlutter createof Flutter 3.44, with the app's identifiers:com.example.my_appon Android andcom.example.my-appon iOS. The minimum iOS version is 15.0.README.mdhas the name of the app. Modules can add sections to it, asfirebase_coredoes 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
- Choosing modules shows what else a generated app can have.
- The module model explains how the modules fit together without knowing each other.