Flutter navigation is a stack of routes. The Navigator pushes screens on and pops them off.
Navigator manages the stack. MaterialApp already provides one.
MaterialApp(home: const HomePage(),)
You talk to it through context:
Navigator.of(context).push(...);Navigator.of(context).pop();
Navigator.push / Navigator.pop are shortcuts for the same calls.
A Route is one entry on the stack — usually a full screen.
MaterialPageRoute(builder: (context) => const DetailsPage(),)
MaterialPageRoute → Material slide transitionCupertinoPageRoute → iOS transitionPageRouteBuilder → custom animationDialogRoute / ModalBottomSheetRoute → overlays, still routesNamed routes map a string to a screen:
MaterialApp(initialRoute: "/",routes: {"/": (context) => const HomePage(),"/details": (context) => const DetailsPage(),},)
push adds a route on top. The previous screen stays in memory underneath.
Navigator.push(context,MaterialPageRoute(builder: (context) => const DetailsPage(),),);
Named version:
Navigator.pushNamed(context, "/details");
Other useful methods:
pushReplacement → swap the current routepushAndRemoveUntil → push and clear routes belowpushNamedAndRemoveUntil → same, with a namepop removes the top route and returns to the one below.
Navigator.pop(context);
Return a result to the previous screen:
// details pageNavigator.pop(context, "saved");// home pagefinal result = await Navigator.push<String>(context,MaterialPageRoute(builder: (context) => const DetailsPage()),);print(result); // "saved"
maybePop pops only if the navigator can pop. canPop checks first.
Pass data into a screen when you push it.
Constructor (preferred for type safety):
Navigator.push(context,MaterialPageRoute(builder: (context) => DetailsPage(id: 42),),);class DetailsPage extends StatelessWidget {final int id;const DetailsPage({super.key, required this.id});// ...}
Named routes use arguments:
Navigator.pushNamed(context,"/details",arguments: 42,);// on the details pagefinal id = ModalRoute.of(context)!.settings.arguments as int;
Or read them in onGenerateRoute:
onGenerateRoute: (settings) {if (settings.name == "/details") {final id = settings.arguments as int;return MaterialPageRoute(builder: (context) => DetailsPage(id: id),);}return null;},
Each Navigator has its own stack. Put a second navigator inside a tab or a shell so inner screens do not replace the bottom bar.
class ShopTab extends StatelessWidget {const ShopTab({super.key});@overrideWidget build(BuildContext context) {return Navigator(onGenerateRoute: (settings) {return MaterialPageRoute(builder: (context) => const ProductListPage(),);},);}}
Navigator.of(context) finds the nearest navigatorNavigator.of(context, rootNavigator: true) uses the root one — use this for dialogs and full-screen flowsA GlobalKey<NavigatorState> lets you pop or push a nested stack from outside it.
A deep link opens a screen from a URL, for example myapp://details/42 or https://example.com/details/42.
With the built-in navigator, parse the path in onGenerateRoute:
MaterialApp(initialRoute: "/",onGenerateRoute: (settings) {final uri = Uri.parse(settings.name ?? "/");if (uri.path == "/") {return MaterialPageRoute(builder: (context) => const HomePage());}if (uri.pathSegments.length == 2 && uri.pathSegments[0] == "details") {final id = int.parse(uri.pathSegments[1]);return MaterialPageRoute(builder: (context) => DetailsPage(id: id));}return MaterialPageRoute(builder: (context) => const NotFoundPage());},)
For production apps, go_router maps URLs to screens and handles web, Android, and iOS:
GoRouter(routes: [GoRoute(path: "/", builder: (context, state) => const HomePage()),GoRoute(path: "/details/:id",builder: (context, state) {final id = state.pathParameters["id"]!;return DetailsPage(id: int.parse(id));},),],)
Enable it in AndroidManifest.xml (intent-filter) and iOS (CFBundleURLTypes / associated domains) so the OS forwards the URL to Flutter.