Routing and Navigation: Moving Between Screens in Flutter

Karthik Ponnam
6 min readAug 31, 2024

--

Flutter’s navigation allow us to manage the app’s navigation flow effortlessly. Whether we are moving between different screens, passing data, or handling navigation stacks, understanding how navigation works is important for building any multi-screen app.

Flutter uses the Navigator widget to manage routes in our app. As we discussed every new screen (or “route”) in Flutter is just a widget. When navigating between screens, Flutter adds or removes routes from the Navigator stack.

Push, Pop, and Navigation Stacks

At the core of Flutter’s navigation system is the concept of stack-based navigation. Each screen we navigate to is pushed onto the stack, and when we move back, the top-most screen is popped off the stack. Let’s break this down:

  • Push: Adds a new route (screen) onto the stack.
  • Pop: Removes the top route (screen) from the stack and returns to the previous one.

Basic Example of Push and Pop

// Push a new screen
Navigator.push(
context,
MaterialPageRoute(builder: (context) => NewScreen()),
);

// Pop the current screen
Navigator.pop(context);

Named Routes

While we can navigate between screens by directly passing widget constructors (like we did in the example above), but Flutter provides an alternative way to handle navigation that scales better: named routes.

Named routes allow us to assign a name to each screen and reference the name when navigating, which makes it easier to manage complex navigation flows at a single location.

Step 1: Define Routes in MaterialApp

First, lets define our routes in the MaterialApp widget. This maps route names to the widget constructors.

void main() {
runApp(MaterialApp(
initialRoute: '/', // The default route (home)
routes: {
'/': (context) => HomeScreen(),
'/profile': (context) => ProfileScreen(),
'/settings': (context) => SettingsScreen(),
},
));
}

Step 2: Navigate Using Named Routes

Next, we can navigate between screens using the route names like below:

Navigator.pushNamed(context, '/profile'); // Navigate to ProfileScreen
Navigator.pushNamed(context, '/settings'); // Navigate to SettingsScreen

Passing Data Between Screens

In real-world applications, we often need to pass data between screens. There are two primary ways we canachieve this:

  1. Passing Data Directly: If we are using MaterialPageRoute, we can pass data directly via the constructor.
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProfileScreen(username: 'Karthik'),
),
);

2. Passing Data via Named Routes: If we are using named routes, we can pass data using the arguments parameter.

Navigator.pushNamed(
context,
'/profile',
arguments: 'Karthik',
);

In the target screen (ProfileScreen), we can retrieve the data using ModalRoute:

class ProfileScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final String username = ModalRoute.of(context)!.settings.arguments as String;

return Scaffold(
appBar: AppBar(title: Text('Profile')),
body: Center(
child: Text('Hello, $username!'),
),
);
}
}

Example: Building a Multi-Screen App

Let’s build a sample multi-screen app with three screens: Home, Profile, and Settings.

import 'package:flutter/material.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Multi-Screen Navigation',
// Define the initial route
initialRoute: '/',
// Define the routes
routes: {
'/': (context) => HomeScreen(),
'/profile': (context) => ProfileScreen(),
'/settings': (context) => SettingsScreen(),
},
);
}
}

// Home Screen
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
// Navigate to the Profile Screen
Navigator.pushNamed(context, '/profile');
},
child: Text('Go to Profile'),
),
ElevatedButton(
onPressed: () {
// Navigate to the Settings Screen
Navigator.pushNamed(context, '/settings');
},
child: Text('Go to Settings'),
),
],
),
),
);
}
}

// Profile Screen
class ProfileScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Profile'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Profile Screen', style: TextStyle(fontSize: 24)),
ElevatedButton(
onPressed: () {
// Navigate back to the previous screen
Navigator.pop(context);
},
child: Text('Go Back'),
),
],
),
),
);
}
}

// Settings Screen
class SettingsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Settings'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Settings Screen', style: TextStyle(fontSize: 24)),
ElevatedButton(
onPressed: () {
// Navigate back to the previous screen
Navigator.pop(context);
},
child: Text('Go Back'),
),
],
),
),
);
}
}

main.dart Overview:

  • The app starts with MyApp as the root widget.
  • MaterialApp manages the routing and screens of our app, using named routes.
  • We define the initial route as '/' (which is the HomeScreen), and additional routes for the ProfileScreen and SettingsScreen.

HomeScreen:

  • This is the first screen the user sees.
  • Contains buttons that navigate to the ProfileScreen and SettingsScreen using Navigator.pushNamed().

ProfileScreen:

  • Displays a basic text and a button to navigate back to the previous screen using Navigator.pop().

SettingsScreen:

  • Similar to ProfileScreen, it contains a text and a button to go back to the previous screen.

Limitations

Although named routes can handle deep links, the behavior is always the same and can’t be customized. When a new deep link is received by the platform, Flutter pushes a new Route onto the Navigator regardless of where the user currently is.

Flutter also doesn’t support the browser forward button for applications using named routes. For these reasons, we don’t recommend using named routes in most applications.

Using the Router

Flutter applications with advanced navigation and routing requirements (such as a web app that uses direct links to each screen, or an app with multiple Navigator widgets) should use a routing package such as go_router that can parse the route path and configure the Navigator whenever the app receives a new deep link.

To use the Router, switch to the router constructor on MaterialApp or CupertinoApp and provide it with a Router configuration. Routing packages, such as go_router, typically provide a configuration for you. For example:

Adding go_router for More Robust Routing

While named routes are easy to set up, they come with certain limitations, especially in more complex apps:

  • Deep linking always pushes a new route onto the stack, which might not be the desired behavior.
  • Browser forward/back button support is limited in web apps.
  • Named routes are less flexible for custom navigation transitions and complex routing logic.

To address these limitations, the go_router package provides more advanced features like:

  • Better handling of deep links.
  • Browser forward and back button support.
  • A declarative approach to defining routes.

Let’s set up go_router to handle navigation in the same multi-screen app.

Step-by-Step: Using go_router

  1. Add go_router to pubspec.yaml:
  2. First, you need to add the go_router package to your project’s dependencies.
dependencies:
flutter:
sdk: flutter
go_router: ^9.0.0 # Add the latest version of go_router

2. Set up go_router in main.dart:

Now we’ll set up go_router to manage navigation. The approach is declarative, and each route is configured with its path.

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
// Define the GoRouter instance
final GoRouter _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: '/profile',
builder: (context, state) => ProfileScreen(),
),
GoRoute(
path: '/settings',
builder: (context, state) => SettingsScreen(),
),
],
);

@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router, // Use go_router for navigation
title: 'Multi-Screen Navigation with GoRouter',
);
}
}

// Home Screen
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
// Navigate to Profile using GoRouter
context.go('/profile');
},
child: Text('Go to Profile'),
),
ElevatedButton(
onPressed: () {
// Navigate to Settings using GoRouter
context.go('/settings');
},
child: Text('Go to Settings'),
),
],
),
),
);
}
}

// Profile Screen
class ProfileScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Profile'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Profile Screen', style: TextStyle(fontSize: 24)),
ElevatedButton(
onPressed: () {
// Go back to the previous screen
context.pop();
},
child: Text('Go Back'),
),
],
),
),
);
}
}

// Settings Screen
class SettingsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Settings'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Settings Screen', style: TextStyle(fontSize: 24)),
ElevatedButton(
onPressed: () {
// Go back to the previous screen
context.pop();
},
child: Text('Go Back'),
),
],
),
),
);
}
}

How go_router Improves our Experience

  1. Browser Navigation: With go_router, we get better support for the browser’s back and forward buttons, which is crucial for web-based Flutter applications.
  2. Deep Linking: go_router can handle deep links efficiently, making it easier to load specific routes based on a URL without always pushing a new screen onto the navigation stack.
  3. Declarative Routing: Instead of using a switch-case system, go_router lets us declare routes upfront, allowing for a more maintainable and scalable navigation system.
  4. Simplified Syntax: Navigating between screens is as simple as calling context.go('/route') instead of dealing with the complexities of Navigator.pushNamed()

These fundamentals of navigation will give us the backbone of how to use navigations while building the multi-screen Flutter applications, whether we’re building a small app or a large, complex project. Understanding how navigation works is important for providing a smooth and user-friendly navigation experience.

For simple apps, named routes are sufficient. However, as our app grows in complexity — especially if we’re targeting the web or need to handle advanced navigation patterns — go_router provides a more robust solution. It offers us more features like deep linking, browser button support, and a declarative approach for routing that make it ideal for large-scale Flutter applications.

Stay tuned, and happy coding!

--

--

Karthik Ponnam
Karthik Ponnam

Written by Karthik Ponnam

❤️ to Code. Full Stack Developer, Flutter, Android Developer, Web Development, Known Languages Java, Python so on.,

No responses yet