Lesson 09 / 19
Navigation and Routing
Whichever route opens the page, the navigator determines its width. The lesson measures three routes and three deep links and shows that the request of the page does not enter the result.
Published Updated
In this lesson
- Tells the route definition, the route stack, and the deep link apart
- Measures that the navigator gives the page a tight constraint at the screen width
- Shows that the sizing of the page stays forced even when the route changes
- Shows that a page opened by a deep link receives the same constraint
Contents
The previous lesson measured the width of a badge fed by a shared counter and showed that state changes the request, not the constraint. This lesson steps up one layer in the tree: to the navigation that decides which screen the app shows. A page is placed on the route stack, removed from the stack, or opened directly by a link that arrives from outside. The course question is the same again: who determines this widget’s width? The lesson looks for the answer in the navigator that wraps the page.
Route, Stack, and Deep Link
Every screen of the app is named by a route. A route is a name and a function that produces the page that name will open. Open routes are kept in a stack: a new route is placed on top of the stack, going back removes the top, and the screen always shows the page of the route on top. Routing libraries make the route definition and the stack management declarative; the library name is not written in this lesson, and it is referred to by its function.
A deep link opens the app from outside, with an address. The path of the address is resolved to a route name, and the resolved route is placed on the stack. The user did not reach the page from the home screen, but the place where the page stands on the screen does not change.
The framework side of navigation is a container that manages the route stack and the pages placed inside that container.
The Core
Every lesson uses the same core. measure walks the tree with a tight constraint at the screen width, produces a record for each node, and gives the sizing in the record by a rule: if the constraint is tight, forced; if the request stays inside the range, chosen; if the request falls outside the range, or there is no request and there is no upper bound, clamped; if there is no request and the upper bound is finite, stretched. The four names are written in ASCII in the output: forced, chosen, clamped, stretched. The parent: line is the total of forced, clamped, and stretched; it counts the records whose width is not the widget’s own request.
// sizing.dart — classifies who decides a widget's width
const sizings = ['forced', 'chosen', 'clamped', 'stretched'];
class Limit {
final int min;
final int? max; // null: unbounded
const Limit(this.min, this.max);
bool get tight => max == min;
Limit loosen() => Limit(0, max);
Limit deflate(int by) => Limit(_floor(min - by), max == null ? null : _floor(max! - by));
String bounds() => '$min..${max ?? 'inf'}';
}
int _floor(int value) => value < 0 ? 0 : value;
class Part {
final String kind;
final String name;
final int? width; // null: as wide as it can get
final List<Part> children;
final int factor;
final bool tightFlex;
const Part(this.kind, this.name, this.width, this.children,
{this.factor = 0, this.tightFlex = true});
}
Part leaf(String name, int width) => Part('leaf', name, width, const []);
Part fill(String name) => Part('leaf', name, null, const []);
Part pad(String name, int padding, Part child) => Part('pad', name, padding, [child]);
Part center(String name, Part child) => Part('center', name, null, [child]);
Part fixed(String name, int width, Part child) => Part('fixed', name, width, [child]);
Part column(String name, List<Part> children) => Part('column', name, null, children);
Part row(String name, List<Part> children) => Part('row', name, null, children);
Part flex(String name, int factor, Part child, {bool tight = true}) =>
Part('flex', name, null, [child], factor: factor, tightFlex: tight);
class Sizing {
final String sizing;
final String path;
final int width;
final Limit limit;
const Sizing(this.sizing, this.path, this.width, this.limit);
}
Sizing classify(String path, int? desire, Limit limit) {
if (limit.tight) return Sizing('forced', path, limit.min, limit);
if (desire == null) {
return limit.max == null
? Sizing('clamped', path, limit.min, limit)
: Sizing('stretched', path, limit.max!, limit);
}
if (desire < limit.min) return Sizing('clamped', path, limit.min, limit);
if (limit.max != null && desire > limit.max!) {
return Sizing('clamped', path, limit.max!, limit);
}
return Sizing('chosen', path, desire, limit);
}
int layout(Part part, Limit limit, List<Sizing> log, [String path = '']) {
final here = '$path/${part.name}';
final below = <Sizing>[];
if (part.kind == 'fixed') {
final own = classify(here, part.width, limit);
layout(part.children.first, Limit(own.width, own.width), below, here);
log..add(own)..addAll(below);
return own.width;
}
int? desire;
switch (part.kind) {
case 'leaf':
desire = part.width;
case 'pad':
final space = 2 * part.width!;
desire = layout(part.children.first, limit.deflate(space), below, here) + space;
case 'center':
final inner = layout(part.children.first, limit.loosen(), below, here);
desire = limit.max == null ? inner : null;
case 'flex':
desire = layout(part.children.first, limit, below, here);
case 'column':
var widest = 0;
for (final child in part.children) {
final width = layout(child, limit.loosen(), below, here);
if (width > widest) widest = width;
}
desire = widest;
case 'row':
final logs = [for (final _ in part.children) <Sizing>[]];
final bounded = limit.max != null;
var used = 0;
var factors = 0;
for (var i = 0; i < part.children.length; i++) {
final child = part.children[i];
if (child.kind == 'flex' && bounded) {
factors += child.factor;
} else {
used += layout(child, const Limit(0, null), logs[i], here);
}
}
final left = bounded ? _floor(limit.max! - used) : 0;
var given = 0;
var seen = 0;
for (var i = 0; i < part.children.length; i++) {
final child = part.children[i];
if (child.kind != 'flex' || !bounded) continue;
seen += child.factor;
final share = (seen == factors ? left : left * seen ~/ factors) - given;
given += share;
final room = child.tightFlex ? Limit(share, share) : Limit(0, share);
used += layout(child, room, logs[i], here);
}
for (final entries in logs) {
below.addAll(entries);
}
desire = bounded ? null : used;
}
final own = classify(here, desire, limit);
log..add(own)..addAll(below);
return own.width;
}
List<Sizing> measure(Part root, int screen) {
final log = <Sizing>[];
layout(root, Limit(screen, screen), log);
return log;
}
void printSizes(List<Sizing> log) {
for (final entry in log) {
print('${entry.sizing}: ${entry.path} width=${entry.width} constraint=${entry.limit.bounds()}');
}
}
void countSizings(List<Sizing> log) {
for (final name in sizings) {
print('$name: ${log.where((entry) => entry.sizing == name).length}');
}
print('parent: ${log.where((entry) => entry.sizing != 'chosen').length}');
}
The Framework Side
The snippet below shows a route being placed on the stack. The snippet is not measured; the model does not run the framework’s code.
// Platform code: route stack
Future<void> openOrders(BuildContext context) {
return Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => const OrdersPage(),
),
);
}
void closeOrders(BuildContext context) {
Navigator.of(context).pop();
}
push places a route on top of the stack, and pop removes the top. The function that builds the page is builder; when the navigator calls it, it gives the page a constraint. The question of this lesson is where that constraint comes from and what the page’s own request does to it.
From Route to Page
The navigator of the model is a fixed container at the screen width: fixed('Navigator', screen, page). The page is the only child of the container, and the width of the container comes down to the child as a tight constraint. The page of each route has a request; the requests live in a helper file. The helper file also carries the route stack and the function that turns an address into a route name. None of the three prints anything to the output.
// routes.dart — route stack, page requests, and link resolution
const pageRequests = {'Orders': 200, 'Profile': 480, 'Settings': 120};
const _linkRoutes = {'orders': 'Orders', 'profile': 'Profile', 'settings': 'Settings'};
class RouteStack {
final List<String> _names;
RouteStack(String first) : _names = [first];
String get top => _names.last;
int get depth => _names.length;
void push(String name) => _names.add(name);
void pop() {
if (_names.length > 1) {
_names.removeLast();
}
}
}
String routeFromLink(String link) {
final segments = Uri.parse(link).pathSegments;
return _linkRoutes[segments.first]!;
}
buildRoute takes the name of the route on top of the stack and puts the page of that name inside the container. The first measurement opens the Orders page.
// main.dart
import 'sizing.dart';
import 'routes.dart';
Part buildRoute(String name, {int screen = 360}) {
final page = leaf(name, pageRequests[name]!);
return fixed('Navigator', screen, page);
}
void main() {
final stack = RouteStack('Orders');
final log = measure(buildRoute(stack.top), 360);
printSizes(log);
countSizings(log);
}
forced: /Navigator width=360 constraint=360..360 forced: /Navigator/Orders width=360 constraint=360..360 forced: 2 chosen: 0 clamped: 0 stretched: 0 parent: 2
The Orders page asks for 200 units, but the /Navigator/Orders record is forced: its width is 360 and its constraint is 360..360. The navigator gives the page the whole screen as a tight constraint; as soon as the page’s request enters the record, it is equated to the single point of the range. The container is also forced, by the constraint the screen gives. Neither the page nor the container chose the width itself: both received it from outside.
As the Route Changes
Now place three routes on the stack and look at the record of the page each time one is placed. Then we go back twice. The stack depth carries meaning in the prose; the only input of the measurement is the name of the route on top.
// main.dart
import 'sizing.dart';
import 'routes.dart';
Part buildRoute(String name, {int screen = 360}) {
final page = leaf(name, pageRequests[name]!);
return fixed('Navigator', screen, page);
}
void show(RouteStack stack) {
final log = measure(buildRoute(stack.top), 360);
final page = log.firstWhere((entry) => entry.path == '/Navigator/${stack.top}');
print('${stack.top} depth=${stack.depth} request=${pageRequests[stack.top]} ${page.sizing} width=${page.width}');
}
void main() {
final stack = RouteStack('Orders');
show(stack);
stack.push('Profile');
show(stack);
stack.push('Settings');
show(stack);
stack.pop();
show(stack);
stack.pop();
show(stack);
}
Orders depth=1 request=200 forced width=360 Profile depth=2 request=480 forced width=360 Settings depth=3 request=120 forced width=360 Profile depth=2 request=480 forced width=360 Orders depth=1 request=200 forced width=360
The requests of the three pages are 200, 480, and 120, and all three stand at the same width, 360. In every line the sizing is forced. Profile asks for a page wider than the screen; the container still brings it down to 360. Settings asks for only 120; it still fills 360. On the way back, Profile and Orders return with the same width. However long a page stays on the stack, and even if it is removed from the stack and opened again, the page does not give its own width; on every opening the container gives it again.
The question where does the lifetime end looks for the end of a value’s lifespan: when a page is removed from the stack, the values of the page end. This lesson does not ask it. Whatever the lifetime of a route page is, the container imposes its width; the measurement does not classify the moment the lifetime begins or ends, but the owner of the page’s width once it is on the screen.
The Bounding Measurement
A link that arrives from outside opens a different path to the page but does not change the constraint. The measurement below turns three links into route names and measures each of them at two screen widths. The first width is 360 and the second is 280. The requests of the pages are again 200, 480, and 120.
// main.dart
import 'sizing.dart';
import 'routes.dart';
Part buildRoute(String name, {int screen = 360}) {
final page = leaf(name, pageRequests[name]!);
return fixed('Navigator', screen, page);
}
void main() {
final links = [
'app://shop/orders/42',
'app://shop/profile',
'app://shop/settings/theme',
];
for (final screen in [360, 280]) {
for (final link in links) {
final name = routeFromLink(link);
final log = measure(buildRoute(name, screen: screen), screen);
final page = log.firstWhere((entry) => entry.path == '/Navigator/$name');
print('screen=$screen $name request=${pageRequests[name]} ${page.sizing} width=${page.width} constraint=${page.limit.bounds()}');
}
}
}
screen=360 Orders request=200 forced width=360 constraint=360..360 screen=360 Profile request=480 forced width=360 constraint=360..360 screen=360 Settings request=120 forced width=360 constraint=360..360 screen=280 Orders request=200 forced width=280 constraint=280..280 screen=280 Profile request=480 forced width=280 constraint=280..280 screen=280 Settings request=120 forced width=280 constraint=280..280
The focus nodes are the pages opened by a link: /Navigator/Orders, /Navigator/Profile, and /Navigator/Settings. In all six of six lines the sizing is forced, the width equals the screen width, and the constraint equals the range screen..screen. The request of the page does not enter the record; the lesson’s own line prints the request value in the output separately. Profile, which asks for 480, comes down to 280 on a screen of 280. A deep link does not give the page a different constraint: since the container stays fixed at the screen width whichever way it is reached, how the page entered the stack does not show up in the width.
AS1. The model measures only the width. The height of the page, the transition animation, the top bar of the container, and the preservation of state on the way back are not in the model. The model builds this sizing’s rule; the model is not the framework itself. Counting the navigator as fixed at the screen width is an assumption of the lesson; a real container can give a page on the stack its constraint differently.
This result is a contract for the page author: the page can expect a width for the widgets inside it, but since the container gives the page its own width, the page cannot adjust itself by asking to be wider or narrower than the screen. The place inside the page where the request carries meaning is the children of the page; they are measured with the constraint the page gives.
Summary
- This lesson’s sizing: forced. The focus node is
/Navigator/Orders, and with the360..360constraint the screen gives, it receives 360 even though the page’s request is 200. - A route is placed on the stack or removed from it; the width of the page comes from the navigator on every opening and does not change with the route.
- Three pages asked for 200, 480, and 120, and all three came out forced at 360.
- A page opened by a deep link received the same constraint as a page placed on the stack from the home screen; when the screen came down to 280, the width came down to 280.
- The rule sees the owner of the width, not the lifetime of the page: however many times the page is opened and closed, the record is the same.
Next Step
In this lesson, the navigator imposed the width of the page, and the page’s request did not enter the result. The next lesson, Networking and Serialization, measures the case where the request comes from the server: it looks at what the rule does to a request that comes from outside when it falls outside the range.
To keep your progress and take notes, Log in
My notes
Log in to take notes.