Keyboard Shortcuts

j / kScroll down / up
ggScroll to top
GScroll to bottom
K / /Open search
?Show this help
EscClose search / help
n / NNext / previous section
hGo to landing
dGo to docs
:Command mode — type section name

Press ? or Esc to close

Getting Started

Feature Template

Every feature is a self-contained vertical slice. The only thing the outside world imports is the feature's config/ folder — its route and DI module.

Canonical shape

<feature>/
config/
{feature}_route.dart # GoRoute definition
{feature}_di.dart # registerXxxDependencies()
data/
models/ # DTOs: fromJson / toJson / empty()
source/ # raw ApiClient calls
store/ # extends BaseStore
local/ # sqflite cache (opt-in)
logic/
{feature}_cubit.dart # state + cubit
presentation/
screens/ # StatefulWidget, cubit in initState
widgets/ # stateless, reusable within feature

DI module

One function per feature, called from bootstrap(). Singletons are lazy (created on first access); Cubits are factories (a fresh instance per screen).

registerLazySingletonDataSource, Store — one shared instance
registerFactoryCubit — new instance per BlocProvider
registerSingletonGlobal services — ApiClient, Logger, secure storage
lib/features/profile/config/profile_di.dart
void registerProfileDependencies() {
  // Shared instance, created on first access
  sl.registerLazySingleton(() => ProfileDataSource(sl<ApiClient>()));
  sl.registerLazySingleton(() => ProfileStore(sl<ProfileDataSource>()));

  // Fresh instance each time the screen is opened
  sl.registerFactory(() => ProfileCubit(sl<ProfileStore>()));
}

Route module

The feature owns its GoRoute. The app collects them all. Full-screen sub-routes (e.g. detail pages) set parentNavigatorKey so they push above the shell.

lib/features/profile/config/profile_route.dart
final profileRoute = GoRoute(
  path: AppRoutes.profile.route,
  name: AppRoutes.profile.name,
  builder: (context, state) => const ProfileScreen(),
  routes: [
    GoRoute(
      path: AppRoutes.profileView.route,
      parentNavigatorKey: rootNavigatorKey,
      builder: (context, state) => ProfileViewScreen(
        userId: state.pathParameters['id']!,
      ),
    ),
  ],
);
Rule of one: outside code may import exactly two things from a feature — its route and its DI module. Everything else is internal.
sam's arch — A Pragmatic Flutter Architecture