Lesson 06 / 19
Layout Widgets
Column, row, and flexible child are the layout widgets that pass the constraint down. The lesson separates tight and loose flexible children and shows by measurement that being a flexible child does not make the widget inside it stretched.
Published Updated
In this lesson
- Explain which constraint a column and a row give their children
- Tell apart how a tight and a loose flexible child use the share
- Measure that being a flexible child does not mean the widget inside it is stretched
Contents
In the previous lesson we saw that the render object tree passes the constraint down. In the tree, this passing is done most by the layout widgets: column, row, and flexible child. These widgets show nothing on screen themselves; they decide which range will be given to their children.
Column and Row
A column lays its children out one below another. It loosens the constraint that reaches it and gives each child the range 0..largest. Its own width is the widest of its children.
A row lays its children out side by side. Here there is a difference: the row’s width is shared among the children. A non-flexible child is given 0..inf, meaning the child can be as wide as it likes. Flexible children share the remaining space. The core of the lesson is again the first lesson’s sizing.dart file.
// 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}');
}
DC1. In the model, the row places the non-flexible children first and then distributes the remaining share to the flexible children. The framework’s main axis alignment and cross axis options are not modeled; the model measures only the width.
The Flexible Child
A flexible child is a child that takes a slice of the row’s remaining share. The size of the slice is determined by the flex factor: a child with a factor of 2 takes twice the share of a child with a factor of 1. In the framework a flexible child comes in two forms, and the difference lies in how they use the share.
// Platform code: flexible children
Row(
children: [
const Icon(Icons.person),
const Expanded(child: Text('Full Name')),
const Flexible(child: Text('Note')),
],
)
Expanded gives the child the whole share as tight: the child must fill the share. Flexible gives the share as loose: the child picks any width it likes inside the share and may take less. Their counterparts in the model are the calls flex(..., tight: true) and flex(..., tight: false).
DC2. In the model, a flexible child knows nothing except its factor and whether it is tight or loose. Any other rule the framework applies to a flexible child, for example overflow behavior, is not in the model.
The Tight Flexible Child
In a row, the icon on the left is a fixed 48 wide, and the name next to it is a tight flexible child.
// main.dart
import 'sizing.dart';
void main() {
final tree = pad('Screen', 16, row('Line', [
fixed('Icon', 48, leaf('Glyph', 48)),
flex('Name', 1, fill('Text')),
]));
printSizes(measure(tree, 360));
}
forced: /Screen width=360 constraint=360..360 forced: /Screen/Line width=328 constraint=328..328 chosen: /Screen/Line/Icon width=48 constraint=0..inf forced: /Screen/Line/Icon/Glyph width=48 constraint=48..48 forced: /Screen/Line/Name width=280 constraint=280..280 forced: /Screen/Line/Name/Text width=280 constraint=280..280
The row places the non-flexible child first: Icon requests 48 and chooses 48. What remains is 328 minus 48, that is, 280. Name, the only flexible child, takes the whole share as a tight constraint. A tight constraint determines the width whatever the child’s request is; that is why both Name and the Text inside it are forced.
The Loose Flexible Child
Now we put a loose flexible child in the same row in place of the name.
// main.dart
import 'sizing.dart';
void main() {
final tree = pad('Screen', 16, row('Line', [
fixed('Icon', 48, leaf('Glyph', 48)),
flex('Note', 1, fill('Text'), tight: false),
]));
printSizes(measure(tree, 360));
}
forced: /Screen width=360 constraint=360..360 forced: /Screen/Line width=328 constraint=328..328 chosen: /Screen/Line/Icon width=48 constraint=0..inf forced: /Screen/Line/Icon/Glyph width=48 constraint=48..48 chosen: /Screen/Line/Note width=280 constraint=0..280 stretched: /Screen/Line/Note/Text width=280 constraint=0..280
The share is again 280, but this time it is given to Note as 0..280. Because Text reports no request, it fills the upper bound and is stretched. Note takes the 280 that its child reported as a value chosen inside the range: chosen. Note, the flexible child, is not itself stretched; the stretched one is the Text inside it.
If what is inside the loose child is a leaf that reports a request, the situation changes.
// main.dart
import 'sizing.dart';
void main() {
final tree = pad('Screen', 16, row('Line', [
fixed('Icon', 48, leaf('Glyph', 48)),
flex('Note', 1, leaf('Text', 120), tight: false),
]));
printSizes(measure(tree, 360));
}
forced: /Screen width=360 constraint=360..360 forced: /Screen/Line width=328 constraint=328..328 chosen: /Screen/Line/Icon width=48 constraint=0..inf forced: /Screen/Line/Icon/Glyph width=48 constraint=48..48 chosen: /Screen/Line/Note width=120 constraint=0..280 chosen: /Screen/Line/Note/Text width=120 constraint=0..280
Text requests 120 and chooses 120 in the range 0..280; Note takes as much space as its child, that is, 120. Although the share is 280, both stay at 120. A loose flexible child gives the right to use the share; it does not bring an obligation to use it.
These three measurements show a distinction: being a flexible child and being stretched are not the same thing. A flexible child is a layout role of the row. Stretched is a class that describes how a node gets its width: there is no request, the upper bound is finite, and the node fills it. The tight flexible child came out forced and the loose flexible child came out chosen; the stretched one was the leaf inside the loose child that had no request.
The Bounding Measurement
The focus node is the text inside the loose flexible child: /Screen/Line/Note/Text. We measure the same row at two screen widths.
// main.dart
import 'sizing.dart';
void main() {
for (final width in [480, 320]) {
print('screen: $width');
final tree = pad('Screen', 16, row('Line', [
fixed('Icon', 48, leaf('Glyph', 48)),
flex('Note', 1, fill('Text'), tight: false),
]));
printSizes(measure(tree, width));
}
}
screen: 480 forced: /Screen width=480 constraint=480..480 forced: /Screen/Line width=448 constraint=448..448 chosen: /Screen/Line/Icon width=48 constraint=0..inf forced: /Screen/Line/Icon/Glyph width=48 constraint=48..48 chosen: /Screen/Line/Note width=400 constraint=0..400 stretched: /Screen/Line/Note/Text width=400 constraint=0..400 screen: 320 forced: /Screen width=320 constraint=320..320 forced: /Screen/Line width=288 constraint=288..288 chosen: /Screen/Line/Icon width=48 constraint=0..inf forced: /Screen/Line/Icon/Glyph width=48 constraint=48..48 chosen: /Screen/Line/Note width=240 constraint=0..240 stretched: /Screen/Line/Note/Text width=240 constraint=0..240
This lesson’s sizing: stretched. /Screen/Line/Note/Text reports no request, its upper bound is finite, and it fills that bound: 400 on a 480 screen, 240 on a 320 screen. The width is determined neither by Text‘s request nor by its own choice; it is determined by its parent’s share. Note, on the other hand, is chosen in both, because the width its child reported stays inside the range.
Summary
- This lesson’s sizing: stretched. The text inside the loose flexible child reports no request and fills the share: 280 on a 360 screen, 400 on a 480 screen, 240 on a 320 screen.
- A column loosens the constraint and makes the widest of its children its own width; a row gives non-flexible children
0..infand distributes the remaining share to the flexible children. - A tight flexible child takes the whole share as a tight constraint and is forced; a loose flexible child takes the share as an upper bound.
- If what is inside a loose flexible child is a fixed leaf, the share is left unused; the child is chosen.
- Being a flexible child is a layout role; being stretched is a class of how the width is obtained. The two are not the same thing.
Next Step
The last lesson of this topic takes up building the same interface with different platform widget families. The Platform Widget Families lesson measures the widget sets of two design languages and how a fixed slot protects the width of an action button.
To keep your progress and take notes, Log in
My notes
Log in to take notes.