Explore navigations in the flutter

Pravin Palkonda
11 min readJan 11, 2023

--

Navigations are an essential part of the application. As the application contains multiple screens, it is necessary to navigate to another screen that shows different information.

Let's understand navigation by a simple example…

Create two screens and name them as ScreenOne and ScreenTwo.

On screen one, add a TextButton on click of it, it will navigate to screen two.

On screen two, add a TextButton on click of it, it will navigate back to previous screen.

In flutter, we use Navigator for the use of navigation.

Navigator is a widget that helps us to navigate between the routes, here routes mean the pages or screens. Navigator works like a stack for the routes. When any action is performed by the user, the routes are stacked one over the other. When a user performs the back action, the route moves to a previously visited route.

In flutter, we use Navigator.push to move to a new screen. It will add a new screen on top of the home route. Here we need to provide a page route that constructs the route using its builder property.

/// to navigate to new screen
Navigator.push(context,
MaterialPageRoute(builder: (_) => const ScreenTwo()));

We also need to navigate back to the previous screen when we land on to last screen or destination screen. For this purpose, the navigator provides us pop() method, which will remove the top route and move to a previously visited route.

/// navigate back to previous screen
Navigator.pop(context);
screen_one.dart

class ScreenOne extends StatefulWidget {
const ScreenOne({super.key});

@override
State<ScreenOne> createState() => _ScreenOneState();
}

class _ScreenOneState extends State<ScreenOne> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Screen One"),
),
body: Center(
child: TextButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (_) => const ScreenTwo()));
},
child: const Text("GO to screen two")),
),
);
}
}
screen_two.dart

class ScreenTwo extends StatefulWidget {
const ScreenTwo({super.key});

@override
State<ScreenTwo> createState() => _ScreenTwoState();
}

class _ScreenTwoState extends State<ScreenTwo> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Screen Two"),
),
body: Center(
child: TextButton(
onPressed: () {
// to go back to previous screen
Navigator.pop(context);
},
child: const Text("GO back to screen one")),
),
);
}
}

Passing a value from screen one to screen two…

Sometimes we must pass the data from one screen to another to display some information. This can be achieved using navigation. Firstly we need to define the arguments that need to be passed to a new route. So these arguments can be used in the new route.

Let's understand it using an example where we will pass the data from screen one and display it on screen two.

We will display a title and description on screen two and pass these values from screen one.

class ScreenOne extends StatefulWidget {
const ScreenOne({super.key});

@override
State<ScreenOne> createState() => _ScreenOneState();
}

class _ScreenOneState extends State<ScreenOne> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Screen One"),
),
body: Center(
child: TextButton(
onPressed: () {
/// to navigate to screen two
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const ScreenTwo(
/// passing title as string hello
title: "Hello",

/// passing description as string flutter
description: "Flutter",
)));
},
child: const Text("GO to screen two")),
),
);
}
}
class ScreenTwo extends StatefulWidget {
/// declaring the fields for title and description that holds string data
/// we can have any type of object here
/// ? ensures that it can be null else it gives exception
final String? title;
final String? description;
const ScreenTwo(
{super.key,

/// constructor for title
this.title,

/// constructor for description
this.description});

@override
State<ScreenTwo> createState() => _ScreenTwoState();
}

class _ScreenTwoState extends State<ScreenTwo> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Screen Two"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
/// displaying a title which is passed from previous screen
Text(
/// displaying using widget property
widget.title!,

style:
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0),
),

/// displaying a description which is passed fro previous screen
Text(
widget.description!,
style:
const TextStyle(fontWeight: FontWeight.bold, fontSize: 20.0),
),
TextButton(
onPressed: () {
// to go back to previous screen
Navigator.pop(context);
},
child: const Text("GO back to screen one")),
],
),
),
);
}
}
Displaying the text Hello Flutter passed from screen one to screen two.

Return data back to the previous screen…

There may be a situation where we want to send data back to the previous screen. This can be achieved using the Navigator.pop() method.

Let's understand it by example. Here we pass the value from screen two back to screen one and display that data on-screen one.

class ScreenOne extends StatefulWidget {
const ScreenOne({super.key});

@override
State<ScreenOne> createState() => _ScreenOneState();
}

class _ScreenOneState extends State<ScreenOne> {
/// declaring a string variable where we collect returned data
String? returnedData;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Screen One"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
/// displaying a returned data else another string data
Text(
returnedData ?? "I am from screen one...",
style:
const TextStyle(fontWeight: FontWeight.bold, fontSize: 20.0),
),
TextButton(
onPressed: () async {
/// to navigate to screen two
/// Navigator.push return the future that completes after calling
/// So we are using async await
/// Here we collecting the data in variable
returnedData = await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const ScreenTwo(
/// passing title as string hello
title: "Hello",

/// passing description as string flutter
description: "Flutter",
)));

/// we are displaying a data after data is returned,
/// so we need to call setState, state gets changed and new data is displayed
setState(() {});
},
child: const Text("GO to screen two")),
],
),
),
);
}
}
Screen one without returned data.
Screen two from here will return data to screen one.
Screen one with returned data

Navigate with named routes…

In flutter, we can also navigate to screens with named routes. Here we have to define the name of the routes( page or screen ) and use it for navigation purposes. So here, the navigator will help us avoid code duplication if we frequently use the navigation to the same screen.

Here we are just exploring the named routes, that one must be aware of it that named routes are not recommended as per the flutter documentation.

To navigate to a new screen using named routes, we use the function Navigator.pushNamed(). It will work the same as the navigator push method which works like a stack adding new screens to one another.

/// to navigate to new screen using named routes.
Navigator.pushNamed(context, '/route_name');

To navigate back to previous screens, we have to use Navigator.pop() method. It will pop the first screen and navigate back to the previously visited screen.

/// to navigate back to previous back
Navigator.pop(context);

While using named routes we have to define routes in the Material App. Here we have to define the initial route and routes property.

initial route → Here we have to define the initial route which means whenever the application gets started, it will land on that page.

routes → Here we will define all the named routes which are used for navigation.

Note → While using the initial route, the home property should not be defined.

Let's explore named routes with examples. Here we will navigate from screen one to screen two using named routes.

Defining the routes…

main.dart

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

class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Navigation in flutter',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
// home: const ScreenOne(), not need to define home route if we use initial route

/// initial route when application starts it will land to this route
initialRoute: "/",

/// here we will define all routes
routes: {
/// when application starts, it will land to screen one
'/': (context) => const ScreenOne(),

/// '/second' we can use this named route to navigate to screen two.
'/second': (context) => const ScreenTwo()

/// like this we can create numbers of named routes and use them for navigation purpose.
},
);
}
}
screen_one.dart

class ScreenOne extends StatefulWidget {
const ScreenOne({super.key});

@override
State<ScreenOne> createState() => _ScreenOneState();
}

class _ScreenOneState extends State<ScreenOne> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Screen One"),
),
body: Center(
child: TextButton(
onPressed: () {
/// to navigate to ScreenTwo using named route '/second'.
Navigator.pushNamed(context, '/second_screen');
},
child: const Text("GO to screen two")),
),
);
}
}
screen_two.dart

class ScreenTwo extends StatefulWidget {
const ScreenTwo({
super.key,
});

@override
State<ScreenTwo> createState() => _ScreenTwoState();
}

class _ScreenTwoState extends State<ScreenTwo> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Screen Two"),
),
body: Center(
child: TextButton(
onPressed: () {
/// to navigate back to previous screen
Navigator.pop(context);
},
child: const Text("GO back to screen one")),
),
);
}
}

Passing data from one screen to another screen using named routes…

So navigator allows us to navigate to any screen by using named routes. Sometimes we need to send data from one screen to another screen where the data need to be shown. Here we can pass the data by using the argument property which is provided by the Navigator.pushNamed() function. The passed data can be extracted using the ModalRoute.of() method.

Let's explore this with an example where we will pass data from one screen to another screen.

Firstly we need to decide what data we have to pass. Let’s pass a number and a title from screen one to screen two and display this number and title on screen two.

screen_one.dart

class ScreenOne extends StatefulWidget {
final String? title;
final int? number;
const ScreenOne({super.key, this.title, this.number});

@override
State<ScreenOne> createState() => _ScreenOneState();
}

class _ScreenOneState extends State<ScreenOne> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Screen One"),
),
body: Center(
child: TextButton(
onPressed: () {
/// to navigate to ScreenTwo using named route '/second'.
/// passing the data to screen two when user clicks on button
/// we can pass the data of any type
Navigator.pushNamed(context, '/second_screen',
arguments: const ScreenOne(
number: 1,
title: "Hello I am flutter",
));
},
child: const Text("GO to screen two")),
),
);
}
}
screen_two.dart

class ScreenTwo extends StatefulWidget {
const ScreenTwo({
super.key,
});

@override
State<ScreenTwo> createState() => _ScreenTwoState();
}

class _ScreenTwoState extends State<ScreenTwo> {
@override
Widget build(BuildContext context) {
/// extracting th passed arguments using ModalRoute setting and cast them as screen one
final args = ModalRoute.of(context)!.settings.arguments as ScreenOne;
return Scaffold(
appBar: AppBar(
title: const Text("Screen Two"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
/// displaying a number which is passed from screen one
Text(
args.number.toString(),
style:
const TextStyle(fontWeight: FontWeight.bold, fontSize: 20.0),
),

/// displaying a title which is passed from screen one
Text(
args.title ?? "", // for null safety
style:
const TextStyle(fontWeight: FontWeight.bold, fontSize: 20.0),
),
TextButton(
onPressed: () {
/// navigate back to previous screen
Navigator.pop(context);
},
child: const Text("GO back to screen one")),
],
),
),
);
}
}
screen one with text button
screen two with passed arguments and text button.

Work with onGenerateRoute with named routes…

onGenerateRoute is the cleanest way to maintain the routes of the application, especially if the application has lots of routes.

Let's understand it by example. Firstly we create a separate file where we manage all the routes of the application. This file will maintain separate logic for the navigation between the screens using named routes.

Create a separate file and name it application routes. In this file create a class as follows,

import 'package:flutter/material.dart';
import 'package:navigations/navigations/screen_one.dart';
import 'package:navigations/navigations/screen_two.dart';

class ApplicationRoutes {
/// creating name of the routes
static const firstScreen = "/first_screen";
static const secondScreen = "/first_screen";

/// generating routes based on the route name created
Route<dynamic> generateRoute(RouteSettings settings) {
/// route setting provides us two properties name and argumnets
/// name decides which view to be displayed
/// arguments are used to pass the data between routes
switch (settings.name) {
case firstScreen:
return MaterialPageRoute(builder: (_) => const ScreenOne());
case secondScreen:
return MaterialPageRoute(builder: (_) => const ScreenTwo());

default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(
child: Text('No route defined for ${settings.name}')),
));
}
}
}

Now in the material app define the initial route and onGenerateRoute property.

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

class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Navigation in flutter',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
// home: const ScreenOne(), not need to define home route if we use initial route

/// initial route when application starts it will land to this route
initialRoute: ApplicationRoutes.firstScreen,

/// defining the onGenerateRoute
onGenerateRoute: ApplicationRoutes.generateRoute,
);
}
}
screen_one.dart

class ScreenOne extends StatefulWidget {
final String? title;
final int? number;
const ScreenOne({super.key, this.title, this.number});

@override
State<ScreenOne> createState() => _ScreenOneState();
}

class _ScreenOneState extends State<ScreenOne> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Screen One"),
),
body: Center(
child: TextButton(
onPressed: () {
/// navigate to second screen
Navigator.pushNamed(
context,

/// name of the route which os define in application routes file
ApplicationRoutes.secondScreen,
);
},
child: const Text("GO to screen two")),
),
);
}
}
screen_two.dart

class ScreenTwo extends StatefulWidget {
const ScreenTwo({
super.key,
});

@override
State<ScreenTwo> createState() => _ScreenTwoState();
}

class _ScreenTwoState extends State<ScreenTwo> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Screen Two"),
),
body: Center(
child: TextButton(
onPressed: () {
/// navigate back to previous screen
Navigator.pop(context);
},
child: const Text("GO back to screen one")),
),
);
}
}

Passing arguments to the new screen.

Let's pass data from screen one to screen two. For that create a constructor on screen two which will hold the data and display it. Instead of extracting the arguments directly inside the widget, it allows us to extract the arguments inside an onGenerateRoute() function and pass them to a widget.

application_routes.dart

class ApplicationRoutes {
/// creating name of the routes
static const firstScreen = "/first_screen";
static const secondScreen = "/second_screen";

/// generating routes based on the route name created
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case firstScreen:
return MaterialPageRoute(builder: (_) => const ScreenOne());
case secondScreen:

/// data which is passed during the navigation and extracting to pass to widget
var title = settings.arguments as String;

return MaterialPageRoute(
builder: (_) => ScreenTwo(
title: title,
));

default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(
child: Text('No route defined for ${settings.name}')),
));
}
}
}
screen_one.dart

class ScreenOne extends StatefulWidget {
const ScreenOne({super.key});

@override
State<ScreenOne> createState() => _ScreenOneState();
}

class _ScreenOneState extends State<ScreenOne> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Screen One"),
),
body: Center(
child: TextButton(
onPressed: () {
/// navigate to second screen
Navigator.pushNamed(
context,

/// name of the route which os define in application routes file
ApplicationRoutes.secondScreen,

/// passing data in the arguments
arguments: "Hello I am from screen One");
},
child: const Text("GO to screen two")),
),
);
}
}
screen_two.dart

class ScreenTwo extends StatefulWidget {
/// defining a title as string
final String? title;
const ScreenTwo({super.key, this.title});

@override
State<ScreenTwo> createState() => _ScreenTwoState();
}

class _ScreenTwoState extends State<ScreenTwo> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Screen Two"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
/// displaying the passed data here
Text(widget.title.toString()),
TextButton(
onPressed: () {
/// navigate back to previous screen
Navigator.pop(context);
},
child: const Text("GO back to screen one")),
],
),
),
);
}
}
screen two with passed data.

We have also other navigation methods which are useful according to the requirement.

In this article, we have explored the navigation, named route navigation, onGenerateRoute, passing data to a new screen and returning data to the previous screen which is commonly used while developing the application.

Let me know in the comments if I need to correct anything. I will try to improve it.

Clap if you like the article. 👏

--

--