Never miss a beat

Join my newsletter.

Flutter routing inside of the Scaffold

Posted: 8/11/2020

Tagged under: dartfluttermobilenavigationweb
Flutter routing inside of the Scaffold

If you’re coming from React Native to Flutter, one of the first things you’ll likely ask is “How do I do routing?” First, I’d ask you to consider if you actually need routing. Instead, could you just have a global state that determines which screen to show? In most cases, probably. But if you want things to feel right when building Flutter for Web (or want decent deep linking support), you’ll probably want to build your flutter app with routing.

Routing can easily be accomplished via the MaterialApp widget in Flutter. In fact, the MaterialApp has a routes property for exactly that! When Flutter’s Navigator finds a route that matches one defined on the MaterialApp’s routes property, it will swap out the MaterialApp’s current child with the one that matches the route. This is common behavior for most frontend routing libraries (including React Router for React, for example).

The dilemma here is that most of the time, your MaterialApp’s direct child will be a Scaffold, which may or may not include a drawer (in my case, it does). However, as stated above, routing will swap out the direct child of the MaterialApp widget (in this case, the scaffold).

Why is this problematic? There are two key issues here. First, you’ll end up creating a new Scaffold as the main widget that you route to for each route that you define. Let’s consider the following code [dartpad link]:

import 'package:flutter/material.dart';
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
initialRoute: '/1',
routes: {
'/1': (ctx) => Widget1(),
'/2': (ctx) => Widget2(),
'/3': (ctx) => Widget3(),
}
);
}
}
class Widget1 extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
drawer: Drawer(
child: const Text('In the Drawer', textAlign: TextAlign.center),
),
body: Text('Hello, World!', style: Theme.of(context).textTheme.headline4)
);
}
}
class Widget2 extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
drawer: Drawer(
child: const Text('In the Drawer', textAlign: TextAlign.center),
),
body: Text('Hello, World!', style: Theme.of(context).textTheme.headline4)
);
}
}
class Widget3 extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
drawer: Drawer(
child: const Text('In the Drawer', textAlign: TextAlign.center),
),
body: Text('Hello, World!', style: Theme.of(context).textTheme.headline4)
);
}
}

You’ll notice that we’ve duplicated our Scaffold three times, which is not great for code reuse, and if you paste this code in a flutter project and build for mobile, you’d notice that despite the route that you’re on, you’d ALWAYS have the hamburger/drawer in the top left. This probably isn’t desired, especially if you allow the user to navigate down into data (for example, navigating to a specific item from a list view). Let’s solve for both of these issues.

The Scaffold copy/paste can be reduced by creating a widget for the scaffold and passing the scaffold’s body to our widget [dartpad link].

import "package:flutter/material.dart"
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
initialRoute: '/1',
routes: {
'/1': (ctx) => Widget1(),
'/2': (ctx) => Widget2(),
'/3': (ctx) => Widget3(),
}
);
}
}
class MyScaffold extends StatelessWidget {
final Widget body;
MyScaffold({this.body});
Widget build(BuildContext context) {
return Scaffold(
drawer: Drawer(
child: const Text('In the Drawer', textAlign: TextAlign.center),
),
body: this.body
);
}
}
class Widget1 extends StatelessWidget {
Widget build(BuildContext context) {
return MyScaffold(
body: Text('Widget 1', style: Theme.of(context).textTheme.headline4)
);
}
}
class Widget2 extends StatelessWidget {
Widget build(BuildContext context) {
return MyScaffold(
body: Text('Widget 2', style: Theme.of(context).textTheme.headline4)
);
}
}
class Widget3 extends StatelessWidget {
Widget build(BuildContext context) {
return MyScaffold(
body: Text('Widget 3', style: Theme.of(context).textTheme.headline4)
);
}
}

Great! This helps us avoid the need to recreate our drawer and scaffold in each child component. Now let’s add some code to allow you to actually navigate. We’ll do something simple that navigates by pushing a new route from Widget1 to Widget2 to Widget3. [dartpad link]

import 'package:flutter/material.dart';
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
initialRoute: '/1',
routes: {
'/1': (ctx) => Widget1(),
'/2': (ctx) => Widget2(),
'/3': (ctx) => Widget3(),
}
);
}
}
class MyScaffold extends StatelessWidget {
final Widget body;
MyScaffold({this.body});
Widget build(BuildContext context) {
return Scaffold(
drawer: Drawer(
child: const Text('In the Drawer', textAlign: TextAlign.center),
),
body: this.body
);
}
}
class Widget1 extends StatelessWidg