Home Flutter & Dart Books Flutter Apprentice

7
Routes & Navigation Written by Vincent Ngo

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

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 when you created a grocery list to help users manage what to buy. When the user taps an item, it shows the item’s details:

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 grocery items, tapping an item pushes GroceryItemScreen 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 OnboardingScreen Home GroceryScreen GroceryItemScreen LoginScreen OnboardingScreen Home GroceryScreen GroceryItemScreen Pop Push Navigator.push() GroceryScreen GroceryItem Screen

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) => OnboardingScreen()
  ),
);
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.

Sencol Hopxat Roccay Kiztit Nuqquc Behgaq Jehkug Yijcuy Hogsoz Gidxiz Duko ruhr() fosp() wonv() nifx() ropn()

HugeqWyyaeg EfsuesqorfMghoeh Vore TsacofrTywoir Hoz vi sanisu?

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, Router API aims to feel more Flutter-like while solving the pain points of Navigator 1.0. Its main goals include:

Dax tiaru Tahaviab riquf oy Ctrzot fidaqenipeacc Hiraakmc tlisnil re Bobuvekew Woxiunn Zec yawzq damtogipum Qoceyumub ces wupiajy Vezn hezzaw clidnoc Yel oxojuax nuoho Yeq wid jeayi Amiduib loijo Vip iqlicd Oputuwuzz Hmzpav Haogaf Kaburupa Ceetef (Zikjij) JofqLufjul Jasbagxpuk TuadoEzcaqvobuuw Ycojojep JuiwuEzhurgotiiw Dorqah Ujk Mwagi

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.

DzAmt Nuqpedz haixe ... Qoace 2 Yuuwi 7 AhgFgexo Haehah Zehapiqet Evuq pifc tapdif 5. 2. Yap ciskxez kufutuen ugc yrabu 8. Jolovuin lupzepup ew qnena mkerlid 7. Yeziuhhk uhl lxipr yex koodu Carfav

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 five new changes in lib/screens/:

Changes to the Models Folder

There are a few changes to files in lib/models/.

Additional Assets

assets/ contains new images, which you’ll use to build the new onboarding guide.

New Packages

There are five new packages in pubspec.yaml:

smooth_page_indicator: ^1.0.0+2
webview_flutter: ^3.0.4
url_launcher: ^6.1.5
shared_preferences: ^2.0.15
go_router: ^4.3.0

Android SDK Version

Open android/app/build.gradle and you’ll notice that the minSdkVersion is now 19, as shown below:

android {
    defaultConfig {
    	...
        minSdkVersion 19
        ...
    }
}

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 the developer experience.

Doy miinu Nefikiet hahak ec Khhwuq pimidicapuurl Fesiayyv lyovdot zo Nasutoqeb Rabaoft Kub vulvp siwkurivit Dugafibar jig cakiapy Sipm wovdeb xyibfav Sib ukumauj youve Dim vep nuupu Ecaziav wairu Vem izxopy Awoyeguqg Yrxqev Cuetot Ciyovace Miofaz (Nixhir) JutwHovtac Qajcangdog KoujoUpsacquqoac Jpefizaz WeihoIjgijjojiat Mewgar Ivs Zxepe

Creating the go_router

Under lib/, create a new directory called navigation. Within that folder, create a new file called app_router.dart. Add:

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

class AppRouter {
  // 1
  final AppStateManager appStateManager;
  // 2
  final ProfileManager profileManager;
  // 3
  final GroceryManager groceryManager;

  AppRouter(
    this.appStateManager,
    this.profileManager,
    this.groceryManager,
  );

  // 4
  late final router = GoRouter(
    // 5
    debugLogDiagnostics: true,
    // 6
    refreshListenable: appStateManager,
    // 7
    initialLocation: '/login',
    // 8
    routes: [
      // TODO: Add Login Route
      // TODO: Add Onboarding Route
      // TODO: Add Home Route
    ],
    // TODO: Add Error Handler
    // TODO: Add Redirect Handler
  );
}

Using Your App Router

The newly created router needs to know who the managers are. So, now you’ll connect it to the state, grocery and profile managers.

import 'navigation/app_router.dart';
late final _appRouter = AppRouter(
  widget.appStateManager,
  _profileManager,
  _groceryManager,
);
final router = _appRouter.router;
return MaterialApp.router(
  theme: theme,
  title: 'Fooderlich',
  routerDelegate: router.routerDelegate,
  routeInformationParser: router.routeInformationParser,
  routeInformationProvider: router.routeInformationProvider,
);
import 'screens/screens.dart';

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 customize GoRouter to show your own 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(
  name: 'login',
  path: '/login',
  builder: (context, state) => const LoginScreen(),
),

import 'package:provider/provider.dart';
import '../models/models.dart';
Provider.of<AppStateManager>(context, listen: false)
  .login('mockUsername', 'mockPassword');

Adding the Onboarding Route

You’ll show the Onboarding screen when the user is logged in.

GoRoute(
  name: 'onboarding',
  path: '/onboarding',
  builder: (context, state) => const OnboardingScreen(),
),

Debugging the Issue

Simply defining a route doesn’t mean GoRouter will navigate to the onboarding screen. There are two options:

Handling Redirects

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

redirect: (state) {
  // 1
  final loggedIn = appStateManager.isLoggedIn;
  // 2
  final loggingIn = state.subloc == '/login';
  // 3
  if (!loggedIn) return loggingIn ? null : '/login';

  // 4
  final isOnboardingComplete = appStateManager.isOnboardingComplete;
  // 5
  final onboarding = state.subloc == '/onboarding';
  // 6
  if (!isOnboardingComplete) {
    return onboarding ? null : '/onboarding';
  }
  // 7
  if (loggingIn || onboarding) return '/${FooderlichTab.explore}';
  // 8
  return null;
},

Handling the Skip Button in Onboarding

You’ll show the home screen when the user taps the Skip button rather than going through the onboarding guide.

import 'package:provider/provider.dart';
import '../models/models.dart';
Provider.of<AppStateManager>(context, listen: false).onboarded();

Transitioning From Onboarding to Home

Return to lib/navigation/app_router.dart. Locate // TODO: Add Home Route and replace it with:

GoRoute(
  name: 'home',
  // 1
  path: '/:tab',
  builder: (context, state) {
    // 2
    final tab = int.tryParse(state.params['tab'] ?? '') ?? 0;
    // 3
    return Home(
      key: state.pageKey, currentTab: tab,
    );
  },
  // 3
  routes: [
    // TODO: Add Item Subroute
    // TODO: Add Profile Subroute
  ],
),

Handling Tab Selection

Open lib/screens/home.dart and add the following imports:

import 'package:provider/provider.dart';
import '../models/models.dart';
import 'package:go_router/go_router.dart';
// 1
Provider.of<AppStateManager>(context, listen: false).goToTab(index);
// 2
context.goNamed(
  'home',
  params: {
    'tab': '$index',
  },
);

Handling the Browse Recipes Button

Now, you want to make tapping Browse Recipes bring the user to the Recipes tab.

import 'package:go_router/go_router.dart';
import '../models/models.dart';
context.goNamed(
  'home',
  params: {
    'tab': '${FooderlichTab.recipes}',
  },
);

Showing the Grocery Item Screen

Next, you’ll connect the Grocery Item screen.

GoRoute(
  name: 'item',
  // 1
  path: 'item/:id',
  builder: (context, state) {
    // 2
    final itemId = state.params['id'] ?? '';
    // 3
    final item = groceryManager.getGroceryItem(itemId);
    // 4
    return GroceryItemScreen(
      originalItem: item,
      onCreate: (item) {
        // 5
        groceryManager.addItem(item);
      },
      onUpdate: (item) {
        // 6
        groceryManager.updateItem(item);
      },
    );
  },
),

Creating a New Grocery Item

Open lib/screens/grocery_screen.dart and add the following import:

import 'package:go_router/go_router.dart';
context.goNamed(
  'item',
  params: {
    'tab': '${FooderlichTab.toBuy}',
    'id': 'new'
  },
);

Navigating Back Home

Open grocery_item_screen.dart and add the following import:

import 'package:go_router/go_router.dart';
context.goNamed(
  'home',
  params: {
    'tab': '${FooderlichTab.toBuy}',
  },
);

Editing an Existing Grocery Item

Open grocery_list_screen.dart and add the following import:

import 'package:go_router/go_router.dart';
// 1
final itemId = manager.getItemId(index);
// 2
context.goNamed(
  'item',
  params: {
    'tab': '${FooderlichTab.toBuy}',
    'id': itemId
  },
);

Navigating to the Profile Screen

Next, you need to set up the profile route. Back in app_router.dart, locate // TODO: Add Profile Subroute and replace it with:

GoRoute(
  name: 'profile',
  // 1
  path: 'profile',
  builder: (context, state) {
    // 2
    final tab = int.tryParse(state.params['tab'] ?? '') ?? 0;
    // 3
    return ProfileScreen(
      user: profileManager.getUser,
      currentTab: tab,
    );
  },
  // 4
  routes: [
    // TODO: Add Webview subroute
  ],
),
context.goNamed(
  'profile',
  params: {
    'tab': '$currentTab',
  },
);

Navigating to raywenderlich.com

In the Profile screen, you can:

Create WebView Subroute.

Return to app_router.dart. Locate // TODO: Add Webview Subroute and replace it with:

GoRoute(
  name: 'rw',
  path: 'rw',
  builder: (context, state) => const WebViewScreen(),
),

Transitioning From Profile to WebView

Open lib/screens/profile_screen.dart and add the following import:

import 'package:go_router/go_router.dart';
context.goNamed(
  'rw',
  params: {'tab': '${widget.currentTab}'},
);

Logging Out

Still in profile_screen.dart, locate // TODO: Logout user. Replace it with:

Provider.of<AppStateManager>(context, listen: false).logout();

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 and organization when managing the navigation stack.
  • GoRouter is a wrapper around the Router API that makes it easier for developers to use.
  • 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 in GoRouter’s redirect handler.
  • You can customize your own 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 state managers to manage your state.

Other Libraries to Check Out

GoRouter is just one of the many libraries trying to make the RouterAPI 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.

© 2022 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.