Flutter navigation and routing System : Part 1

Scaibu
7 min readJun 26, 2023

--

If you’re using Flutter, you’re probably using the Navigator and are familiar with the following concepts:

  • Navigator — a widget that manages a stack of Route objects.
const Navigator({
super.key,
this.pages = const <Page<dynamic>>[],
this.onPopPage,
this.initialRoute,
this.onGenerateInitialRoutes = Navigator.defaultGenerateInitialRoutes,
this.onGenerateRoute,
this.onUnknownRoute,
this.transitionDelegate = const DefaultTransitionDelegate<dynamic>(),
this.reportsRouteUpdateToEngine = false,
this.clipBehavior = Clip.hardEdge,
this.observers = const <NavigatorObserver>[],
this.requestFocus = true,
this.restorationScopeId,
this.routeTraversalEdgeBehavior = kDefaultRouteTraversalEdgeBehavior,
});
  • Route — an object managed by a Navigator that represents a screen, typically implemented by classes like MaterialPageRoute.
Route({ RouteSettings? settings }) : _settings = settings ?? const RouteSettings();d

Routes were pushed and popped onto the Navigator’s stack with either named routes or anonymous routes. The next sections are a brief recap of these two approaches.

Anonymous routes
Most mobile apps display screens on top of each other, like a stack. In Flutter, this is easy to achieve by using the Navigator.

MaterialApp and CupertinoApp already use a Navigator under the hood. You can access the navigator using Navigator.of() or display a new screen using Navigator.push(), and return to the previous screen with Navigator.pop()

class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FlatButton(
child: Text('View Details'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return DetailScreen();
}),
);
},
),
),
);
}
}
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FlatButton(
child: Text('Pop!'),
onPressed: () {
Navigator.pop(context);
},
),
),
);
}
}

When push() is called, the DetailScreen widget is placed on top of the HomeScreen widget like this The previous screen (HomeScreen) is still part of the widget tree, so any State object associated with it stays around while DetailScreen is visible.

Named routes

@optionalTypeArgs
static Future<T?> pushNamed<T extends Object?>(
BuildContext context,
String routeName, {
Object? arguments,
}) {
return Navigator.of(context).pushNamed<T>(routeName, arguments: arguments);
}

Flutter also supports named routes, which are defined in the routes parameter on MaterialApp or CupertinoApp

class Nav2App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
routes: {
'/': (context) => HomeScreen(),
'/details': (context) => DetailScreen(),
},
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FlatButton(
child: Text('View Details'),
onPressed: () {
Navigator.pushNamed(
context,
'/details',
);
},
),
),
);
}
}
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FlatButton(
child: Text('Pop!'),
onPressed: () {
Navigator.pop(context);
},
),
),
);
}
}

RouteObserver

RouteObserver informs subscribers whenever a route of type R is pushed on top of their own route of type R or popped from it. This is for example useful to keep track of page transitions, e.g. a RouteObserver<PageRoute> will inform subscribed RouteAwares whenever the user navigates away from the current page route to another page route is simple terms

Imagine you are reading a book and you have a special friend sitting next to you. This friend is called a RouteObserver. The RouteObserver’s job is to tell you whenever a new page is added on top of the current page you’re reading or when a page is taken away from the top.

Let’s say you’re reading a storybook, and every time you turn a page, the RouteObserver friend tells you about it. If you go from page 10 to page 11, the RouteObserver friend will let you know that you moved to a new page. Similarly, if you go back from page 11 to page 10, the RouteObserver friend will also tell you that you went back to the previous page.

This can be helpful because it allows you to keep track of your progress and know when you’re moving to a different part of the story. In the same way, a RouteObserver<PageRoute> tells other friends, called RouteAwares, whenever someone switches from one page to another in a special app or website.

So, just like having a friend beside you to keep you updated on the pages of a book, a RouteObserver helps other parts of a computer program know when someone is moving between different pages or screens.

// Register the RouteObserver as a navigation observer.
final RouteObserver<ModalRoute<void>> routeObserver = RouteObserver<ModalRoute<void>>();

void main() {
runApp(MaterialApp(
home: Container(),
navigatorObservers: <RouteObserver<ModalRoute<void>>>[ routeObserver ],
));
}

class RouteAwareWidget extends StatefulWidget {
const RouteAwareWidget({super.key});

@override
State<RouteAwareWidget> createState() => RouteAwareWidgetState();
}

// Implement RouteAware in a widget's state and subscribe it to the RouteObserver.
class RouteAwareWidgetState extends State<RouteAwareWidget> with RouteAware {

@override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context)!);
}

@override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}

@override
void didPush() {
// Route was pushed onto navigator and is now topmost route.
}

@override
void didPopNext() {
// Covering route was popped off the navigator.
}

@override
Widget build(BuildContext context) => Container();

}

Route Extra Methods

  1. didPush(Route route, Route? previousRoute)

2. didRemove(Route route, Route? previousRoute)

3. didReplace({Route? newRoute, Route? oldRoute})

4. didStartUserGesture(Route route, Route? previousRoute)

5. didStopUserGesture()

6. subscribe(RouteAware routeAware, R route)

7. unsubscribe(RouteAware routeAware)

Advanced named routes with onGenerateRoute

A more flexible way to handle named routes is by using onGenerateRoute. This API gives you the ability to handle all paths

MaterialPageRoute({
required this.builder,
super.settings,
this.maintainState = true,
super.fullscreenDialog,
super.allowSnapshotting = true,
}) {
assert(opaque);
}

A modal route that replaces the entire screen with a platform-adaptive transition.

For Android, the entrance transition for the page zooms in and fades in while the exiting page zooms out and fades out. The exit transition is similar, but in reverse.

For iOS, the page slides in from the right and exits in reverse. The page also shifts to the left in parallax when another page enters to cover it. (These directions are flipped in environments with a right-to-left reading direction.)

By default, when a modal route is replaced by another, the previous route remains in memory. To free all the resources when this is not necessary, set maintainState to false.

The fullscreenDialog property specifies whether the incoming route is a fullscreen modal dialog. On iOS, those routes animate from the bottom to the top rather than horizontally.

The type T specifies the return type of the route which can be supplied as the route is popped from the stack via Navigator.pop by providing the optional result argument.

class Nav2App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
onGenerateRoute: (settings) {
// Handle '/'
if (settings.name == '/') {
return MaterialPageRoute(builder: (context) => HomeScreen());
}

// Handle '/details/:id'
var uri = Uri.parse(settings.name);
if (uri.pathSegments.length == 2 &&
uri.pathSegments.first == 'details') {
var id = uri.pathSegments[1];
return MaterialPageRoute(builder: (context) => DetailScreen(id: id));
}

return MaterialPageRoute(builder: (context) => UnknownScreen());
},
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FlatButton(
child: Text('View Details'),
onPressed: () {
Navigator.pushNamed(
context,
'/details/1',
);
},
),
),
);
}
}
class DetailScreen extends StatelessWidget {
String id;

DetailScreen({
this.id,
});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Viewing details for item $id'),
FlatButton(
child: Text('Pop!'),
onPressed: () {
Navigator.pop(context);
},
),
],
),
),
);
}
}
class UnknownScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Text('404!'),
),
);
}
}

Imagine you have a special way of showing something on a screen. It’s called a modal route. When you use a modal route, it completely replaces whatever was on the screen before with something new, using a special transition that matches the device you’re using.

Let’s say you’re using an Android device. When the new thing appears on the screen, it zooms in and fades in, making it look like it’s coming closer to you. When the new thing goes away and the previous thing comes back, it zooms out and fades out, as if it’s going farther away.

On an iOS device, when the new thing appears, it slides in from the right side of the screen. When it goes away, it slides out in reverse, like going back to where it came from. Also, if there’s another new thing that comes on top of it, the previous thing moves a little bit to the left, giving a cool effect like it’s shifting.

Sometimes, when you replace one thing with another using a modal route, the previous thing stays in the memory, using resources. But if you don’t need it anymore, you can choose to free up those resources by setting something called “maintainState” to false.

There’s also a property called “fullscreenDialog” which decides whether the new thing coming in is a full-screen dialog or not. On iOS, if it’s a full-screen dialog, it comes up from the bottom of the screen to the top, instead of moving horizontally.

Lastly, there’s something called the “return type” which is like a special message that the new thing can send back when it’s done and you want to go back to what you were doing before. It’s like a way for the new thing to tell the device, “Hey, I’m done, and here’s the result.” This helps with the process of going back to the previous screen.

So, a modal route is a special way of showing something on a screen that completely replaces what was there before. It has different effects and behaviors depending on the device you’re using. It can also send a message back when it’s done.

Here, settings is an instance of RouteSettings. The name and arguments fields are the values that were provided when Navigator.pushNamed was called, or what initialRoute is set to.

--

--

Scaibu
Scaibu

Written by Scaibu

Revolutionize Education with Scaibu: Improving Tech Education and Building Networks with Investors for a Better Future

No responses yet