Flutter Navigation: A Complete Guide to Routing and Navigation

Are you getting started with building mobile applications using the Flutter framework? Congratulations! You are about to embark on an exciting journey that will enable you to create stunning mobile apps with a single codebase for both Android and iOS platforms. One of the key pillars of any mobile application is Navigation. Users expect to be able to move between different screens and flows seamlessly.

In this article, we'll explore Flutter Navigation - a powerful and flexible system for routing and navigation in Flutter. Whether you are building a simple app with a few screens or a complex app with multiple nested navigators, Flutter Navigation has you covered.

Prerequisites

Before diving into Flutter Navigation, you should be familiar with the basics of Flutter and Dart. If you are new to Flutter, we recommend checking out our Getting Started with Flutter article.

Understanding Navigation in Flutter

In Flutter, every screen or page in your app is a widget. Navigation refers to the process of moving from one widget to another.

Flutter Navigation provides us with two core widgets that form the backbone of our navigation system:

Together, the Navigator and Route widgets provide us with a powerful and flexible system for building complex navigation flows in our app.

Basic Navigation

Let's start by looking at the simplest form of navigation in Flutter - navigating between two screens. In this example, we have a simple app with two screens - a home screen and a details screen. When the user taps on a button on the home screen, we want to navigate to the details screen.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Navigation',
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home Screen'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Go to Details'),
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => DetailsScreen()),
            );
          },
        ),
      ),
    );
  }
}

class DetailsScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Details Screen'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Go back'),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
    );
  }
}

In the HomeScreen widget, we create a RaisedButton with a text 'Go to Details'. When the user taps on this button, we call the Navigator.push method. This method takes two arguments - the BuildContext and the Route that we want to push onto the Navigator stack. In this case, we create a new MaterialPageRoute with the DetailsScreen widget as the builder function.

In the DetailsScreen widget, we again create a RaisedButton with a text 'Go back'. When the user taps on this button, we call the Navigator.pop method. This method pops the current Route off the Navigator stack and returns us to the previous screen (in this case, the HomeScreen).

That's it! We have successfully navigated between two screens in our Flutter app.

Named Routes

In the previous example, we used the MaterialPageRoute constructor to create a new Route object. This approach works fine for simple apps with only a few screens. However, as our app grows, the number of screens will also increase, and managing all those Route objects can become a challenge.

Flutter Navigation provides us with a better way to handle navigation in our app through the use of Named Routes. Each screen in our app is assigned a unique name, and we can navigate between screens by using these names instead of creating new Route objects manually.

Let's rewrite our previous example using Named Routes.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Navigation',
      initialRoute: '/',
      routes: {
        '/': (context) => HomeScreen(),
        '/details': (context) => DetailsScreen(),
      },
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home Screen'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Go to Details'),
          onPressed: () {
            Navigator.pushNamed(context, '/details');
          },
        ),
      ),
    );
  }
}

class DetailsScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Details Screen'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Go back'),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
    );
  }
}

In the MyApp widget, we use the MaterialApp widget and define our initialRoute as '/' - this is the default route for our app. We also define our routes as a Map, with each key representing a unique route name and each value representing the widget (or builder function) for that route.

In the HomeScreen widget, we use the Navigator.pushNamed method to navigate to the '/details' route. Note that we don't need to create a new MaterialPageRoute object here - Flutter Navigation handles that for us.

In the DetailsScreen widget, we use the Navigator.pop method to return to the previous screen.

Named Routes make it easier to manage navigation in our app, especially as the number of screens increases. We can also pass parameters between screens using Named Routes by adding arguments to the pushNamed method.

Passing Data between Screens

In many cases, we need to pass data from one screen to another - for example, when the user taps on an item in a list, we want to display the details of that item on a new screen.

Flutter Navigation provides us with several ways to pass data between screens:

Pass data as constructor arguments

One way to pass data between screens is to include the data as constructor arguments when creating the Route object.

class ItemListScreen extends StatelessWidget {
  final List<Item> items;

  ItemListScreen({this.items});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Item List'),
      ),
      body: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(items[index].title),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => ItemDetailsScreen(item: items[index]),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

class ItemDetailsScreen extends StatelessWidget {
  final Item item;

  ItemDetailsScreen({this.item});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Item Details'),
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(item.title, style: TextStyle(fontSize: 24)),
          Text(item.description),
        ],
      ),
    );
  }
}

In the ItemListScreen widget, we define a List of items and pass it as a constructor argument. When the user taps on an item in the list, we navigate to the ItemDetailsScreen and pass the selected item as a constructor argument to the Route.

In the ItemDetailsScreen widget, we define an Item object as a constructor argument and use it to display the details of the selected item.

Pass data using ModalRoute

Another way to pass data between screens is to use the ModalRoute class.

class ItemListScreen extends StatelessWidget {
  final List<Item> items;

  ItemListScreen({this.items});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Item List'),
      ),
      body: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(items[index].title),
            onTap: () async {
              final result = await Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => ItemDetailsScreen()),
              );
              Scaffold.of(context).showSnackBar(
                SnackBar(
                  content: Text('Result: $result'),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

class ItemDetailsScreen extends StatelessWidget {
  final TextEditingController textController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Item Details'),
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          TextField(controller: textController),
          RaisedButton(
            child: Text('Save'),
            onPressed: () {
              Navigator.pop(context, textController.text);
            },
          ),
        ],
      ),
    );
  }
}

In the ItemListScreen widget, when the user taps on an item in the list, we use the Navigator.push method to navigate to the ItemDetailsScreen. Instead of passing the item as a constructor argument, we use the await keyword to wait for a result from the ItemDetailsScreen.

In the ItemDetailsScreen widget, we create a TextEditingController to capture the user input, and a RaisedButton to save the input and return it to the calling screen. We use the Navigator.pop method to pass the result back to the ItemListScreen.

Nested Navigation

In some apps, we need to organize our screens into nested navigation structures - for example, a tab bar with different screens for each tab, or a drawer with different screens for each item.

Flutter Navigation provides us with the TabBar and Drawer widgets, each of which includes a built-in navigation system. We can also create nested Navigators ourselves, by using the Navigator widget within other widgets.

Let's explore a few examples of nested navigation in Flutter:

Tabbed Navigation

In this example, we'll create a simple tab bar with two tabs - one for displaying a list of items and one for displaying a profile screen.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Navigation',
      home: MainScreen(),
    );
  }
}

class MainScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          title: Text('Tabbed Navigation'),
          bottom: TabBar(
            tabs: [
              Tab(icon: Icon(Icons.list)),
              Tab(icon: Icon(Icons.person)),
            ],
          ),
        ),
        body: TabBarView(
          children: [
            ItemListScreen(),
            ProfileScreen(),
          ],
        ),
      ),
    );
  }
}

class ItemListScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text('Item List Screen'),
      ),
    );
  }
}

class ProfileScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text('Profile Screen'),
      ),
    );
  }
}

In the MainScreen widget, we use the DefaultTabController widget to create a tab bar with two tabs. We define the length of the tab bar (in this case, 2), and the child widget for the tab bar (in this case, a Scaffold with an AppBar and TabBarView).

In the ItemListScreen and ProfileScreen widgets, we define simple screens for displaying text.

Drawer Navigation

Here, we'll create a navigation drawer with three items - one for displaying a list of items, one for displaying a favorites screen, and one for displaying settings.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Navigation',
      home: MainScreen(),
    );
  }
}

class MainScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Drawer Navigation'),
      ),
      drawer: Drawer(
        child: ListView(
          padding: EdgeInsets.zero,
          children: [
            DrawerHeader(
              child: Text('Drawer Header'),
              decoration: BoxDecoration(
                color: Colors.blue,
              ),
            ),
            ListTile(
              leading: Icon(Icons.list),
              title: Text('Item List'),
              onTap: () {
                Navigator.pop(context);
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => ItemListScreen()),
                );
              },
            ),
            ListTile(
              leading: Icon(Icons.favorite),
              title: Text('Favorites'),
              onTap: () {
                Navigator.pop(context);
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => FavoritesScreen()),
                );
              },
            ),
            ListTile(
              leading: Icon(Icons.settings),
              title: Text('Settings'),
              onTap: () {
                Navigator.pop(context);
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => SettingsScreen()),
                );
              },
            ),
          ],
        ),
      ),
      body: Center(
        child: Text('Main Screen'),
      ),
    );
  }
}

class ItemListScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Item List'),
      ),
      body: Center(
        child: Text('Item List Screen'),
      ),
    );
  }
}

class FavoritesScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Favorites'),
      ),
      body: Center(
        child: Text('Favorites Screen'),
      ),
    );
  }
}

class SettingsScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Settings'),
      ),
      body: Center(
        child: Text('Settings Screen'),
      ),
    );
  }
}

In the MainScreen widget, we define a Scaffold with an AppBar and a Drawer. In the Drawer, we define a ListView with three ListTile widgets - one for each screen we want to navigate to.

In each ListTile, we define an onTap method that pops the Drawer and then navigates to the desired screen using the Navigator.push method.

In the ItemListScreen, FavoritesScreen, and SettingsScreen widgets, we define simple screens for displaying text.

Conclusion

Flutter Navigation is a powerful and flexible system for building navigation flows in your Flutter apps. Whether you are building a simple app with a few screens or a complex app with nested navigation structures, Flutter Navigation has you covered.

In this article, we covered the basics of Flutter Navigation, named routes, passing data between screens, and nested navigation structures. Use these tips and tricks to build stunning and intuitive navigation flows in your Flutter apps.

Happy coding!

Editor Recommended Sites

AI and Tech News
Best Online AI Courses
Classic Writing Analysis
Tears of the Kingdom Roleplay
ML SQL: Machine Learning from SQL like in Bigquery SQL and PostgresML. SQL generative large language model generation
Crypto Lending - Defi lending & Lending Accounting: Crypto lending options with the highest yield on alts
Roleplay Community: Wiki and discussion board for all who love roleplaying
Macro stock analysis: Macroeconomic tracking of PMIs, Fed hikes, CPI / Core CPI, initial claims, loan officers survey
Cloud Actions - Learn Cloud actions & Cloud action Examples: Learn and get examples for Cloud Actions