Chapters

Hide chapters

Real-World Flutter by Tutorials

First Edition · Flutter 3.3 · Dart 2.18 · Android Studio or VS Code

Real-World Flutter by Tutorials

Section 1: 16 chapters
Show chapters Hide chapters

7. Routing & Navigating
Written by Edson Bueno

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

Flutter has two routing mechanisms:

  • Navigator 1: Imperative style
  • Navigator 2: Declarative style

Nav 1, which you’re probably most familiar with, is the oldest. It has a straightforward API and is very easy to understand. You can use Nav 1 in three different ways:

  1. Anonymous routes
  • When creating the app widget:

    return MaterialApp(
      home: QuotesListScreen(),
    );
    
  • When pushing a new route:

    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => QuoteDetailsScreen(
          id: id,
        ),
      ),
    );
    
  1. Simple named routes
  • When creating the app widget:

    return MaterialApp(
      initialRoute: '/quotes',
      routes: {
        '/quotes': (context) => QuotesListScreen(),
        '/quotes/details': (context) => QuoteDetailsScreen(
              id: ModalRoute.of(context)?.settings.arguments as int,
            ),
      },
    );
    
  • When pushing a new route:

    Navigator.pushNamed(
      context,
      '/quotes/details',
      arguments: 71, // The quote ID.
    );
    
  1. Advanced named routes
  • When creating the app widget:

    return MaterialApp(
      initialRoute: '/quotes',
      onGenerateRoute: (settings) {
        final routeName = settings.name;
        if (routeName == '/') {
          return MaterialPageRoute(
            builder: (context) => QuotesListScreen(),
          );
        }
    
        if (routeName != null) {
          final uri = Uri.parse(routeName);
          if (uri.pathSegments.length == 2 &&
              uri.pathSegments.first == 'quotes') {
            final id = uri.pathSegments[1] as int;
            return MaterialPageRoute(
              builder: (context) => QuoteDetailsScreen(
                id: id,
              ),
            );
          }
        }
    
        return MaterialPageRoute(
          builder: (context) => UnknownScreen(),
        );
      },
    );
    
  • When pushing a new route:

    Navigator.pushNamed(
      context,
      '/quotes/71',
    );
    

Each of these has its pros and cons:

  • Anonymous routes are the easiest to learn but can give you a hard time when trying to reuse code — if two places in the app can open the same screen, for example.
  • Simple named routes solve the code reuse issue but still have the flaw of not allowing you to parse arguments from the route name. For example, if the app runs on the web, you can’t extract the quote ID from a link like /quotes/73.
  • Lastly, advanced named routes let you parse arguments from the route name but aren’t as easy to learn as their siblings.

As you can see, Navigator 1 has alternatives for all tastes. Why, then, did they have to come up with a Navigator 2?

Nav 1 — and all its variants — has a foundational flaw: It’s very hard to push or pop multiple pages at once, which is terrible for deep links or Flutter Web in general.

Deep linking is the ability to send the user a link — the deep link — that, when opened on a smartphone, launches a specific screen within the app instead of opening a web page. Pay attention to the fact that the link doesn’t simply launch the app; it launches a specific screen within the app — hence the “deep” in the name.

Deep links can be helpful to allow users to share links or enable your app’s notifications to take the user to particular content when tapped. You’ll leave the actual deep link implementation for the next chapter. The vital thing to have in mind now is: A solid routing strategy must be good at deep linking. Here enters Navigator 2.

Nav 2 completely nails any of the issues you can think of for Nav 1, but it comes with a cost: It’s dang hard to learn and use. Fortunately for you, that’s an easy problem to solve: The community has developed a plethora of packages that wrap over Nav 2 and make it easy to use. In this chapter, you’ll learn how to use Routemaster, a package that makes Nav 2 as straightforward as simple named routes. Along the way, you’ll also learn how to:

  • Quickly identify what the routing strategy of a codebase is.
  • Switch from Nav 1 to Nav 2.
  • Support nested navigation for tabs.
  • Manage and inject app-wide dependencies.
  • Connect your feature packages without coupling them.

Throughout this chapter, you’ll work on the starter project from this chapter’s assets folder.

Getting Started

Use your IDE of choice to open the starter project. Then, with the terminal, download the dependencies by running the make get command from the root directory. Wait for the command to finish executing, then build and run your app. For now, expect to see nothing but a giant X on the screen:

Note: If you’re having trouble running the app, it’s because you forgot to propagate the configurations you did in the first chapter’s starter project to the following chapters’ materials. If that’s the case, please revisit Chapter 1, “Setting up Your Environment”.

You can tell what routing strategy an app uses just by looking at how it instantiates MaterialApp:

  • Anonymous routes: Characterized by MaterialApp(home:).
  • Simple named routes: Characterized by MaterialApp(routes:).
  • Advanced named routes: Characterized by MaterialApp(onGenerateRoute:).
  • Navigator 2: Characterized by MaterialApp.router(routerDelegate:, routeInformationParser:).

Open the starter project’s lib/main.dart file. You’ll see that the code is currently using the anonymous route form to set a Placeholder widget as the app’s home screen — which explains the X you’re seeing. Starting with the next section, you’ll work on migrating your app to Navigator 2 and using it to display and connect all the screens you have in your feature packages.

Switching to Navigator 2

As you can see in the diagram below, Navigator 2 has quite a few moving parts:

lep doura zolezoib lemen us Zxgyop hesuwejugiick ceqeokgf fnapteg ze Rohulaluj yijoadw Ekkied gutln yikgibomuy Sileyewuh jom duqoigt yuxb gewyey trutkaw noq idinuor ziehi pim vid zaezi Opahaih woote Ton odnajh Uletolohl Zmwhay yiopemZimedoje Qievaj (Lowneb) lishGakpoyFigjochqik ziowaAklihsexaajJhopiwof seotiExlahrupiumJarjaf Etc Vzala

// 1
late final _routerDelegate = RoutemasterDelegate(
  // 2
  routesBuilder: (context) {
    return RouteMap(
      routes: {
        // 3
        '/': (_) => const MaterialPage(
          child: Placeholder(),
        ),
      },
    );
  },
);
child: MaterialApp(
child: MaterialApp.router(
routeInformationParser: const RoutemasterParser(),
routerDelegate: _routerDelegate,

Supporting Bottom Tabs With Nested Routes

Alongside deep linking, one of the most common challenges when approaching routing in mobile apps is the ability to have nested routes. But what are nested routes?

final l10n = AppLocalizations.of(context);
// 1
final tabState = CupertinoTabPage.of(context);

// 2
return CupertinoTabScaffold(
  controller: tabState.controller,
  tabBuilder: tabState.tabBuilder,
  tabBar: CupertinoTabBar(
    items: [
      BottomNavigationBarItem(
        // 3
        label: l10n.quotesBottomNavigationBarItemLabel,
        icon: const Icon(
          Icons.format_quote,
        ),
      ),
      BottomNavigationBarItem(
        label: l10n.profileBottomNavigationBarItemLabel,
        icon: const Icon(
          Icons.person,
        ),
       ),
    ],
  ),
);

class _PathConstants {
  const _PathConstants._();

  static String get tabContainerPath => '/';

  static String get quoteListPath => '${tabContainerPath}quotes';

  static String get profileMenuPath => '${tabContainerPath}user';

  static String get updateProfilePath => '$profileMenuPath/update-profile';

  static String get signInPath => '${tabContainerPath}sign-in';

  static String get signUpPath => '${tabContainerPath}sign-up';

  static String get idPathParameter => 'id';

  static String quoteDetailsPath({
    int? quoteId,
  }) =>
      '$quoteListPath/${quoteId ?? ':$idPathParameter'}';
}
// 1
Map<String, PageBuilder> buildRoutingTable({
  // 2
  required RoutemasterDelegate routerDelegate,
  required UserRepository userRepository,
  required QuoteRepository quoteRepository,
  required RemoteValueService remoteValueService,
  required DynamicLinkService dynamicLinkService,
}) {
  return {
    // 3
    _PathConstants.tabContainerPath: (_) => 
      // 4
      CupertinoTabPage(
          child: const TabContainerScreen(),
          paths: [
            _PathConstants.quoteListPath,
            _PathConstants.profileMenuPath,
          ],
        ),
    // TODO: Define the two nested routes homes.
  };
}
_PathConstants.quoteListPath: (route) {
  return MaterialPage(
    // 1
    name: 'quotes-list',
    child: QuoteListScreen(
      quoteRepository: quoteRepository,
      userRepository: userRepository,
      onAuthenticationError: (context) {
        // 2
        routerDelegate.push(_PathConstants.signInPath);
      },
      onQuoteSelected: (id) {
        // 3
        final navigation = routerDelegate.push<Quote?>(
          _PathConstants.quoteDetailsPath(
            quoteId: id,
          ),
        );
        return navigation.result;
      },
      remoteValueService: remoteValueService,
    ),
  );
},
_PathConstants.profileMenuPath: (_) {
  return MaterialPage(
    name: 'profile-menu',
    child: ProfileMenuScreen(
      quoteRepository: quoteRepository,
      userRepository: userRepository,
      onSignInTap: () {
        routerDelegate.push(
          _PathConstants.signInPath,
        );
      },
      onSignUpTap: () {
        routerDelegate.push(
          _PathConstants.signUpPath,
        );
      },
      onUpdateProfileTap: () {
        routerDelegate.push(
          _PathConstants.updateProfilePath,
        );
      },
    ),
  );
},
// TODO: Define the subsequent routes.
return RouteMap(
  routes: {
    '/': (_) => const MaterialPage(
      child: Placeholder(),
    ),
  },
);
return RouteMap(
  routes: buildRoutingTable(
    routerDelegate: _routerDelegate,
    userRepository: _userRepository,
    quoteRepository: _quoteRepository,
    remoteValueService: widget.remoteValueService,
    dynamicLinkService: _dynamicLinkService,
  ),
);

Defining the Subsequent Routes

Go back to the lib/routing_table.dart file, find // TODO: Define the subsequent routes., and replace it with:

_PathConstants.updateProfilePath: (_) => MaterialPage(
  name: 'update-profile',
  child: UpdateProfileScreen(
    userRepository: userRepository,
    onUpdateProfileSuccess: () {
      routerDelegate.pop();
    },
  ),
),
_PathConstants.quoteDetailsPath(): (info) => MaterialPage(
    name: 'quote-details',
    child: QuoteDetailsScreen(
      quoteRepository: quoteRepository,
      // 1
      quoteId: int.parse(
        info.pathParameters[_PathConstants.idPathParameter] ?? '',
      ),
      onAuthenticationError: () {
        routerDelegate.push(_PathConstants.signInPath);
      },
      // 2
      shareableLinkGenerator: (quote) =>
          dynamicLinkService.generateDynamicLinkUrl(
        path: _PathConstants.quoteDetailsPath(
          quoteId: quote.id,
        ),
        socialMetaTagParameters: SocialMetaTagParameters(
          title: quote.body,
          description: quote.author,
        ),
      ),
    ),
  ),
_PathConstants.signInPath: (_) => MaterialPage(
  name: 'sign-in',
  fullscreenDialog: true,
  child: Builder(
    builder: (context) {
      return SignInScreen(
        userRepository: userRepository,
        onSignInSuccess: () {
          routerDelegate.pop();
        },
        onSignUpTap: () {
          routerDelegate.push(_PathConstants.signUpPath);
        },
        onForgotMyPasswordTap: () {
          showDialog(
            context: context,
            builder: (context) {
              return ForgotMyPasswordDialog(
                userRepository: userRepository,
                onCancelTap: () {
                  routerDelegate.pop();
                },
                onEmailRequestSuccess: () {
                  routerDelegate.pop();
                },
              );
            },
          );
        },
      );
    },
  ),
),
_PathConstants.signUpPath: (_) => MaterialPage(
  name: 'sign-up',
  child: SignUpScreen(
    userRepository: userRepository,
    onSignUpSuccess: () {
      routerDelegate.pop();
    },
  ),
),

Key Points

  • Navigator 1 is flexible and easy to use but not good at deep linking.
  • A solid routing strategy must support deep linking.
  • Navigator 2 is very good at deep linking but comes with a cost: It’s very hard to learn and use.
  • The best way to cope with Navigator 2 is to use wrapper packages, such as Routemaster.
  • With Routemaster, working with Nav 2 becomes almost as simple as using simple named routes from Nav 1.
  • When architecting an app with feature packages, consider handling all integration between the features — i.e., the navigation — inside a package that’s hierarchically above all of them.
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