A First Look at Flutter (3): Route Management

1. Basic concepts

The so-called Route Management is to manage how to jump between pages. It is usually also called navigation management. Route Management in Flutter is similar to native development. Whether it is Android or iOS, navigation management will maintain a route stack. The route push operation corresponds to opening a new page, and the route pop operation corresponds to the page closing operation. Route Management mainly refers to how to manage the route stack.

2. MaterialPageRoute

2.1 Basic definition

MaterialPageRoute is a component provided by the Material component library. It can implement routing switching animations consistent with the platform page switching animation style for different platforms:

  • For Android, when a new page is opened, the new page will slide from the bottom of the screen to the top of the screen; when the page is closed, the current page will slide from the top of the screen to the bottom of the screen and then disappear, while the previous page will be displayed on the screen.
  • For iOS, when a page is opened, the new page will slide from the right edge of the screen to the left side of the screen until the new page is fully displayed on the screen, while the previous page will slide from the current screen to the left side of the screen and disappear; when the page is closed, just the opposite, the current page will slide out from the right side of the screen, and the previous page will slide in from the left side of the screen.
1
2
3
4
5
6
MaterialPageRoute({
WidgetBuilder builder,
RouteSettings settings,
bool maintainState = true,
bool fullscreenDialog = false,
})
  • builder is a WidgetBuilder callback that constructs the route's content and returns a widget. Typically, this callback returns an instance of the new route.
  • settings Contains the configuration information of the route, such as the route name and whether it is the initial route (home page).
  • maintainState By default, when a new route is pushed into the stack, the original route will still be saved in memory. If you want to release all the resources occupied by the route when it is no longer used, you can set maintainState to false.
  • fullscreenDialog indicates whether the new route is presented as a full-screen modal. On iOS, when fullscreenDialog is true, the page slides in from the bottom of the screen rather than horizontally.

2.2 Usage example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class NewRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("New route"),
),
body: Center(
child: Text("This is new route"),
),
);
}
}

//...

Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
... //Omit irrelevant code
TextButton(
child: Text("open new route"),
onPressed: () {
// Navigate to new route
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return NewRoute();
}),
);
},
),
],
)

3. Navigator

3.1 Definition

Navigator is Flutter's route-management component. It provides methods for opening and closing routes and manages the active routes as a stack. The route at the top of the stack is normally the page displayed on screen. Navigator exposes many methods for managing this stack; here are the two most commonly used ones:

  1. Future push(BuildContext context, Route route)
    Pushes the given route onto the stack, opening a new page. The returned Future completes with the value passed back when the route is popped (closed).

  2. bool pop(BuildContext context, [ result ])
    Pop the top route from the stack, and the result is the data returned to the previous page when the page is closed.

    Navigator There are many other methods, such as Navigator.replace, Navigator.popUntil etc. For details, please refer to the API documentation or SDK source code comments, which will not be repeated here. Next we also need to introduce another concept related to routing, "named routing".

  3. Instance Method
    The first parameter in the Navigator class is context Static method corresponds to a Navigator Instance Method, for example Navigator.push(BuildContext context, Route route) is equivalent to Navigator.of(context).push(Route route), the following methods related to named routing are also the same.

4. Passing Data Between Routes

4.1 Passing Data with Anonymous Routes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class TipRoute extends StatelessWidget {
TipRoute({
Key key,
required this.text, // receives a text parameter
}) : super(key: key);
final String text;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Tip"),
),
body: Padding(
padding: EdgeInsets.all(18),
child: Center(
child: Column(
children: <Widget>[
Text(text),
ElevatedButton(
onPressed: () => Navigator.pop(context, "I am the return value"),
child: Text("return"),
)
],
),
),
),
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class RouterTestRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
onPressed: () async {
// Open `TipRoute` and wait for the result to be returned
var result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return TipRoute(
// Routing parameters
text: "I am promptxxxx",
);
},
),
);
//Output `TipRoute` route return result
print("Route return value:$result");
},
child: Text("Open prompt page"),
),
);
}
}

Requires explanation:

  1. The prompt text “I am prompt xxxx” is passed to the new route through TipRoute's text parameter. Await the Future returned by Navigator.push(...) to receive the route's result.

  2. There are two ways to return from TipRoute: click the back arrow in the navigation bar or click the “Return” button on the page. The first returns no data to the previous route, while the second does. The following shows what print in RouterTestRoute writes to the console for each action:

1
2
I/flutter (27896): Route return value: I am the return value
I/flutter (27896): Route return value: null

4.2 Passing Data with Named Routes

The so-called "Named Route" is a route with a name. We can give the route a name first, and then directly open a new route through the route name. This brings an intuitive and simple way to Route Management.

4.2.1 Route table

To use named routing, we must first provide and register a route table (route table) so that the application knows which name corresponds to which route component. In fact, registering a route table means giving a name to the route. The route table is defined as follows:

1
Map<String, WidgetBuilder> routes;

The route table is a Map: each key is a route-name string, and each value is a builder callback that creates the corresponding route widget. When a route is opened by name, the app looks up its WidgetBuilder, invokes it, and returns the generated widget.

4.2.2 Register route table

Registering the route table is straightforward. Return to the earlier counter example, locate MaterialApp in MyApp.build, and add the routes property:

1
2
3
4
5
6
7
8
9
10
11
12
MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
//Register routing table
routes:{
"new_page":(context) => NewRoute(),
... // Omit other routing registration information
} ,
home: MyHomePage(title: 'Flutter Demo Home Page'),
);

Now we have completed the registration of the route table. In the above code, the home route does not use a named route. What should we do if we also want to register home as a named route? It’s actually very simple, just look at the code:

1
2
3
4
5
6
7
8
9
10
11
12
MaterialApp(
title: 'Flutter Demo',
initialRoute:"/", //The route named "/" is applied as home(Homepage)
theme: ThemeData(
primarySwatch: Colors.blue,
),
//Register routing table
routes:{
"new_page":(context) => NewRoute(),
"/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //Register home page routing
}
);

Register MyHomePage in the route table, then use its name as the value of MaterialApp.initialRoute. This property determines the app's initial named route.

4.2.3 Open a new route by route name

To open a new route by route name, you can use the pushNamed method of Navigator:

1
Future pushNamed(BuildContext context, String routeName,{Object arguments})

In addition to pushNamed, Navigator provides methods such as pushReplacementNamed for managing named routes. See the API documentation for the full list. To open a new route by name, update the onPressed callback of TextButton as follows:

1
2
3
4
5
6
7
onPressed: () {
Navigator.pushNamed(context, "new_page");
//Navigator.push(context,
// MaterialPageRoute(builder: (context) {
// return NewRoute();
//}));
},

Hot reload application, click the "open new route" button again, you can still open a new route.

4.2.4 Named route parameter passing

In the initial version of Flutter, named routes could not pass parameters, and parameters were only supported later. The following shows how named routes pass and obtain route parameters:

We first register a route:

1
2
3
routes:{
"new_page":(context) => EchoRoute(),
} ,

Obtain routing parameters through the RouteSetting object on the route:

1
2
3
4
5
6
7
8
9
class EchoRoute extends StatelessWidget {

@override
Widget build(BuildContext context) {
//Get routing parameters
var args=ModalRoute.of(context).settings.arguments;
//...Omit irrelevant code
}
}

Pass parameters when opening route

1
Navigator.of(context).pushNamed("new_page", arguments: "hi");

4.2.5 Adaptation

Suppose we also want to register the TipRoute route in the route parameter passing example above into the route table, so that it can also be opened by the route name. However, since TipRoute accepts a text parameter, how can we adapt to this situation without changing the TipRoute source code? It's actually very simple:

1
2
3
4
5
6
7
8
MaterialApp(
... //Omit irrelevant code
routes: {
"tip2": (context){
return TipRoute(text: ModalRoute.of(context)!.settings.arguments);
},
},
);

4.3 Route generation hook

4.3.1 onGenerateRoute

Suppose we want to develop an e-commerce App. When users are not logged in, they can view store, product and other information, but transaction records, shopping carts, user personal information and other pages need to be logged in before they can be viewed. In order to achieve the above functions, we need to determine the user login status before opening each route! If we need to make a judgment every time before opening the routing, it will be very troublesome. Is there any better way? The answer is yes!

MaterialApp has an onGenerateRoute property that may be called when opening a named route. If the requested name is registered in the route table, Flutter uses that route's builder. Otherwise, it calls onGenerateRoute to create the route. The callback signature is:

1
Route<dynamic> Function(RouteSettings settings)

With the onGenerateRoute callback, it is very easy to implement the above function of controlling page permissions: we abandon the use of routing tables and instead provide a onGenerateRoute callback, and then perform unified permission control in this callback, such as:

1
2
3
4
5
6
7
8
9
10
11
MaterialApp(
... //Omit irrelevant code
onGenerateRoute:(RouteSettings settings){
return MaterialPageRoute(builder: (context){
String routeName = settings.name;
// If the accessed routing page requires login, but you are not currently logged in, you will directly return to the login page routing.
// Guide users to log in; in other cases, open routing normally.
}
);
}
);

Note that onGenerateRoute will only take effect on named routes.

4.3.2 navigatorObservers (to be added)

Monitor all routing jump actions

4.3.3 onUnknownRoute (to be added)

Will be called when opening a named route that does not exist

4.4 How to choose Route Management method

It is best to use the unified management method of named routing, which will bring the following benefits:

  1. The semantics are clearer.
  2. The code is easier to maintain; if you use anonymous routing, you must create a new route where Navigator.push is called. This not only requires importing the dart file of the new route, but also such code will be very scattered.
  3. Can use onGenerateRoute to do some global route jump pre-processing logic.