Introducing Navigation
- In Flutter, you use a Navigator widget to manage your screens or pages. Think of screens and pages as routes.
- 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.
Navigator 1.0 Overview
So navigator 1 was a simple push/pop API so if you wanted to “route” to different views you would have to create a MaterialPageRoute that returned your widget and then put that machine behind all the different buttons or interface elements you’d be working with. So it’s kinda tedious.
Pushing and Popping Routes
- push(): Adds view to stack
- pop(): Removes view from stack
bool result = await Navigator.push<bool>(
context,
MaterialPageRoute<bool>(
builder: (BuildContext context) => OnboardingScreen()
),
);
So in the above example the we’re calling push() and then instantiating a route that contains our view to pass as our argument to push. So we’re kinda doing double work, because we already had to make components > view > MaterialPageRoute > push() and I’m pretty sure we’re about to find out there’s fewer steps later in the chapter.
Navigator 1.0’s Disadvantages
- It’s not scalable – Since you’re essentially managing a tree of navigation chains.
- the route stack is not exposed to developers so they cannot handle complicated cases like +/- screens.
- Does not update the web URL, so if you’re writing a flutter app you’re going to deploy as a web application, your backwards actions might not work as expected.
the solution to these things was the Router API
Router API Overview
The features of the router API are:
- The Page Stack is exposed so developers have more control of how to move about the application
- Backwards compatible with Imperative API – So you can use both Imperative and declarative styles in the same app.
- Handles OS events, like when the system or web back button is pressed.
- Manages nested navigators – Gives you control of which navigator has priority
- Managing navigation state – You can parse routes and handle URLS and deep linking.
What are the key components of Router’s declartive API?
- Page: An abstract class that describes the configuration for a route.
- Router: Handles configuring the list of pages the Navigator displays.
- RouterDelegate: Defines how the router listens for changes to the app state to rebuild the navigator’s configuration.
- RouteInformationProvider: Provides
RouteInformation
to the router. Route information contains the location info and state object to configure your app. - RouteInformationParser: Parses route information into a user-defined data type.
- BackButtonDispatcher: Reports presses on the platform system’s Back button to the router.
- TransitionDelegate: Decides how pages transition into and out of the screen.
Navigation and Unidirectional Data Flow
So the idea is that you’re basically always working your way in one direction and there’s no logic or multiple routes. Basically you’re trying to keep things simple. Basically routes are state-drive. Since things are STATE driven vs imperitively defined, routing and navigation can happen based on what’s going on in the app, not because you explicitly defined a route somewhere and then associated that route with a button touch or something like that.
Is Declarative Always Better Than Imperative?
It depends. If you’re writing small apps, imperative is okay, if you’re writing larger apps then declarative is the route to go but even then
Getting Started
Changes to the Project Files
Nothing of note.
What’s New in the Screens Folder
Nothing of note.
Changes to the Models Folder
Nothing of note.
Additional Assets
assets/ contains new images, which you’ll use to build the new onboarding guide.
New Packages
Nothing of note.
Android SDK Version
Nothing of note.
Looking Over the UI Flow
Breaks down the views and what leads to what and how A leads to B.
Introducing go_router
Creating the go_router
Alright so here’s the router we created:
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../models/models.dart';
import '../screens/screens.dart';
class AppRouter {
// Class Attributes.
// 1
final AppStateManager appStateManager;
// 2
final ProfileManager profileManager;
// 3
final GroceryManager groceryManager;
// Class Constructor which sets the value of it's own properties.
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
);
}
So to kinda walk through what we’ve done: We create an instance of our application router called AppRouter whose properties are the state, by way of ChangeNotifiers, of the different views we’re going to display with this router. so we’re putting our state with our routes. Then we initialize our AppRouter with a constructor that requires the state managers for the views we’re working with. and then we instantiate a new instance of GoRouter, and for our purposes, we tell it which view to open with and then we have a list of routes that we want it to handle. So our AppRouter is basically a wrapper around GoRouter so something tells me we;re going to to do something like AppRouter.GOTOVIEW or something like that later on in the chapter.
I suspect but am not sure that the thing that “hooks” this router into our application is the refreshListenable using an instance of appStateManager.
yeah I was right:
6.Sets your router to listen for app state changes. When the state changes, your router will trigger a rebuild of your routes.
Flutter Apprentice, Chapter 7
Using Your App Router
so in this section we imported the AppRouter we created in the previous section and then in the declaration of state for our fooderlich widget, we added a new property for our app router using the state managers for the different views that we have in our application then we inherit the app state manager declared in the “parent” widget.
class Fooderlich extends StatefulWidget {
// this is the appStateManager we're passing to construct
// our our AppRouter.
final AppStateManager appStateManager;
const Fooderlich({super.key, required this.appStateManager});
@override
FooderlichState createState() => FooderlichState();
}
class FooderlichState extends State<Fooderlich> {
late final _groceryManager = GroceryManager();
late final _profileManager = ProfileManager();
late final _appRouter =
AppRouter(widget.appStateManager, _profileManager, _groceryManager);
(...)
Using Your App Router
Alright so this part of the section is pretty significant:
final router = _appRouter.router;
return MaterialApp.router(
theme: theme,
title: 'Fooderlich',
routerDelegate: router.routerDelegate,
routeInformationParser: router.routeInformationParser,
routeInformationProvider: router.routeInformationProvider,
);
So it’s worth noting here that we didn’t have to make the routerDelegate (which we would without go_router) routerInformationParser, and routerInformationProvider, all of those things are provided for us with when we create a new instance of GoRouter. So that’s saving us a lot of headache and manual coding to get navigation to work. and since we’re returning an instance of a MaterialApp with a router that routes to the screen vs a material app that that returns a starting view we can get rid of our screens import.
Adding Screens
Nothing of Note.
Setting Up Your Error Handler
Alright so GoRouter property called errorPageBuilder which is kinda like the same thing we see in listViewBuilder() where there’s a buidler property that takes an anonymous function that returns some kind of widget. In this case we returned a new material page with the error centered out as text.
errorPageBuilder: (context, statroutere) {
return MaterialPage(
key: state.pageKey,
child: Scaffold(
body: Center(child: Text(state.error.toString())),
));
}
Adding the Login Route
So this section shows us how to use GoRoute. An instance of GoRouter takes a list of GoRoute to satisfy it’s routes property. basically we name and path the route and then provide a pre-built widget as the builder for that specific route.
you will always need to pass through context (where am i?) and state (what’s going on? or, what’s the status of things?)
So when we got done with this section the routes list looked look:
routes: [
GoRoute(
name: 'login',
path: '/login',
builder: (context, state) => const LoginScreen(),
),
//TODO: ADd Onboarding Route
//TODO: Add Home Route
],
From the book:name
names the route. If set, you must provide a unique string name; this can’t be empty.
path
is this route’s path.builder
is this route’s page builder. It’s responsible for building your screen widget.
From there we update the login view to by importing provider and then consuming the Appstate provider such that when we tap the login button we call the AppStateManager (ChangeNotifier) login function which sets the login status to true, which should move us on to the next screen (which I believe is onboarding):
onPressed: () async {
Provider.of<AppStateManager>(context, listen: false).login('mock', 'mock');
},
Adding the Onboarding Route
we basically just added another route:
GoRoute(
name: 'onboarding',
path: '/onboarding',
builder: (context, state) => const OnboardingScreen(),
),
and while It’s true that now the route is present in the application, it doesn’t mean that flutter will just go to it. so we have two options
Debugging the Issue
The books outlines that there two approaches to handling navigation or telling the router how to handle sending users through the app:
Implementing Redirection in the Router
We set the redirect property
Handling Redirects
So implementing Redirection in the router involves getting the value of different properties of our appStateManager for example isLoggedIn or isOnboarding. Basically we’re implementing a linear data flow everything should be false until we finally reach a condition that returns a route and from there our router will handle it. our redirect handler (property is set for):
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;
},
To try and say it a little more plainly, since the GoRouter class provides a redirect handler (property of the class) you can basically write a little script to explain to goRouter how it should switch between views
Handling the Skip Button in Onboarding
For this we import provider and models then use them to call the onboarded function of our AppStateManager which sets the onboarded bool to true and notifies listeners so we’re done with onboarding and can be redirected to explore.
Provider.of<AppStateManager>(context, listen:false).onboarded();
Transitioning From Onboarding to Home
Transitioning From Onboarding to Home
So in this section we learned that a GoRoute can have child GoRoute’s implemented the same way they are in the GoRouter implementation. so we create a route for our Home view and set the normal name, path, and builder but then we define the additional property routes which we will later use to implement nagivation to Items and Profiles.
GoRoute(
// 1
name: 'home',
path: '/:tab', //<- ORGINALLY MESSED UP HERE W/ /home
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
So basically we handle this by updating our bottomNavigationBar ‘s onTap property to first update out application state so that our selectedTab property is equal to the button number or index of the button we just tapped’s postiton in the list of options. With that number we then pass the index / tab numerical value as a parameter which I’m sure we will use when we add the Item and Profile routes to our route list.
Out updated code looks like:
bottomNavigationBar: BottomNavigationBar(
selectedItemColor: Theme.of(context).textSelectionTheme.selectionColor,
currentIndex: widget.currentTab,
onTap: (index) {
// 1
Provider.of<AppStateManager>(context, listen: false).goToTab(index);
// 2
context.goNamed('home', params: {'tab': '$index'},);
},
items: const [
(...)
and our home route, again for reference:
GoRoute(
// 1
name: 'home',
path: '/:tab', //<- Fucked up here.
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 the Browse Recipes Button
In this section we demonstrate being able to directly consume or navigate to routes using go router. for example we’re saying “Hey when you click this button, you need to go to the recipes view which is a member of the FooderlichTab, which I seem to have forgotten what it does.
Showing the Grocery Item Screen
We accomplished this by creating a subroute from home that returns an instance of the GroceryItemScreen that takes the item (defined by the ItemID as an integer in our application’s state) and then using the onCreate and onUpdate properties of our GroceryItemScreen to determine what happens we create or update a recipe item.
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
Same as before we used context.goNamed to navigate to the ‘item’ view by name and pass in the place holder ID of “new” since it’s going to be a new options. I’m FooderlichTab pretty sure we could pass in a numerical value and it would modify it would bring up a view with the particulars for the Item with that ID.
Navigating Back Home
we used another goNamed and navigated back to the home screen by name. importing go_router everywhere we use our router class.
context.goNamed(
'home',
params: {
'tab': '${FooderlichTab.toBuy}',
},
);
Editing an Existing Grocery Item
Would you be suprised if I told you we did another goNamed?
// 1
final itemId = manager.getItemId(index);
// 2
context.goNamed(
'item',
params: {
'tab': '${FooderlichTab.toBuy}',
'id': itemId
},
);
Navigating to the Profile Screen
Oh my god am I done with this chapter yet? like, for real, can I please be done and move on.
Navigating to raywenderlich.com
Seriously I hate all of this and I’m still not done yet. anywas, we made a new route:
GoRoute(
name: 'rw',
path: 'rw',
builder: (context, state) => const WebViewScreen(),
),
and then we made a button “do the thing” tm with the code below:
context.goNamed(
'rw',
params: {'tab': '${widget.currentTab}'},
);
Logging Out
to handle logging out we consume the AppStateManager provider and call it’s logout function
AND BY THE POWER OF THE LORD THIS CHAPTER IS DONE.
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 usinggoNamed
instead ofgo
. - 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.
You also learned to create a GoRouter
widget, which encapsulates and configures all the page routes for a navigator. Now, you can easily manage your navigation flow in a single router object!
To learn about navigation in Flutter, here are some recommendations for high-level theory and walk-throughs:
- To understand the motivation behind Navigator 2.0, check out the design document.
- Watch this presentation by Chun-Heng Tai, who contributed to the declarative API: Navigator 2.0.
- In this video, Simon Lightfoot walks you through a Navigator 2.0 example.
- Flutter Navigation 2.0 by Dominik Roszkowski goes through the differences between Navigator 1.0 and 2.0, including a video example.
- For in-depth knowledge about Navigator, check out Flutter’s documentation.
- GoRouter developer documentation.
- GoRouter navigation examples.