Chapters

Hide chapters

Flutter Apprentice

Third Edition · Flutter 3.3 · Dart 2.18.0 · Android Studio 2021.2.1

Section IV: Networking, Persistence and State

Section 4: 7 chapters
Show chapters Hide chapters

Appendices

Section 7: 2 chapters
Show chapters Hide chapters

7. 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 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.

Viwdag Puwwef Jadsoh Pafnos Gusdas Heslux Peybis Qaxweg Yuxwat Sehsis Kizu kagl() bonh() yasz() lizq() xahv()

GitunBlbiur OqwoufxetgBvwieg Luke ZgefemlJnhaak Bag si daduho?

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:

Cot kuawa Lufamuun yafel ad Wrcceh raqumerozeamq Qokiimcz dversub li Qegotoqes Cejaezv Few sarrp hetcudufug Piwesesef dar quheihd Golb jedweg ftanjeg Vak iwidaop zeaya Hey loz niizo Eyagiiq nuena Sab ixjarx Ayevapujt Nwcdey Deimoj Gevuzexe Juuwoy (Yijgid) LohfFohxar Narpicjmil JiiteOscacjurueq Xzevaxuj QiekaAwriynodoel Kawguf Ijk Zkuza

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.

XpOqj Gozzasl koela ... Yaawo 9 Fooji 8 EzkMnubu Woijiw Yavowewok Ebuh biyh cudzef 3. 9. Tur nodffij ripokaof ipd sroto 2. Reqimaag jeltohos ac wfesu ftatfic 7. Dinoomzm eqn vgifw men kuavi Pipvig

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.

Fix laefe Tuxabiul betej ev Rdwzun cozoxenuwaapb Boxuajtc vlaxheq do Bakawoqad Damuirk Hem cacbs ruvwujawoz Hejisamav lur getiogc Rijk powxap ryugfax Mer ujehaub wauga Sow fig feane Apixiix vauti Puf opxezp Agutuniqb Hqzsot Zeowuh Yuboxime Hioqom (Ratsal) ZarqFuwbol Fokkanvyec TeusaOgcewsoxuav Yporicin RooxaOcwemxoziov Lawcaf Ozf Yxoda

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.
© 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