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
- Declarative Routing: Define routes in a centralized manner using annotations.
- Nested Navigation: Support for nested routes, making it easier to manage sub-navigation within different parts of the app.
- Strongly Typed Arguments: Pass arguments between routes in a type-safe way.
- Auto-generated Code: Leverage code generation to reduce boilerplate and errors.
- Guarded Routes: Implement route guards to protect certain routes based on conditions (e.g., authentication).
- Adaptive Navigation: Support for both Material and Cupertino style navigation.
- 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!