Creating an admin dashboard with a custom side menu (drawer) in Flutter can be elegantly achieved using the auto_route package. With AutoTabsRouter, we can manage tabs and nested routing while keeping the code modular and easy to scale.
In this article, we’ll walk through:
- Setting up
AutoRoute - Creating a
SideMenu(custom drawer) - Using
AutoTabsRoute - Nesting routes and using a wrapper screen when needed
Packages Used
dependencies:
auto_route: ^7.2.0
flutter:
sdk: flutter
dev_dependencies:
flutter_lints: ^3.0.2
auto_route_generator:
json_serializable:
Step 1: Define the App Routes
We define our app routes using AutoRoute and nest screens under HomeRoute, including a RouterWrapperRoute for grouping related screens.
@AutoRouterConfig(
replaceInRouteName: 'Screen,Route',
)
class AppRouter extends _$AppRouter {
AppRouter([GlobalKey<NavigatorState>? navigatorKey])
: super(navigatorKey: navigatorKey);
@override
List<AutoRoute> get routes => [
AutoRoute(page: LoginRoute.page, path: '/auth/login'),
AutoRoute(page: ForgetPasswordRoute.page, path: '/forget-password'),
AutoRoute(page: ChangePasswordRoute.page, path: '/change-password'),
AutoRoute(
page: HomeRoute.page,
path: "/home",
initial: true,
children: [
AutoRoute(
path: 'dashboard',
page: DashboardRoute.page,
initial: true,
),
// Wrapped routes (nested)
AutoRoute(
path: '',
page: RouterWrapperRoute.page,
children: [
AutoRoute(path: 'events', page: EventsRoute.page, initial: true),
AutoRoute(path: 'create-event', page: CreateNewEventRoute.page),
],
),
],
),
];
}
Why Use a Wrapper?
Some screens (like EventsRoute and CreateNewEventRoute) are related and should share layout or behavior. So we wrap them inside a common route (RouterWrapperRoute), which gives us better control and a cleaner hierarchy.
Step 2: Building the HomeScreenBody with AutoTabsRouter
We use AutoTabsRouter.pageView to handle switching between routes inside the admin dashboard
class HomeScreenBody extends StatelessWidget {
const HomeScreenBody({super.key});
@override
Widget build(BuildContext context) {
return AutoTabsRouter.pageView(
routes: const [
DashboardRoute(),
EventsRoute(), // Comes from RouterWrapper
],
builder: (context, child, _) {
final tabsRouter = AutoTabsRouter.of(context);
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SideMenu(tabsRouter: tabsRouter), // Our Custom Drawer
Expanded(child: child),
],
);
},
);
}
}
Step 3: The Custom Side Drawer (SideMenu)
class SideMenu extends StatelessWidget {
final TabsRouter tabsRouter;
const SideMenu({super.key, required this.tabsRouter});
@override
Widget build(BuildContext context) {
final currentIndex = tabsRouter.activeIndex;
return Container(
width: 235,
decoration: BoxDecoration(color: context.colors.whiteColor),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.rw, vertical: 16.rh),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Assets.images.ventlyLogoDashboard.image(package: 'design_system', width: 135.rw),
Gap(24.rh),
MenuItem(
title: context.homeDashboard,
icon: Assets.dashboard.dashboard,
onTap: () => tabsRouter.setActiveIndex(0),
isSelected: currentIndex == 0,
),
MenuItem(
title: context.homeEvents,
icon: Assets.dashboard.events,
onTap: () => tabsRouter.setActiveIndex(1),
isSelected: currentIndex == 1,
),
],
),
),
);
}
}
MenuItem Component
class MenuItem extends StatelessWidget {
const MenuItem({
super.key,
required this.title,
required this.icon,
this.trailing,
this.onTap,
this.isSelected = false,
});
final String title;
final SvgGenImage icon;
final VoidCallback? onTap;
final bool isSelected;
final Widget? trailing;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? context.colors.primaryColor : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
AppIcon(icon,
color: isSelected
? context.colors.whiteColor
: context.colors.primaryColor),
Gap(8.rw),
Expanded(
child: AppText.body16TextStyle(
title,
color: isSelected
? context.colors.whiteColor
: context.colors.primaryColor,
),
),
if (trailing != null) trailing!,
],
),
),
);
}
}
Step 4: Adding a Wrapper Screen for Nested Tabs
You can create a screen like RouterWrapperScreen that acts as a layout wrapper for multiple nested router
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
@RoutePage()
class RouterWrapperScreen extends StatelessWidget {
const RouterWrapperScreen({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: AutoRouter(),
);
}
}
This allows nested routes under it to inherit layout or functionality without needing repetition.
Final Thoughts
With AutoTabsRouter, you can create a scalable, clean and nested dashboard layout in Flutter. Whether you're building an admin panel, CMS, or B2B dashboard — combining a custom side menu and auto_route makes routing feel native and seamless.
Want to go further? Add role-based access, dynamic tabs, or responsive drawer behavior.


Top comments (0)