Named Routes
Named Routes
As a Flutter app grows, managing navigation with anonymous route constructors passed directly to Navigator.push() quickly becomes unwieldy. Named routes solve this by giving every screen a string identifier and registering all of them in a central route map inside MaterialApp. Any widget anywhere in the tree can then navigate to a screen by name, keeping the navigation logic decoupled from the widget hierarchy.
go_router build on top of the same concepts introduced here.Registering the Route Map in MaterialApp
You declare every named route once, inside the routes parameter of MaterialApp. The map key is a route name string (by convention starting with /), and the value is a builder function that returns the target widget.
Defining Named Routes in MaterialApp
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
// Centralise every route name as a static constant
// so a typo is caught at compile time, not at runtime.
static const String homeRoute = '/';
static const String profileRoute = '/profile';
static const String settingsRoute = '/settings';
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Named Routes Demo',
initialRoute: MyApp.homeRoute,
routes: {
MyApp.homeRoute: (context) => const HomeScreen(),
MyApp.profileRoute: (context) => const ProfileScreen(),
MyApp.settingsRoute:(context) => const SettingsScreen(),
},
);
}
}
Setting initialRoute tells Flutter which named route to display first. It replaces the older home parameter when you use the routes map.
Navigating with Navigator.pushNamed
Once routes are registered, navigation is a single method call from any widget. Navigator.pushNamed(context, routeName) pushes the named route onto the navigation stack exactly like Navigator.push(), but without importing the target screen class.
Pushing and Replacing Named Routes
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Push — user can pop back to Home
ElevatedButton(
onPressed: () =>
Navigator.pushNamed(context, MyApp.profileRoute),
child: const Text('Go to Profile'),
),
const SizedBox(height: 12),
// pushReplacementNamed — replaces current route (no back button)
ElevatedButton(
onPressed: () =>
Navigator.pushReplacementNamed(
context, MyApp.settingsRoute),
child: const Text('Replace with Settings'),
),
const SizedBox(height: 12),
// pushNamedAndRemoveUntil — clear the whole stack (e.g. after login)
ElevatedButton(
onPressed: () =>
Navigator.pushNamedAndRemoveUntil(
context,
MyApp.homeRoute,
(route) => false, // remove every preceding route
),
child: const Text('Reset to Home'),
),
],
),
),
);
}
}
Navigator.pushNamed over Navigator.push everywhere in the app. This means a refactor — renaming or reorganising a screen — only requires changing the route map in one place, not hunting through every call-site.Passing Typed Arguments via RouteSettings
A common requirement is passing data to the target screen — for example, a user ID or a product object. Named routes support this through the optional arguments parameter of pushNamed. The receiving screen reads the value from ModalRoute.of(context)!.settings.arguments and casts it to the expected type.
Sending and Receiving Route Arguments
// --- Sender side ---
ElevatedButton(
onPressed: () {
Navigator.pushNamed(
context,
MyApp.profileRoute,
arguments: {'userId': 42, 'displayName': 'Edrees'},
);
},
child: const Text('Open Profile'),
),
// --- Receiver side (ProfileScreen) ---
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
// Cast to Map<String, dynamic> — use a named DTO class for bigger payloads
final args =
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
final int userId = args['userId'] as int;
final String displayName = args['displayName'] as String;
return Scaffold(
appBar: AppBar(title: Text('Profile — $displayName')),
body: Center(
child: Text('User ID: $userId'),
),
);
}
}
as Map<String, dynamic> will throw a TypeError at runtime if the caller passes the wrong type or navigates to the route directly from the initialRoute without any arguments. Guard with a null-check (settings.arguments as Map<String, dynamic>?) or use onGenerateRoute for stricter type-safe argument parsing.Using a Named Argument DTO (Best Practice)
For screens that accept several arguments, replacing the raw Map with a dedicated data class (sometimes called a route arguments DTO) makes the contract between screens explicit and catches type errors at compile time.
Route Arguments as a Typed Class
// Define the DTO alongside its screen or in a separate file
class ProfileArgs {
final int userId;
final String displayName;
const ProfileArgs({required this.userId, required this.displayName});
}
// Caller
Navigator.pushNamed(
context,
MyApp.profileRoute,
arguments: const ProfileArgs(userId: 42, displayName: 'Edrees'),
);
// Receiver
final args =
ModalRoute.of(context)!.settings.arguments as ProfileArgs;
Text('Welcome, ${args.displayName}');
Limitations and When to Go Further
- No compile-time route name safety — route strings are still plain
String; a mistyped name causes a runtime error. Use static constants (as shown above) to mitigate this. - No URL synchronisation on web — named routes do not update the browser URL bar in a SEO-friendly way. Use
go_routerfor web targets. - No navigation guards — there is no built-in way to block a route (e.g. redirect unauthenticated users). Use
onGenerateRoutefor imperative guards orgo_router'sredirectfor declarative guards.
Summary
Named routes centralise your app's navigation map in MaterialApp.routes, letting any widget navigate by name with Navigator.pushNamed. Arguments are passed through pushNamed(arguments: ...) and retrieved via ModalRoute.of(context)!.settings.arguments. Using typed DTOs instead of raw maps makes the caller–screen contract explicit and safe. For apps requiring deep links, web URL sync, or authentication guards, these fundamentals directly underpin packages such as go_router.