Navigation moves users between screens. Flutter has two approaches: Navigator for simple push/pop navigation, and go_router for URL-based routing with path parameters, bottom navigation, and auth guards. Most apps should use go_router.
This guide assumes you know widgets. If not, start with Widgets. For the full routing guide, see the official Flutter navigation docs.
Navigator
Navigator works like a stack of cards. You push a new screen on top, and pop it off to go back. It is built into Flutter with no extra packages needed.
// Push a new screen onto the stack
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailScreen()),
);
// Pop the current screen to go back
Navigator.pop(context);
// Push a screen and remove everything behind it (e.g., after login)
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => HomeScreen()),
(route) => false,
);Passing Data
Pass data to the next screen through its constructor, just like any other widget.
class DetailScreen extends StatelessWidget {
final String itemId;
const DetailScreen({super.key, required this.itemId});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Item $itemId')),
);
}
}
// Navigate and pass data
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailScreen(itemId: '123'),
),
);Returning Data
A screen can send data back when it pops. The calling screen awaits the result.
// The screen being popped sends a result back
Navigator.pop(context, 'selected_value');
// The calling screen waits for it
final result = await Navigator.push<String>(
context,
MaterialPageRoute(builder: (context) => SelectionScreen()),
);
// result is 'selected_value', or null if the user pressed backgo_router
go_router is a routing package that gives you URL-based navigation, path parameters, query strings, redirects, and bottom navigation with preserved state. It is the recommended approach for any app beyond a couple of screens. Add it to your pubspec.yaml:
dependencies:
go_router: ^14.0.0Setup
Define your routes as a GoRouter object. Each GoRoute maps a URL path to a screen widget.
import 'package:go_router/go_router.dart';
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: '/details/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return DetailScreen(id: id);
},
),
GoRoute(
path: '/settings',
builder: (context, state) => SettingsScreen(),
),
],
);Then use MaterialApp.router instead of the regular MaterialApp to connect the router:
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: router,
);
}
}Navigation
go_router gives you two navigation methods. context.go is declarative — it navigates to a path and rebuilds the entire navigation stack to match. Use it for top-level navigation (switching tabs, going to home after login). context.push is imperative — it adds a screen on top of whatever is already there, so the user can press back. Use it for detail screens.
// Navigate declaratively (replaces the stack)
context.go('/details/123');
// Navigate imperatively (pushes on top, can go back)
context.push('/details/123');
// Go back
context.pop();
// Replace the current screen
context.pushReplacement('/home');Path Parameters
Use :name in the path to capture dynamic segments. Read them from state.pathParameters in the builder. Path parameters are part of the URL itself, like /user/123.
Query Parameters
Query parameters come after a ? in the URL, like /search?q=flutter. Use them for optional filters or search terms.
// Navigate with a query parameter
context.go('/search?q=flutter');
// Read it in the route builder
GoRoute(
path: '/search',
builder: (context, state) {
final query = state.uri.queryParameters['q'] ?? '';
return SearchScreen(query: query);
},
)Passing Objects
For complex data that does not belong in the URL, use extra to pass any Dart object directly.
// Pass an object
context.push('/details', extra: myItem);
// Read it in the builder
GoRoute(
path: '/details',
builder: (context, state) {
final item = state.extra as Item;
return DetailScreen(item: item);
},
)Bottom Navigation
go_router handles bottom navigation with StatefulShellRoute. Each tab is a StatefulShellBranch with its own routes and navigation stack. This means switching tabs preserves scroll position and screen history, instead of resetting everything.
final router = GoRouter(
initialLocation: '/home',
routes: [
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return ScaffoldWithNav(navigationShell: navigationShell);
},
branches: [
StatefulShellBranch(
routes: [
GoRoute(
path: '/home',
builder: (context, state) => HomeScreen(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/search',
builder: (context, state) => SearchScreen(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/profile',
builder: (context, state) => ProfileScreen(),
),
],
),
],
),
],
);The ScaffoldWithNav widget wraps the shell with a NavigationBar. The navigationShellis the current tab's content. Use goBranch to switch tabs.
class ScaffoldWithNav extends StatelessWidget {
final StatefulNavigationShell navigationShell;
const ScaffoldWithNav({super.key, required this.navigationShell});
@override
Widget build(BuildContext context) {
return Scaffold(
body: navigationShell,
bottomNavigationBar: NavigationBar(
selectedIndex: navigationShell.currentIndex,
onDestinationSelected: (index) {
navigationShell.goBranch(index);
},
destinations: [
NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.search), label: 'Search'),
NavigationDestination(icon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
}Redirects
Use redirects to guard routes based on auth state. The redirect function runs before every navigation. Return a path to redirect, or null to allow the navigation to proceed normally.
final router = GoRouter(
redirect: (context, state) {
final isLoggedIn = authService.isLoggedIn;
final isOnLogin = state.matchedLocation == '/login';
// Not logged in and not on login page → send to login
if (!isLoggedIn && !isOnLogin) return '/login';
// Logged in but on login page → send to home
if (isLoggedIn && isOnLogin) return '/home';
// Otherwise, let the navigation happen
return null;
},
routes: [...],
);