Chapters

Hide chapters

Flutter Apprentice

Fourth Edition · Flutter 3.16.9 · Dart 3.2.6 · Android Studio 2023.1.1

Section II: Everything’s a Widget

Section 2: 5 chapters
Show chapters Hide chapters

Section IV: Networking, Persistence & State

Section 4: 6 chapters
Show chapters Hide chapters

8. Routes & Navigation
Written by Vincent Ngo

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Navigation, or how users switch between screens, is an important concept to master. Good navigation keeps your app organized and helps users find their way around without getting frustrated.

In the previous chapter, you got a taste of navigation where users tapped on a restaurant to view its menu items as shown below:

But this uses the imperative style of navigation, known as Navigator 1.0. In this chapter, you’ll learn to navigate between screens the declarative way.

You’ll cover the following topics:

  • Overview of Navigator 1.0.
  • Overview of Router API.
  • How to use go_router to handle routes and navigation.

By the end of this chapter, you’ll have everything you need to navigate to different screens!

Note: If you’d like to skip straight to the code, jump to Getting Started. If you’d like to learn the theory first, read on!

Introducing Navigation

If you come from an iOS background, you might be familiar with UINavigationController from UIKit, or NavigationStack from SwiftUI.

In Android, you use Jetpack Navigation to manage various fragments.

In Flutter, you use a Navigator widget to manage your screens or pages. Think of screens and pages as routes.

Note: This chapter uses these terms interchangeably because they all mean the same thing.

A stack is a data structure that manages pages. You insert the elements last-in, first-out (LIFO), and only the element at the top of the stack is visible to the user.

For example, when a user views a list of restaurants, tapping a restaurant pushes RestaurantPage to the top of the stack. Once the user finishes making changes, you pop it off the stack.

Here’s a top-level and a side-level view of the navigation stack:

LoginScreen Home LoginScreen Home RestaurantPage RestaurantPage Pop Push Navigator.push() Home RestaurantPage

Now, it’s time for a quick overview of Navigator 1.0.

Navigator 1.0 Overview

Before Flutter 1.22, you could only shift between screens by issuing direct commands like “show this now” or “remove the current screen and go back to the previous one”. Navigator 1.0 provides a simple set of APIs to navigate between screens. The most common ones are:

Pushing and Popping Routes

To show the user another screen, you need to push a Route onto the Navigator stack using Navigator.push(context). Here’s an example:

bool result = await Navigator.push<bool>(
  context,
  MaterialPageRoute<bool>(
    builder: (BuildContext context) =>RestaurantPage(
      restaurant: restaurants[index],
      cartManager: cartManager,
      ordersManager: orderManager,
    )
  ),
);
Navigator.pop(context);

Navigator 1.0’s Disadvantages

The imperative API may seem natural and easy to use, but, in practice, it’s hard to manage and scale.

Sokcon Fizdef Pekwef Fupbig Qayxap Modgek Gelkiq Xojfoq Rapgiq Jetpim Qepu hubd() zucy() xumx() nasx() xogc()

KoniwMuha AdciafrexwZsguew Kexo LakqieqobtHezo Fiv pe zizuva?

Router API Overview

Flutter 1.22 introduced the Router API, a new declarative API that lets you control your navigation stack completely. Also known as Navigator 2.0, the Router API aims to feel more Flutter-like while solving the pain points of Navigator 1.0. Its main goals include:

Saw diaxa Yayigaet xehed uv Gyfzac pizewocuxioqj Genuunln cxadwug hi Xewuqogut Xaguiyl Kay merww lajpeqigih Juluyucuv viz wikuufy Hoct pozneq svetgov Cic unukuez yuofu Yes yew ruiki Ededoav gaegu Xij opzinn Unaseyikv Wsklov Roepah Koyagapu Qoapet (Kotsov) CegpMiqbem Dizyemtzoj ZuudoEjbedramuas Cgesafar XuiquIrvapsuraad Nepxem Onr Knije

Navigation and Unidirectional Data Flow

As discussed with Navigator 1.0, the imperative API is very basic. It forces you to place push() and pop() functions all over your widget hierarchy which couples all your widgets! To present another screen, you must place callbacks up the widget hierarchy.

YcEtw Bexwonw zeoqi ... Coemo 3 Qaima 6 EysLsale Luavip Viwirupav Ajed vabd xiffid 8. 7. Tuq xalfqim gawolior ilt lmodo 3. Qipifaab yacdifew om mwibu kxiprer 3. Fimuocrr upr hsuqb mub couta Soktiq

Is Declarative Always Better Than Imperative?

You don’t have to migrate or convert your existing code to use the new API if you have an existing project.

Getting Started

Open the starter project in Android Studio. Run flutter pub get and then run the app.

Changes to the Project Files

Before you dive into navigation, there are new files in this starter project to help you out.

What’s New in the Screens Folder

There are new changes in lib/ and lib/screens/:

What’s New in the Models Folder

There are three new model objects in lib/models/.

What’s New in the Components Folder

There is one change in lib/components/.

New Packages

There are three new packages in pubspec.yaml:

url_launcher: ^6.2.1
go_router: ^13.0.1
shared_preferences: ^2.2.2

Looking Over the UI Flow

Here are the first two screens you show the user:

Introducing go_router

The Router API gives you more abstractions and control over your navigation stack. However, the API’s complexity and usability hindered a bit the developer experience.

Jej zause Sukewiig yezos oy Wsyfej junagoheleacm Hiroemzm stocsud fe Zeyoqavem Mipeazc Koz loztv zihnabecom Sogerupih ton gameurc Gorc midtag fbavmaz Qor ofuxaem naeci Qus gid qaayu Owedeef cuuxu Quk unvijw Oqatakusq Fwplom Suanab Mahukozi Kionoj (Vestod) ZosxNiwsun Pazpopbpef JuafiIpfodguluus Jlujoxep NoeduAssetreyiaq Yevwub Onp Cxiwo

Creating the go_router

Within main.dart, add the following import:

import 'package:go_router/go_router.dart';
// 1
late final _router = GoRouter(
  // 2
  initialLocation: '/login',
  // TODO: Add App Redirect
  // 3
  routes: [
    // TODO: Add Login Route
    // TODO: Add Home Route
  ],
  // TODO: Add Error Handler
);

Using Your Router

Next, locate // TODO: Replace with Router. Replace it and the entire return MaterialApp(); code with:

// 1
return MaterialApp.router(
  debugShowCheckedModeBanner: false,
  // 2
  routerConfig: _router,
  // TODO: Add Custom Scroll Behavior
  title: 'Yummy',
  scrollBehavior: CustomScrollBehavior(),
  themeMode: themeMode,
  theme: ThemeData(
    colorSchemeSeed: colorSelected.color,
    useMaterial3: true,
    brightness: Brightness.light,
  ),
  darkTheme: ThemeData(
    colorSchemeSeed: colorSelected.color,
    useMaterial3: true,
    brightness: Brightness.dark,
  ),
);

Adding Screens

With all the infrastructure in place, it’s time to define which screen to display according to the route. But first, check out the current situation.

Setting Up Your Error Handler

You can tweak GoRouter to show a custom error page. It’s common for users to enter the wrong URL path, especially with web apps. Web apps usually show a 404 error screen.

errorPageBuilder: (context, state) {
  return MaterialPage(
    key: state.pageKey,
    child: Scaffold(
      body: Center(
        child: Text(
          state.error.toString(),
        ),
      ),
    ),
  );
},

Adding the Login Route

You’ll start by displaying the Login screen.

GoRoute(
  // 1
  path: '/login',
  // 2
  builder: (context, state) =>
    // 3
    LoginPage(
      // 4
      onLogIn: (Credentials credentials) async {
        // 5
        _auth
          .signIn(credentials.username, credentials.password)
          // 6
          .then((_) => context.go('/${YummyTab.home.value}'));
    })),

Adding the Home Route

Once you log in, you need to navigate to the home route. Locate the comment // TODO: Add Home Route and replace it with the following:

// 1
GoRoute(
  path: '/:tab',
  builder: (context, state) {
    // 2
  return Home(
    //3
    auth: _auth,
    //4
    cartManager: _cartManager,
    //5
    ordersManager: _orderManager,
    //6
    changeTheme: changeThemeMode,
    //7
    changeColor: changeColor,
    //8
    colorSelected: colorSelected,
    //9
    tab: int.tryParse(state.pathParameters['tab'] ?? '') ?? 0);
    },
    // 10
    routes: [
    // TODO: Add Restaurant Route
  ]),

Navigate to the Current Tab

Try clicking on the tab bar items and notice that nothing works. You’ll now add a way to navigate between tabs.

context.go('/$index');
import 'package:go_router/go_router.dart';

Handling Redirects

You redirect when you want your app to go to a different location. GoRouter lets you do this with its redirect handler.

// 1
Future<String?> _appRedirect(
  BuildContext context, GoRouterState state) async {
  // 2
  final loggedIn = await _auth.loggedIn;
  // 3
  final isOnLoginPage = state.matchedLocation == '/login';

  // 4
  // Go to /login if the user is not signed in
  if (!loggedIn) {
    return '/login';
  }
  // 5
  // Go to root if the user is already signed in
  else if (loggedIn && isOnLoginPage) {
    return '/${YummyTab.home.value}';
  }

  // 6
  // no redirect
  return null;
}
redirect: _appRedirect,

Adding the Restaurant Route

When the user taps on a restaurant on the Explore page, the app navigates to a subroute. Locate // TODO: Add Restaurant Route and replace it with:

GoRoute(
  // 1
  path: 'restaurant/:id',
  builder: (context, state) {
    // 2
    final id =
        int.tryParse(state.pathParameters['id'] ?? '') ?? 0;
    // 3
    final restaurant = restaurants[id];
    // 4
    return RestaurantPage(
      restaurant: restaurant,
      cartManager: _cartManager,
      ordersManager: _orderManager,
    );
  }),

Navigate to the Restaurant Page

Open lib/components/restaurant_section.dart, locate the comment // TODO: Navigate to Restaurant and replace it with the following:

context.go('/${YummyTab.home.value}/restaurant/${restaurants[index].id}');
import 'package:go_router/go_router.dart';
import '../constants.dart';

Navigate to the Order Page

Once a user adds items to the cart and submits an order, it would be nice to navigate to the orders tab, so that customers can review the order.

import 'package:go_router/go_router.dart';
import '../constants.dart';
context.pop();
context.go('/${YummyTab.orders.value}');

Handle Log Out

Lastly you’ll work on the logout functionality.

widget.auth.signOut().then((value) => context.go('/login'));

Key Points

  • Navigator 1.0 is useful for quick and simple prototypes, presenting alerts and dialogs.
  • Router API is useful when you need more control when managing the navigation stack.
  • GoRouter is a wrapper around the Router API that makes it easier for developers to build navigation logic.
  • With GoRouter, you navigate to other routes using goNamed() instead of go().
  • Use a router widget to listen to navigation state changes and configure your navigator’s list of pages.
  • If you need to navigate to another page after some state change, handle that with the redirect() handler.
  • You can customize the error page by implementing the errorPageBuilder.

Where to Go From Here?

You’ve now learned how to navigate between screens the declarative way. Instead of calling push() and pop() in different widgets, you use multiple managers to manage your state.

Other Libraries to Check Out

GoRouter is just one of the many libraries trying to make the Router API easier to use. Check them out here:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now