Navigation. with AutoRoute

Orexjeka
4 min readAug 2, 2024

--

Flutter 3.24.1, Dart 3.5.1. Mac OS. VS Code.

Be sure you seen a Setup

AutoRoute is a powerful routing package for Flutter that simplifies the management of complex navigation scenarios. It provides a declarative way to define routes and handle navigation, making it easier to manage and maintain your app’s navigation structure. Here are the key features and concepts of AutoRoute

  1. Declarative Routing: Define routes in a centralized manner using annotations.
  2. Nested Navigation: Support for nested routes, making it easier to manage sub-navigation within different parts of the app.
  3. Strongly Typed Arguments: Pass arguments between routes in a type-safe way.
  4. Auto-generated Code: Leverage code generation to reduce boilerplate and errors.
  5. Guarded Routes: Implement route guards to protect certain routes based on conditions (e.g., authentication).
  6. Adaptive Navigation: Support for both Material and Cupertino style navigation.
  7. Custom transition animations: Supports for custom PageRouteBuilder

Lets quickly outline the user scenarios for our demo app. (see in Setup)

  • root -> home
  • root -> home -> pre-recording dialog
  • root -> about us

To achieve this behavior we should create widgets and mark them with auto_route annotations. Lets create our Pages first

// lib/features/home/home_page.dart

@RoutePage()
class HomePage extends StatelessWidget {
const HomePage({super.key});

@override
Widget build(BuildContext context) {
return StoreConnector(
//
// AppStore is injected into ViewModelMapper
converter: (AppStore store) => getIt<HomePageViewModelMapper>().map(),
builder: (context, model) {
return Scaffold(
drawer: const ApplicationDrawer(),
appBar: const ApplicationBar(),
resizeToAvoidBottomInset: true,
body: Container(
color: context.colorScheme.primary,
child:. ...
),
);
},
);
}
}
// lib/features/home/about_us_page.dart

@RoutePage()
class AboutUsPage extends StatelessWidget {
const AboutUsPage({super.key});

@override
Widget build(BuildContext context) {
return StoreConnector(
converter: (AppStore store) => getIt<AboutUsPageViewModelMapper>().map(),
builder: (context, store) {
return Scaffold(
drawer: const ApplicationDrawer(),
appBar: const ApplicationBar(),
body: ...
);
},
);
}
}
// lib/features/home/pre_recording_dialog_page.dart

@RoutePage()
class PreRecordingDialogPage extends StatelessWidget {
const PreRecordingDialogPage({super.key});

@override
Widget build(BuildContext context) {
return StoreConnector(
converter: (AppStore store) => getIt<PreRecordingDialogPageMapper>().map();
builder: (context, model) {
...
}
}
}

Run generator.sh, then we can create a root tab

// lib/features/tab_bar/auto_tabs_router_key.dart

import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart';

final GlobalKey<AutoTabsRouterState> tabsRouterKey = GlobalKey<AutoTabsRouterState>();
// lib/features/tab_bar/tab_bar_page.dart

@RoutePage()
class TabBarPage extends StatelessWidget {
const TabBarPage({super.key});

@override
Widget build(BuildContext context) {
return AutoTabsRouter(
routes: const [
HomeRoute(),
AboutUsRoute(),
],
transitionBuilder: (context, child, animation) {
return child;
},
builder: (context, child) {
return Scaffold(
key: tabsRouterKey,
body: child,
);
},
);
}
}

Run generator.sh again, and declare users flows in a way auto_route designed:

// lib/routing/app_router.dart

import 'package:auto_route/auto_route.dart';
import 'package:record_app/routing/dialog_router.dart';
// <> important <>
import 'package:record_app/routing/app_router.gr.dart';

// For widgets annotated with @RouterPage().
// eg @RouterPage() class TabBarPage extends StatelessWidget { ..
// auto_route generates TabBarRoute as PageRouteInfo model
//
// see how to pass data and more in package doc
@AutoRouterConfig(replaceInRouteName: 'Page,Route')
class AppRouter extends RootStackRouter {
@override
List<AutoRoute> get routes => [
AdaptiveRoute(
page: TabBarRoute.page,
initial: true,
children: [
AdaptiveRoute(
page: HomeTabRootRoute.page,
children: [
AdaptiveRoute(page: HomeRoute.page),
DialogRouter(page: PreRecordingDialogRoute.page),
],
),
AdaptiveRoute(
page: AboutUsTabRootRoute.page,
children: [
AdaptiveRoute(page: AboutUsRoute.page),
],
),
],
),
];
// lib/routing/dialog_router.dart

class DialogRouter extends CustomRoute {
DialogRouter({required super.page})
: super(
transitionsBuilder: TransitionsBuilders.fadeIn,
durationInMilliseconds: 270,
customRouteBuilder: _dialogRouteBuilder,
);
}

CustomRouteBuilder _dialogRouteBuilder = <T>(
BuildContext context,
Widget child,
AutoRoutePage<T> page,
) {
return PageRouteBuilder<T>(
opaque: false,
settings: page,
barrierColor: Colors.black54,
pageBuilder: (context, animation, secondaryAnimation) => child,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final tween = Tween(begin: 0.0, end: 1.0).chain(CurveTween(curve: Curves.ease));
return FadeTransition(
opacity: animation.drive(tween),
child: child,
);
},
);
};

Injection

// lib/DI/app_router_inject.dart

@module
abstract class AppRouterInject {
@singleton
AppRouter appRouter() => AppRouter();
}

Update you application:

MaterialApp.router(
theme: themeDataFun(),
routerConfig: getIt<AppRouter>().config(
navigatorObservers: () => [
AutoRouteObserver(),
],
),
);

Great, all preparations are done, lets see how can we use it!

Im going to create a Redux actions for tab switching and pushing screens back and forward.

// lib/redux/navigation/navigation_action.dart

class SelectTabAction {
final TabId tab;
BuildContext context;
SelectTabAction(this.tab, this.context);
}

class NavigateBackAction {
BuildContext context;
NavigateBackAction(this.context);
}

class NavigatePushAction {
final BuildContext context;
final PageRouteInfo routeInfo;

NavigatePushAction(
this.context,
this.routeInfo,
);
}

class NavigateReplaceAction {
final BuildContext context;
final PageRouteInfo routeInfo;

NavigateReplaceAction(
this.context,
this.routeInfo,
);
}
// lib/redux/navigation/navigation_middleware.dart

List<Middleware<AppState>> navigationMiddleware() {
return [
_selectTab.call,
_navigatingPush.call,
_navigatingReplace.call,
_navigatingBack.call,
];
}

final _selectTab = TypedMiddleware<AppState, SelectTabAction>(
(store, action, next) {
final tabsContext = tabsRouterKey.currentContext;
if (tabsContext != null) {
final tabsRouter = AutoTabsRouter.of(tabsContext);
tabsRouter.setActiveIndex(action.tab.index);
}
next(action);
},
);

final _navigatePush = TypedMiddleware<AppState, NavigatePushAction>(
(store, action, next) {
final router = AutoRouter.of(action.context);
router.push(action.routeInfo);
next(action);
},
);

final _navigateReplace = TypedMiddleware<AppState, NavigateReplaceAction>(
(store, action, next) {
final router = AutoRouter.of(action.context);
router.replace(action.routeInfo);
next(action);
},
);

final _navigateBack = TypedMiddleware<AppState, NavigateBackAction>(
(store, action, next) {
final router = AutoRouter.of(action.context);
router.maybePop();
next(action);
},
);

Now all you have to do for navigation is:

store.dispatch(
NavigatePushAction(
context,
PreRecordingDialogRoute(),
),
);

Congratulations! By embracing AutoRoute, you’re not just enhancing your development workflow but also ensuring a smoother and more maintainable navigation structure for your Flutter applications. Enjoy your navigation experience!

--

--