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

Architecture

Routing

Feature defines its own route. App collects all routes. Navigate via enum — never hardcoded strings.

Feature defines its route

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.settings.route,
      name: AppRoutes.settings.name,
      parentNavigatorKey: shellNavigatorKey,
      builder: (context, state) => const SettingsScreen(),
    ),
  ],
);

App collects all feature routes

lib/app/app_router.dart
final appRouter = GoRouter(
  initialLocation: AppRoutes.splash.route,
  navigatorKey: rootNavigatorKey,
  redirect: (context, state) async {
    final noAuthRoutes = [
      AppRoutes.splash.route,
      AppRoutes.webView.route,
      AppRoutes.mediaViewer.route,
    ];
    if (noAuthRoutes.contains(state.uri.path)) return null;

    final token = await sl<TokenManager>().getAccessToken();
    final isAuthenticated = token != null && token.isNotEmpty;
    final isAuthRoute = state.uri.path.startsWith(AppRoutes.auth.route);

    if (!isAuthenticated && !isAuthRoute) return AppRoutes.auth.route;
    if (isAuthenticated && isAuthRoute) return AppRoutes.home.route;
    return null;
  },
  routes: [
    GoRoute(path: AppRoutes.splash.route, builder: ...),
    authRoute,
    StatefulShellRoute.indexedStack(
      builder: (context, state, shell) => _AppShell(shell),
      branches: [
        StatefulShellBranch(routes: [homeRoute]),
        StatefulShellBranch(routes: [feedRoute]),
        StatefulShellBranch(routes: [discoverRoute]),
        StatefulShellBranch(routes: [activityRoute]),
        StatefulShellBranch(routes: [profileRoute]),
      ],
    ),
  ],
);

Navigate — always via AppRoutes enum

lib/features/home/presentation/screens/home_screen.dart
// Never hardcoded strings — always enum
context.go(AppRoutes.profile.route);
context.push(AppRoutes.settings.route);

// With parameters
context.push(AppRoutes.profileView.route, pathParameters: {'user_id': userId});

StatefulShellRoute — Bottom Navigation

Five top-level tabs, each its own StatefulShellBranch. The shell owns the Scaffold and AppBottomNavBar; switching tabs calls goBranch so each branch keeps its own navigation stack.

lib/app/app_router.dart
final appRouter = GoRouter(
  initialLocation: AppRoutes.splash.route,
  navigatorKey: rootNavigatorKey,
  redirect: _authGuard,
  routes: [
    GoRoute(path: AppRoutes.splash.route, builder: ...),
    authRoute,
    StatefulShellRoute.indexedStack(
      builder: (context, state, shell) => _AppShell(shell),
      branches: [
        StatefulShellBranch(routes: [homeRoute]),
        StatefulShellBranch(routes: [feedRoute]),
        StatefulShellBranch(routes: [discoverRoute]),
        StatefulShellBranch(routes: [activityRoute]),
        StatefulShellBranch(routes: [profileRoute]),
      ],
    ),
  ],
);

class _AppShell extends StatefulWidget {
  const _AppShell(this.navigationShell);
  final StatefulNavigationShell navigationShell;

  @override
  State<_AppShell> createState() => _AppShellState();
}

class _AppShellState extends State<_AppShell> {
  void _onTabTapped(int index) {
    if (index == widget.navigationShell.currentIndex) return;
    widget.navigationShell.goBranch(index);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: widget.navigationShell,
      bottomNavigationBar: AppBottomNavBar(
        currentIndex: widget.navigationShell.currentIndex,
        onTap: _onTabTapped,
        items: AppBottomNavItem.values.toList(),
      ),
    );
  }
}
sam's arch — A Pragmatic Flutter Architecture