Navigator 1.0 Fundamentals
Navigator 1.0 Fundamentals
Flutter's Navigator 1.0 is an imperative, stack-based navigation system built into the framework. Think of it as a physical stack of screens: each new page you open is pushed on top of the stack, and pressing the back button pops it off to reveal the previous screen. Understanding this mental model is the foundation of all Flutter navigation.
The Navigator widget manages a stack of Route objects. Each Route encapsulates a full-screen (or modal) UI, its animation, and its lifecycle. The most common route type is MaterialPageRoute, which gives the platform-correct slide-in/slide-out transition automatically on Android, and a cupertino-style slide from the right on iOS.
Accessing the Navigator
You access the nearest Navigator in the widget tree through the BuildContext. Flutter provides two convenient static methods:
Navigator.push(context, route)— pushes a new route onto the stack.Navigator.pop(context)— removes the top route from the stack.Navigator.pushNamed(context, routeName)— pushes a named route (requires a route table inMaterialApp).Navigator.pushReplacement(context, route)— replaces the current route without leaving it in the stack (useful for login → home transitions).Navigator.popUntil(context, predicate)— pops routes until the predicate returnstrue.
Pushing a New Screen with MaterialPageRoute
MaterialPageRoute is a PageRoute subclass that builds your widget inside a standard Material page transition. Its required argument is a builder callback that receives a BuildContext and returns the destination widget.
Navigating to a Detail Screen
// First screen
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: ElevatedButton(
onPressed: () {
// Push DetailScreen onto the navigation stack
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DetailScreen(itemId: 42),
),
);
},
child: const Text('Go to Detail'),
),
),
);
}
}
// Second screen
class DetailScreen extends StatelessWidget {
final int itemId;
const DetailScreen({super.key, required this.itemId});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Item $itemId')),
body: Center(
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('Go Back'),
),
),
);
}
}
AppBar widget automatically adds a back button when there is a route below it in the stack — you do not need to add Navigator.pop for the back arrow. The manual pop call is only needed for custom back buttons or programmatic navigation.Returning Data from a Screen
One of the most powerful features of Navigator 1.0 is the ability to return data from a pushed screen. Navigator.push returns a Future that completes when the pushed route is popped. You pass the result to Navigator.pop(context, result).
Passing Data Back to the Caller
// Caller: await the result from the picker screen
Future<void> _openColorPicker(BuildContext context) async {
final String? selectedColor = await Navigator.push<String>(
context,
MaterialPageRoute(
builder: (context) => const ColorPickerScreen(),
),
);
if (selectedColor != null) {
// Use the returned value
print('User picked: $selectedColor');
}
}
// ColorPickerScreen pops with the chosen value
class ColorPickerScreen extends StatelessWidget {
const ColorPickerScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Pick a Color')),
body: ListView(
children: ['Red', 'Green', 'Blue'].map((color) {
return ListTile(
title: Text(color),
onTap: () => Navigator.pop(context, color), // return value
);
}).toList(),
),
);
}
}
The Navigation Stack Lifecycle
Understanding how routes affect widget lifecycles prevents common memory and state bugs:
- When a route is pushed, its widget tree is built and
initStateis called on anyStatefulWidgetin it. - The previous route is kept alive in memory but is obscured; its widgets are not rebuilt while hidden.
- When a route is popped,
disposeis called on allStatefulWidgets in that route — controllers, streams, and listeners must be cleaned up there. - If you want code to run every time a screen becomes visible again (e.g. after returning from a child route), use
RouteObserveror call a refresh function in theFuturereturned byNavigator.push.
Navigator.pop if the current route is the last route in the stack. This will close the entire app on Android or throw an error. Use Navigator.canPop(context) to guard the call when you are unsure.Named Routes (Quick Overview)
For apps with many screens, registering route names in MaterialApp.routes keeps navigation calls clean. Define a routes map and use Navigator.pushNamed:
Named Route Registration and Usage
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomeScreen(),
'/detail': (context) => const DetailScreen(itemId: 0),
'/settings': (context) => const SettingsScreen(),
},
)
// Navigate from anywhere in the app
Navigator.pushNamed(context, '/detail');
Navigator.push with MaterialPageRoute for anonymous routes, Navigator.pushNamed for a cleaner multi-screen setup, and always pop with optional data when a screen needs to return a result to its caller.