You are currently viewing Navigation in Flutter

Navigation in Flutter

In this article, we’ll be talking about the navigation in flutter. It’s going to be quite a ride, so fasten your seatbelts.

Why use Navigation?

Navigation is the process of switching from one distinct screen/page to another.

Many apps contain several screens for displaying some form of information, for example, an app might have several onboarding screens for first-time users or a list of posts screen and a posts detail screen. We want to achieve a logical flow from screen to screen when some button is tapped or in response to some other event, bringing about a seamless user experience.

Many tools and frameworks today have some provision for implementing navigation built-in. 

Flutter makes the concept of navigation stress-free and easy to implement. This is achieved with the help of the Navigator object, a simple widget that maintains a stack of routes, or a history of visited pages.

Alright, lets Begin…

So we’re going to get our hands dirty with some code. To get started, let’s create a simple flutter project. To do this we could use the terminal to run the following command. 

 flutter create <MY_APP> 

Where <MY_APP> is the name of your project, in my case its navigation_demo.

Open the generated project with your preferred text editor and you should see a main.dart file like so. (Note that all the auto-generated comments have been removed).

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
  return MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
      primarySwatch: Colors.blue,
    ),
    home: LoginScreen(),
  );
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);

final String title;

@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;

void _incrementCounter() {
  setState(() {
    _counter++;
  });
}

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text(widget.title),
    ),
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text(
            'You have pushed the button this many times:',
          ),
          Text(
            '$_counter',
            style: Theme.of(context).textTheme.headline4,
          ),
        ],
      ),
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: _incrementCounter,
      tooltip: 'Increment',
      child: Icon(Icons.add),
    ),
  );
}
}

Once you run this project, you should get the following screen. This will be our home page.

Let’s create a login screen. When users login to our app, they would be redirected to the home page.

 class LoginScreen extends StatefulWidget {
@override
_LoginScreenState createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
String _email = "";
String _password = "";

Widget submit() {
  return MaterialButton(
      onPressed: () {},
      color: Colors.blue,
      textColor: Colors.white,
      child: Text("Login"));
}

Widget emailForm() {
  return TextFormField(
    decoration: InputDecoration(hintText: "email"),
  );
}

Widget passwordForm() {
  return TextFormField(     obscureText: true,
    decoration: InputDecoration(hintText: "Password"),
  );
}


@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      child: Container(
        width: MediaQuery.of(context).size.width * 0.8,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              "Login",
              style: TextStyle(fontSize: 30, color: Colors.blue),
            ),
            emailForm(),
            SizedBox(
              height: 10,
            ),
            passwordForm(),
            SizedBox(
              height: 10,
            ),
            submit()
          ],
        ),
      ),
    ),
  );
}
}

 

Alright, so we’re all done with the setup. We can begin to do some navigation.

In our app, we want to switch to the homepage on user login. To do that, we can add the following block of code to the submit method.

Widget submit() {
return MaterialButton(
    onPressed: () {
      Navigator.push(
          context,
          MaterialPageRoute(
              builder: (context) => MyHomePage(
                    title: "Navigation Demo",
                  )));
    },
    color: Colors.blue,
    textColor: Colors.white,
    child: Text("Login"));
}

Is that all? 

Yes, that’s all the code we need!

Once we tap login, we are immediately navigated to the homepage screen. Easy right!

You might notice some interesting block of code in the sample above

Navigator.push(
          context,
          MaterialPageRoute(
              builder: (context) => MyHomePage(
                    title: "Navigation Demo",
                  )));

The Navigator widget has a push method that explicitly pushes a Route widget to a stack of routes it maintains internally. In the code above, we created a new MaterialPageRoute and pushed it to the top of the route stack using navigator.push.

As our application grows larger, we might want to avoid creating route widgets manually as this can lead to duplication. One way to achieve this is with the help of the pushNamed method in the Navigator. 

It works like this.

Navigator.pushNamed(context, path)

Where path is a string representing the route name of the page we want to navigate to.

One question I know you’re probably asking at this point is 

How exactly does flutter know what route to navigate to based on the path string ?

Well, flutter doesn’t by default, we tell flutter how we want this to happen using one of two ways.

  1. Defining a routes table 
  2. Passing an onGenerateRoute call back to the Material App

1) Defining a routes table.

A routes table is a simple map of strings to WidgetBuilder callbacks, i.e (Map <String, WidgetBuilder Function(BuildContext)>)  that we define to let flutter know the relationships between the paths and the routes. In our app, our routes table would look like this

var routes = {
 "/login": (context) => LoginScreen(),
 "/home": (context) => MyHomePage(
       title: "Navigation Demo",
     )
};

And we can pass it in as an argument to MaterialApp

@override
Widget build(BuildContext context) {
return MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
  routes: routes,
  home: LoginScreen(),
);
}

Notice how the /home path is mapped to the MyHomePage screen and the /login path is mapped to the Login screen. 

Whenever Navigation.push(context, “/home”)  is called the Navigator checks to see if a routes map is defined first, then tries to find out what Route is returned from the widget builder callback and pushes that route onto its stack.

If a route map isn’t defined, it checks to see if an onGenerateRoute callback is passed in.

2) OnGenerateRoute

Rather than define a routes table, we could create an onGenerateRoute callback that would return a route based on a path. The onGenerateRoute method looks like this.

Route onGenerateRoute(RouteSettings settings) {
switch (settings.name) {
  case "/login":
    return MaterialPageRoute(builder: (context) => LoginScreen());
  case "/home":
    return MaterialPageRoute(
        builder: (context) => MyHomePage(
              title: "Navigation Demo",
            ));
}
}

Similar to the routes map we defined earlier, we return a new route based on the path string, Unlike the routes map though, we use a switch statement to check the paths and return a new Route based on the path string, which would be accessed from settings.name.

We could also return a default route using onGenerateRoute, when no valid path was passed in,  a 404 page of sorts.

Route onGenerateRoute(RouteSettings settings) {
switch (settings.name) {
case "/login":
  return MaterialPageRoute(builder: (context) => LoginScreen());
case "/home":
  return MaterialPageRoute(
      builder: (context) => MyHomePage(
            title: "Navigation Demo",
          ));
default:
  return MaterialPageRoute(
      builder: (context) => Container(
            child: Text("404: No route found for path ${settings.name}"),
          ));
}

After defining the onGenerateRoute call back, all we have to do is pass it in as an argument to the MaterialApp widget.

@override
Widget build(BuildContext context) {
  return MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
      primarySwatch: Colors.blue,
    ),
    onGenerateRoute: onGenerateRoute,
    home: LoginScreen(),
  );
}
}

And that’s it, we’ve set up navigation with named arguments.

This method as the name implies pops the current route of the stack.

When the back button is tapped, Navigator.of(context).pop()  is called and we are taken back to our login screen, which is the route underneath our home route.

So that’s it, guys. We have a working navigation flow implemented in our app which can be extended further by simply adding new paths and routes to our onGenerateRoute callBack.

CONCLUSION

Navigation is an important aspect of many applications today and using proper navigation techniques would help our applications scale better and save us future pain. This article only touches the surface but hopefully presents you with the necessary tools to take a deeper dive into flutter navigation.