---
title: Flutter
source: 'https://academia.sh/en/courses/flutter'
language: en
updated: '2026-10-21T05:02:51+00:00'
license: 'CC BY-SA 4.0'
lessons: 19
outcomes:
    - 'Writes applications with the core constructs of the Dart language'
    - 'Explains the widget tree and the rendering model'
    - 'Compares state management approaches and chooses between them'
---

# Flutter

Flutter is a framework that paints a widget tree written in Dart onto the screen with its own rendering engine; layout runs in a single pass: constraints go down the tree, sizes go up. The course measures every topic with a single question — who decides this widget's width? A width is forced, chosen, clamped or stretched; Dart fundamentals, stateful widgets, the three-tree model, state management, navigation, data and storage layers, animations, platform channels, testing, performance and release all pass through the same frame.

## Dart and the Component Model

Dart fundamentals and asynchronous Dart, the widget concept, stateless and stateful widgets, the three-tree model, layout widgets and the constraint flow, platform widget families.

### Dart Fundamentals

Source: https://academia.sh/en/courses/flutter/dart-fundamentals

A small Dart core that classifies who decides a widget's width. The lesson teaches Dart's types, null safety, and classes through that core and introduces the four sizings: forced, chosen, clamped, stretched.

This course opens a UI framework with a single question: **who determines this widget's width?** Here a **widget** is the piece of interface that takes up space on screen: a text, an image, a button, or a container that holds other widgets. The word keeps this meaning throughout the course; it does not mean a module or a service in software architecture.

Every widget receives a **constraint** from its parent: the smallest and largest width it may take. The widget may or may not have a request of its own. The combination of the constraint and the request produces the width, and the owner of the width is one of four parties. The names stay the same throughout the course; the output prints them in lowercase, and the prose uses the same words in bold.

- **forced**: the constraint is tight, meaning the smallest and largest width are equal. The widget's request cannot change the result.
- **chosen**: the widget's request stays inside the constraint, and the widget chooses the width.
- **clamped**: the request falls outside the constraint, or the constraint is unbounded and there is no request. The constraint cuts the width.
- **stretched**: there is no request, the constraint has an upper bound, and the widget grows up to that bound.

In every sizing except chosen, the width is determined not by the widget but by the constraint from above. The total of those three is called the **parent** count; the last line of the output counts it.

#### The Layout Contract

Three sentences say where the constraint comes from. Constraints go down: the parent gives the child the range it will accept. Sizes go up: the child picks a width inside that range and reports it to the parent. The parent sets the position: the parent, not the child, decides where the child will sit. This course deals only with the first two sentences, on the width axis.

**DC1.** The model builds the layout contract on the width axis only: each node receives a single `min..max` range and returns a single integer width. Height, position, and painting are not in the model; the model is not the framework itself.

#### The Core Model

The classifier is a single Dart file, `sizing.dart`. Throughout the course every lesson carries it as its first Dart block, and its content never changes; in later lessons, instead of rereading it, we look only at the trees added on top. This lesson opens the file line by line, because Dart's basic constructs work together here.

```dart
// 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}');
}
```

**Types and null safety.** In the `Limit` class, the `min` field has type `int` and the `max` field has type `int?`. In Dart 3.13.5, a type does not accept `null`; if it does, it carries a `?` at the end of its name. A `null` value for `max` means "no upper bound", and `bounds()` writes it as `inf`. Giving a nullable value to a place that is not nullable is a compile error. An exclamation mark after a value tells the compiler "this value is not `null` here"; the core uses it only where a `null` check has already been made. `max ?? 'inf'` picks the right-hand side if `max` is `null`.

**Classes and immutability.** The fields of the `Limit` and `Part` classes are `final` and their constructors are `const`, so the objects cannot change after they are built. `loosen()` and `deflate()` do not change the existing constraint; they return a new one. `bool get tight => max == min;` is a **getter**: it takes no parentheses when called and is read as `limit.tight`.

**Parameters.** The first four parameters of the `Part` constructor are positional. `factor` and `tightFlex`, inside the braces, are **named** parameters; they have default values and are given by name in the call: `flex('Note', 1, fill('Text'), tight: false)`. Helper functions such as `leaf`, `fill`, and `pad` build the tree with short names. `fill` leaves the width request as `null`: "I grow as wide as I can get".

**Collections and expressions.** `[for (final _ in part.children) <Sizing>[]]` builds an empty list for each child. `log..add(own)..addAll(below)` is a **cascade** call; each `..` applies a new operation to the same object. `~/` is integer division. The `case` branches inside `switch` do not carry `break`; each branch ends in its own block.

##### Rule order

The `classify` function is the only place where sizing is decided, and it asks its questions in this order:

1. If the constraint is tight, the result is forced.
2. If there is no request: stretched if the upper bound is finite, clamped if it is unbounded.
3. If the request is below the smallest width or above the upper bound, clamped.
4. Otherwise, chosen.

**DC2.** The tight constraint is asked first, because if the constraint allows a single width, the widget's request cannot change the result. This order is the model's decision; it is possible to build the classification in another order, but the name in the output carries meaning only with this one.

#### The Opening Layout

The tree below is the course's binding opening example. The root `Screen` leaves 16 of padding on every side and holds a `Body` column. The column has `Title`, which requests 120, `Banner`, which requests 400, and a row named `Toolbar`. The row has `Back` and `Menu`, which request 48 each, and between them `Search` with a flex factor of 1; `Field` inside `Search` requests no width.

```dart
// main.dart
import 'sizing.dart';

void main() {
  final screen = pad('Screen', 16, column('Body', [
    leaf('Title', 120),
    leaf('Banner', 400),
    row('Toolbar', [
      leaf('Back', 48),
      flex('Search', 1, fill('Field')),
      leaf('Menu', 48),
    ]),
  ]));

  final log = measure(screen, 360);
  printSizes(log);
  countSizings(log);
}
```

```text
forced: /Screen width=360 constraint=360..360
forced: /Screen/Body width=328 constraint=328..328
chosen: /Screen/Body/Title width=120 constraint=0..328
clamped: /Screen/Body/Banner width=328 constraint=0..328
stretched: /Screen/Body/Toolbar width=328 constraint=0..328
chosen: /Screen/Body/Toolbar/Back width=48 constraint=0..inf
forced: /Screen/Body/Toolbar/Search width=232 constraint=232..232
forced: /Screen/Body/Toolbar/Search/Field width=232 constraint=232..232
chosen: /Screen/Body/Toolbar/Menu width=48 constraint=0..inf
forced: 4
chosen: 3
clamped: 1
stretched: 1
parent: 6
```

The output applies the rules in order. `Screen` receives a tight constraint of 360, so it is forced. `pad` subtracts 16 from each side; a tight constraint of 328 goes down to `Body`, and `Body` is forced as well. The column loosens the constraint: the children receive `0..328`. `Title` requests 120 and stays inside the range, so it is chosen. `Banner` requests 400 while the upper bound is 328; the constraint cuts its width and it is clamped. `Toolbar` requests no width, and since its upper bound is 328, it is stretched. The row gives the non-flexible children `0..inf`; in this range `Back` and `Menu` choose 48. The remaining 328 - 48 - 48 = 232 goes down to `Search`, whose flex factor is 1, as a tight constraint, and `Search` and `Field` become forced.

The count gives the four sizings and the parent total: 4 forced, 3 chosen, 1 clamped, 1 stretched; the parent count is 4 + 1 + 1 = 6. One detail should be noted now: `Search` is a flexible child, but it is forced, not stretched. The stretched one is `Toolbar`. The two are not the same thing; the topic's sixth lesson opens this distinction.

#### The Widget Class

In Dart, a widget is a class. The piece below shows the smallest widget, derived from the framework's stateless widget class. The piece is not measured; the model does not run the framework's code.

```dart
// Platform code: widget class
class TitleLabel extends StatelessWidget {
  const TitleLabel({super.key, required this.text});
  final String text;

  @override
  Widget build(BuildContext context) {
    return Text(text);
  }
}
```

The class derives from `StatelessWidget` (`extends`). `required` makes the named parameter mandatory; the widget cannot be built without `text`. The `build` method returns what the widget will show as another widget, here a `Text`. `@override` declares that it redefines a method of the superclass.

The class itself says nothing about width. `build` only describes what will be shown; the width is determined by where the widget stands in the tree and the constraint that comes down to it. This course's job is to measure that second half.

#### The Absence of a Request

The `int?` in the core is not only a convenience; it carries the distinction between "no request" and "a request of zero". The block below builds two leaves, prints their requests, and measures the two in a column.

```dart
// main.dart
import 'sizing.dart';

void main() {
  final label = leaf('Label', 120);
  final space = fill('Space');
  print('Label request: ${label.width}');
  print('Space request: ${space.width}');

  final screen = pad('Screen', 16, column('Body', [label, space]));
  printSizes(measure(screen, 360));
}
```

```text
Label request: 120
Space request: null
forced: /Screen width=360 constraint=360..360
forced: /Screen/Body width=328 constraint=328..328
chosen: /Screen/Body/Label width=120 constraint=0..328
stretched: /Screen/Body/Space width=328 constraint=0..328
```

`Label` requests 120 and is chosen. `Space` requests nothing; because its request is `null`, with the constraint at `0..328` it grows up to the upper bound and is stretched. If `null` and `0` were the same thing, these two lines could not be told apart.

#### The Bounding Measurement

A principle is tested only by a measurement that could refute it. This time we measure the opening tree at two screen widths, and we filter and print only the focus node, `/Screen/Body/Banner`. The tree does not change; only the root constraint does. The last lines give the count at 480.

```dart
// main.dart
import 'sizing.dart';

Part buildScreen() {
  return pad('Screen', 16, column('Body', [
    leaf('Title', 120),
    leaf('Banner', 400),
    row('Toolbar', [
      leaf('Back', 48),
      flex('Search', 1, fill('Field')),
      leaf('Menu', 48),
    ]),
  ]));
}

void main() {
  for (final screenWidth in [480, 360]) {
    print('screen=$screenWidth');
    final log = measure(buildScreen(), screenWidth);
    printSizes(log.where((entry) => entry.path == '/Screen/Body/Banner').toList());
  }
  print('count screen=480');
  countSizings(measure(buildScreen(), 480));
}
```

```text
screen=480
chosen: /Screen/Body/Banner width=400 constraint=0..448
screen=360
clamped: /Screen/Body/Banner width=328 constraint=0..328
count screen=480
forced: 4
chosen: 4
clamped: 0
stretched: 1
parent: 5
```

**This lesson's sizing: clamped.** At a width of 480, `Banner`'s request of 400 stays inside the range `0..448`, and `Banner` chooses the width. At 360 the same request of 400 does not fit in the range `0..328`, and the constraint cuts the width. The widget, the tree, and the request are the same; the owner of the result changes only because the root constraint changes. The count at 480 confirms this: clamped is zero, and chosen rises to four.

**DC3.** `Banner`'s request of 400 is a ready-made number in the model. The width of a real image is often known only after it has loaded; the model treats it as a request known in advance. The case where the width arrives later is the subject of the next lesson.

#### Summary

- This lesson's sizing: clamped. `/Screen/Body/Banner` requests 400, the constraint gives `0..328` on a 360 screen, and the constraint cuts the width.
- In Dart, `int?` writes the possibility of "no value" into the type; giving `null` to a non-nullable type is a compile error, and the core carries the "no request" case with `int?`.
- The layout contract is three sentences: constraints go down, sizes go up, the parent sets the position.
- The owner of a width is one of four parties: forced, chosen, clamped, stretched. The opening tree has 4 forced, 3 chosen, 1 clamped, 1 stretched; the parent count is 6.
- The same tree gives a chosen result at 480 and a clamped result at 360: the owner of a width is not a property of the widget alone, but the combination of the widget and the constraint.

#### Next Step

`Banner`'s width was a ready-made number in this lesson. In real applications such a value often does not arrive right away. The **Asynchronous Dart** lesson builds the case where the width arrives later with `Future` and `await`, and measures which sizing the same widget gets before the value arrives and after.

### Asynchronous Dart

Source: https://academia.sh/en/courses/flutter/asynchronous-dart

Two measurements of a widget whose width arrives later. The lesson explains futures, async and await, streams, the event loop, and isolates through the order in which the tree is built.

In the previous lesson, `Banner`'s request of 400 was a ready-made number. On a real screen, a widget's width often arrives later: an image's size is read, a file's content is decoded, a response is awaited. Until then the tree still has to be built. The question of this lesson is who determines the same widget's width before the value arrives and after.

Dart answers this situation with the **future** (`Future`) type. A `Future<int>` is a value that does not exist now and will complete later as an `int` or as an error. A function marked `async` always returns a future; `await` holds the rest of that function until a future completes, and the caller continues its own flow in the meantime.

#### The Tree and the Helper File

Throughout the lesson the tree is built from a single place. The `buildBody` function uses `fill('Avatar')` if the width is unknown and `leaf('Avatar', width)` if it is known. `printAvatar` measures the tree at a width of 360 and prints only the `/Screen/Body/Avatar` line. This file has no output and contains no `main`; the blocks below import it.

```dart
// 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}');
}
```

```dart
// avatar_body.dart — builds the column tree from the avatar width and measures the avatar
import 'sizing.dart';

Part buildBody(int? width) {
  final avatar = width == null ? fill('Avatar') : leaf('Avatar', width);
  return pad('Screen', 16, column('Body', [leaf('Name', 120), avatar]));
}

void printAvatar(int? width) {
  final log = measure(buildBody(width), 360);
  printSizes(log.where((entry) => entry.path == '/Screen/Body/Avatar').toList());
}
```

The `int? width` parameter carries the idea from the first lesson: `null` says the width is not yet known. `width == null ? fill('Avatar') : leaf('Avatar', width)` is a **conditional expression**; in the second branch, `width` can be used as an `int` when it is not `null`, because the compiler tracks the `null` check.

**DC1.** The model has no waiting time, only order. `loadWidth` completes immediately; what is measured is the two trees, before and after the moment the value arrives. The model is not the framework itself.

#### Futures and await

The block below builds a future, shows that the caller continues its flow, and receives the value with `await`. Each step prints one line.

```dart
// main.dart
import 'sizing.dart';
import 'avatar_body.dart';

Future<int> loadWidth() async {
  print('loading started');
  return 200;
}

Future<void> main() async {
  print('request sent');
  final pending = loadWidth();
  print('continuing without waiting');
  final width = await pending;
  print('value arrived: $width');
  printAvatar(width);
}
```

```text
request sent
loading started
continuing without waiting
value arrived: 200
chosen: /Screen/Body/Avatar width=200 constraint=0..328
```

When `loadWidth` is called, its body runs immediately up to the first `await` or to the end; that is why the `loading started` line comes before `continuing without waiting`. The future that `loadWidth` returns goes into the `pending` variable. When execution reaches `await pending`, `main` stops and resumes where it left off once the value arrives. The value has arrived, `Avatar` requests 200, and because that stays inside the range, it is chosen.

#### The Event Loop

Dart code runs on a single thread, driven by an **event loop**. Running code continues to its end. Then the loop looks at two queues: it first empties the **microtask queue**, then takes one task from the **task queue**. An `await` or a `Future(...)` call leaves its continuation in one of these queues. The block below shows the order with numbers.

```dart
// main.dart
import 'sizing.dart';
import 'dart:async';
import 'avatar_body.dart';

void main() {
  print('1 synchronous: first build');
  printAvatar(null);
  scheduleMicrotask(() {
    print('3 microtask');
    printAvatar(200);
  });
  Future(() {
    print('4 task queue');
    printAvatar(320);
  });
  print('2 synchronous: end of main');
}
```

```text
1 synchronous: first build
stretched: /Screen/Body/Avatar width=328 constraint=0..328
2 synchronous: end of main
3 microtask
chosen: /Screen/Body/Avatar width=200 constraint=0..328
4 task queue
chosen: /Screen/Body/Avatar width=320 constraint=0..328
```

The numbers belong to the running order, not to the order in which the code is written. No queue is processed before `main` finishes; that is why line 2 is printed before 3. In the first measurement there is no value and `Avatar` is stretched. The microtask makes the value 200, the job in the task queue makes it 320; in both, the request is inside the range and `Avatar` is chosen.

#### Errors and Streams

A future can complete not only with a value but also with an error. `await` throws that error to the caller, and `try`/`catch` catches it. In the block below the load fails, the function returns to a fallback width, and the tree is built with that width. Because the error is caught, it does not spread through the program; in the output, the single line announcing the fallback width is followed by the measurement of the tree.

```dart
// main.dart
import 'sizing.dart';
import 'avatar_body.dart';

Future<int> loadWidth() async {
  throw StateError('no connection');
}

Future<int> loadWidthOrFallback() async {
  try {
    return await loadWidth();
  } catch (_) {
    print('load failed, using 96');
    return 96;
  }
}

Future<void> main() async {
  final width = await loadWidthOrFallback();
  printAvatar(width);
}
```

```text
load failed, using 96
chosen: /Screen/Body/Avatar width=96 constraint=0..328
```

A future carries a single value. For several values spread over time, Dart uses the **stream** (`Stream`) type. A function marked `async*` produces values one after another with `yield`; an `await for` loop receives each value in the order it arrives.

```dart
// main.dart
import 'sizing.dart';
import 'avatar_body.dart';

Stream<int> widths() async* {
  yield 200;
  yield 320;
  yield 400;
}

Future<void> main() async {
  await for (final width in widths()) {
    print('stream value: $width');
    printAvatar(width);
  }
}
```

```text
stream value: 200
chosen: /Screen/Body/Avatar width=200 constraint=0..328
stream value: 320
chosen: /Screen/Body/Avatar width=320 constraint=0..328
stream value: 400
clamped: /Screen/Body/Avatar width=328 constraint=0..328
```

Each new value brings a new measurement of the tree. The first two values stay in the range `0..328` and are chosen; the third is 400, so the constraint clamps it. On the same widget, in the same place, the owner of the sizing changes as the value flows.

**DC2.** In the model, the stream consists of three fixed values. A real stream spreads its values over time; the model keeps only the order, and there is no waiting between the values.

#### The Isolate

The event loop shares out work but does not run it in parallel: if a heavy computation holds the loop, the queues cannot be processed and the interface freezes. For this Dart offers the **isolate**. An isolate is a separate execution unit with its own memory and its own event loop; it shares no memory with the main unit and only exchanges messages. The piece below moves a heavy parse to a separate unit. The piece is not measured; the model does not build the isolate.

```dart
// Platform code: isolate
import 'dart:isolate';

Future<int> parseInBackground(String raw) {
  return Isolate.run(() => int.parse(raw));
}
```

The return type is again a `Future<int>`. For the caller there is no difference: the value comes later, and the tree is built, as in the two measurements above, before the value arrives and after. Building the tree stays in the main unit; the isolate carries only the work that computes the width.

#### The Bounding Measurement

The block below measures the whole tree before a widget's width arrives and after, and gives the count for each measurement. The focus is `/Screen/Body/Avatar` in the second measurement.

```dart
// main.dart
import 'sizing.dart';
import 'avatar_body.dart';

Future<int> loadWidth() async => 200;

Future<void> main() async {
  print('measurement 1: before the value arrives');
  final before = measure(buildBody(null), 360);
  printSizes(before);
  countSizings(before);

  final width = await loadWidth();
  print('measurement 2: after the value arrives');
  final after = measure(buildBody(width), 360);
  printSizes(after);
  countSizings(after);
}
```

```text
measurement 1: before the value arrives
forced: /Screen width=360 constraint=360..360
forced: /Screen/Body width=328 constraint=328..328
chosen: /Screen/Body/Name width=120 constraint=0..328
stretched: /Screen/Body/Avatar width=328 constraint=0..328
forced: 2
chosen: 1
clamped: 0
stretched: 1
parent: 3
measurement 2: after the value arrives
forced: /Screen width=360 constraint=360..360
forced: /Screen/Body width=328 constraint=328..328
chosen: /Screen/Body/Name width=120 constraint=0..328
chosen: /Screen/Body/Avatar width=200 constraint=0..328
forced: 2
chosen: 2
clamped: 0
stretched: 0
parent: 2
```

**This lesson's sizing: chosen.** Before the value arrives, `Avatar` reports no request; since the column's constraint is `0..328`, it grows as wide as it can get and is stretched. When the value arrives, `Avatar` requests 200, 200 is inside the range, and `Avatar` chooses the width. Between the two measurements only `Avatar`'s request has changed in the tree, and the sizing changes hands at this single node. The count summarizes this: the parent count falls from 3 to 2.

#### Summary

- This lesson's sizing: chosen. Before the value arrives, `/Screen/Body/Avatar` is stretched; when 200 arrives, its request stays in the range `0..328` and `Avatar` chooses the width.
- `Future<int>` is a value that will complete later. An `async` function returns a future, `await` holds the function until the value arrives, and the caller continues in the meantime.
- The event loop finishes synchronous code, empties the microtask queue, then takes one task from the task queue. The numbers in the output show this order.
- A failed future is caught with `try`/`catch` and turned into a fallback value. A stream carries values spread over time, and each value causes the tree to be measured again.
- The isolate runs heavy work in a separate memory and returns the result as a future again.

#### Next Step

The tree so far was a composition of `leaf`, `pad`, and `column` calls. The **Widget Concept** lesson connects these calls to the framework's widget concept: what a widget is, how it nests in the tree, and why a card meets a different owner under a different constraint.

### The Widget Concept

Source: https://academia.sh/en/courses/flutter/the-widget-concept

In an interface model where everything is a widget, padding is a widget too. The lesson measures that a widget is an immutable recipe and why a card, despite the same request, meets a different owner depending on its position.

The previous two lessons used the widget as a tree node: `pad`, `column`, and `leaf` calls nested inside one another and built a screen. This lesson says what that node is. In this interface model nearly everything you see on screen is a **widget**: text, image, button; but also padding, alignment, fixed size, and column. Widgets take other widgets as children, and the tree is formed from this nesting.

#### Nearly Everything Is a Widget

In many interface tools, padding is a property of a widget: a card has a `margin` or `padding` setting. Here, padding is a widget in its own right and wraps its child. Alignment and fixed size are the same. First comes the core of the lesson; this is the first lesson's `sizing.dart` file, and it has not changed.

```dart
// 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 piece below puts a card inside a padding widget. The piece is not measured; the model does not run the framework's code.

```dart
// Platform code: padding widget
Widget buildCard() {
  return const Padding(
    padding: EdgeInsets.all(16),
    child: Card(child: Text('Summary')),
  );
}
```

`Padding` wraps the card and leaves 16 units of space on every side; the card is `Padding`'s `child` parameter. In the model, this piece corresponds to the call `pad('Screen', 16, leaf('Card', 200))`.

**DC1.** The model builds padding, alignment, and fixed size as nodes (`pad`, `center`, `fixed`). It does not offer the framework's whole widget list; the model is not the framework itself.

**DC2.** In the model, `Card` is only a leaf that requests a width. The card has no content, shadow, or corners; what remains is the one thing that can be measured, the request of 200 units.

Because padding is a widget, it can be stacked. The block below nests two paddings and shows how much each narrows the card's constraint.

```dart
// main.dart
import 'sizing.dart';

void main() {
  final tree = pad('Outer', 8, pad('Inner', 8, leaf('Card', 200)));
  printSizes(measure(tree, 360));
}
```

```text
forced: /Outer width=360 constraint=360..360
forced: /Outer/Inner width=344 constraint=344..344
forced: /Outer/Inner/Card width=328 constraint=328..328
```

Each `pad` subtracts its own value from both sides: 16 is taken from the tight constraint of 360 to give 344, and from that again to give 328. None of the widgets "computes" anything on its own; each takes the constraint, gives its child a modified version, and processes the child's answer.

#### A Widget Is a Recipe

A widget object is not the thing standing on screen but its **recipe**. The object is built once and does not change: its fields are `final` and its constructor is `const`. If something is to change, a new widget is built and the old one is discarded. This choice has two consequences.

The first is that building a widget is cheap. Because the recipe does not carry the screen itself, rebuilding a screen's recipe does not mean repainting the screen. The second is that the same widget object can stand in several places. The object carries no position information; the position is given by its place in the tree, and the constraint of that place comes down to it everywhere. The measurements below show this second consequence.

#### Constraints Down, Sizes Up

A tree has two flows, and they go in opposite directions. **Constraints go down**: the parent gives the child the range it will accept. **Sizes go up**: the child picks a width in that range and reports it to the parent. The parent takes this size, determines its own width accordingly, and positions the child.

```dart
// main.dart
import 'sizing.dart';

void main() {
  final tree = pad('Screen', 16, center('Center', column('Body', [
    leaf('A', 100),
    leaf('B', 250),
  ])));
  printSizes(measure(tree, 360));
}
```

```text
forced: /Screen width=360 constraint=360..360
forced: /Screen/Center width=328 constraint=328..328
chosen: /Screen/Center/Body width=250 constraint=0..328
chosen: /Screen/Center/Body/A width=100 constraint=0..328
chosen: /Screen/Center/Body/B width=250 constraint=0..328
```

A tight constraint of 328 goes down from `Screen` to `Center`. `Center` loosens this constraint and gives `Body` `0..328`; the same loose range reaches `A` and `B` too. The sizes that go up are these: `A` reports 100 and `B` reports 250; the column makes the widest of them, 250, its own width. The constraint has moved on by decreasing downward, and the size by growing upward.

In the UI Development with Jetpack Compose course, the question "which direction is this data going in the UI tree?" is asked, and the direction is the answer to the question. In this course the direction is fixed: the constraint goes down and the size goes up. Because the direction is the same in every tree, it does not tell the nodes apart; sizing does. Sizing classifies who determines the width at the end of those two flows. In the output above, `Body` is 250 wide because the widest of its children chose it; `Center` is 328 because its parent forced 328 on it.

#### The Bounding Measurement

The focus node is the card inside the padding: `/Screen/Card`. The card requests 200. `Screen` receives the tight constraint of 360; the padding narrows it by 16 on each side and passes it to the child so that it stays tight.

```dart
// main.dart
import 'sizing.dart';

void main() {
  final tree = pad('Screen', 16, leaf('Card', 200));
  printSizes(measure(tree, 360));
}
```

```text
forced: /Screen width=360 constraint=360..360
forced: /Screen/Card width=328 constraint=328..328
```

**This lesson's sizing: forced.** `/Screen/Card` requests 200 and receives 328..328. The constraint is tight, meaning the smallest and largest width are equal; the card's request does not change the result. The card is 328 wide, not 200, and it is `Screen` and the padding that gave it this width.

Next we put the same card inside a column. The column loosens the constraint, and the card can state its own request for the first time.

```dart
// main.dart
import 'sizing.dart';

void main() {
  final tree = pad('Screen', 16, column('Body', [leaf('Card', 200)]));
  printSizes(measure(tree, 360));
}
```

```text
forced: /Screen width=360 constraint=360..360
forced: /Screen/Body width=328 constraint=328..328
chosen: /Screen/Body/Card width=200 constraint=0..328
```

The card is the same card, its request the same 200. The `Body` column receives the tight constraint of 328 but gives its child `0..328`; in this range 200 is a valid choice, and `Card` chooses the width. The same widget, the same request, two different owners of the result: the position has determined the result.

#### Summary

- This lesson's sizing: forced. `/Screen/Card` requests 200, the padding passes the tight constraint on as 328..328, and the constraint determines the width.
- In this interface model, padding, alignment, and fixed size are widgets; the tree is built by nesting widgets.
- A widget is an immutable recipe: its fields are `final` and its constructor is `const`. The same object can stand in several places and receives that place's constraint everywhere.
- Constraints go down, sizes go up. Sizing does not classify the direction but the owner of the width at the end of these two flows.
- The same card is forced inside the padding and chosen inside the column: the result is determined not by the widget's request but by its position.

#### Next Step

A widget is an immutable recipe; so how is something that changes on screen shown? The **Stateless and Stateful Widgets** lesson separates the widget whose recipe never changes from the widget that carries its own state, and measures who gives a search box its width when the state changes.

### Stateless and Stateful Widgets

Source: https://academia.sh/en/courses/flutter/stateless-and-stateful-widgets

A stateless widget gives its recipe once; a stateful widget carries its own state in a separate object. The lesson measures that the recipe is rebuilt when the state changes and who gives a search box its width after a relayout.

A widget is an immutable recipe. So what changes when a search box opens and closes, or when a counter increases? Since the recipe does not change, whatever changes must live somewhere else. That place is the **state**, and widgets split into two kinds according to where they keep it.

#### The Stateless Widget

A **stateless widget** is built only from the parameters it receives. With the same parameters it gives the same recipe on every call, and it remembers nothing on its own. A title label, an icon, and the text of a row are like this. The core of the lesson is again the first lesson's `sizing.dart` file.

```dart
// 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}');
}
```

In the model we can write a stateless widget as an ordinary function that takes parameters and returns a tree. The function below produces the recipe for a search box; if the box is open, its width spreads over the empty space, and if it is closed, it reports a fixed request.

**DC1.** In the model, state is a field of a class, and the change is made with an ordinary method such as `toggle`. The framework's state-notification pattern (`setState`) is not measured; it is shown as a SOURCE piece.

```dart
// main.dart
import 'sizing.dart';

class SearchModel {
  bool expanded = false;
  void toggle() {
    expanded = !expanded;
  }
}

Part buildSearch(bool expanded) {
  final search = expanded ? fill('Search') : leaf('Search', 120);
  return pad('Screen', 16, column('Body', [search]));
}

void main() {
  final model = SearchModel();
  print('closed');
  printSizes(measure(buildSearch(model.expanded), 360));
  model.toggle();
  print('open');
  printSizes(measure(buildSearch(model.expanded), 360));
}
```

```text
closed
forced: /Screen width=360 constraint=360..360
forced: /Screen/Body width=328 constraint=328..328
chosen: /Screen/Body/Search width=120 constraint=0..328
open
forced: /Screen width=360 constraint=360..360
forced: /Screen/Body width=328 constraint=328..328
stretched: /Screen/Body/Search width=328 constraint=0..328
```

`buildSearch` is stateless: if the `expanded` value is the same, every call gives the same tree. The state lives in `SearchModel` and is outside the function. When the box is closed, `Search` requests 120 and receives 120 under a loose constraint. When open, there is no request, the upper bound is finite, and `Search` takes all 328.

#### The Stateful Widget

A **stateful widget** consists of two objects: the widget that carries the immutable recipe, and the state object that carries the state and keeps living in the tree. Each time the framework calls the state object's `build` method, it gets a new recipe. To change the state, `setState` is called; this call announces that the state has changed and directs the framework to call `build` again.

```dart
// Platform code: stateful widget
class SearchBox extends StatefulWidget {
  const SearchBox({super.key});

  @override
  State<SearchBox> createState() => _SearchBoxState();
}

class _SearchBoxState extends State<SearchBox> {
  bool expanded = false;

  void toggle() {
    setState(() {
      expanded = !expanded;
    });
  }

  @override
  Widget build(BuildContext context) {
    return expanded ? const TextField() : const SizedBox(width: 120);
  }
}
```

In this piece the recipe (`SearchBox`) never changes; the `expanded` value lives in the state object. When `toggle` is called, `build` returns a new subtree. Its counterpart in the model is the pair of `SearchModel` and `buildSearch`.

#### What Changes When the Recipe Changes

When the state changes, what is rebuilt is the recipe. In the model, when a state that does not affect size changes, for example the hint text inside the box, the structure and requests of the tree stay the same. In that case the measurement stays the same too.

```dart
// main.dart
import 'sizing.dart';

Part buildSearch(bool expanded, String hint) {
  final search = expanded ? fill('Search') : leaf('Search', 120);
  return pad('Screen', 16, column('Body', [search]));
}

void main() {
  for (final hint in ['Search', 'Search lessons']) {
    print('hint: $hint');
    printSizes(measure(buildSearch(true, hint), 360));
  }
}
```

```text
hint: Search
forced: /Screen width=360 constraint=360..360
forced: /Screen/Body width=328 constraint=328..328
stretched: /Screen/Body/Search width=328 constraint=0..328
hint: Search lessons
forced: /Screen width=360 constraint=360..360
forced: /Screen/Body width=328 constraint=328..328
stretched: /Screen/Body/Search width=328 constraint=0..328
```

`hint` is given to the function but does not enter the tree; the output of the two calls is identical. A state change triggers a rebuild, but if the rebuilt recipe reports the same request, none of the widths moves.

**DC2.** In the model, the hint text does not affect the width; in a real box, a very long hint text could change the box's request. That case is not modeled here, because the model does not measure text.

In the UI Development with SwiftUI course, what triggers the recalculation of the interface when state changes is asked: the changed value, the views that depend on it, and the chain of recalculation. In this lesson the question is not that one. Here the question "why was it rebuilt" is not asked; in the rebuilt tree, the question is **who gives** the width. Even when the trigger is the same, the owner of the result can change: the box above follows its own request when closed and its parent's constraint when open.

#### The Bounding Measurement

The focus node is the search box: `/Screen/Body/Search`. When open, the box reports no request, and the range above it is finite. We measure at two screen widths.

```dart
// main.dart
import 'sizing.dart';

Part buildSearch(bool expanded) {
  final search = expanded ? fill('Search') : leaf('Search', 120);
  return pad('Screen', 16, column('Body', [search]));
}

void main() {
  for (final width in [480, 320]) {
    print('screen: $width');
    printSizes(measure(buildSearch(true), width));
  }
}
```

```text
screen: 480
forced: /Screen width=480 constraint=480..480
forced: /Screen/Body width=448 constraint=448..448
stretched: /Screen/Body/Search width=448 constraint=0..448
screen: 320
forced: /Screen width=320 constraint=320..320
forced: /Screen/Body width=288 constraint=288..288
stretched: /Screen/Body/Search width=288 constraint=0..288
```

**This lesson's sizing: stretched.** When open, `/Screen/Body/Search` reports no request, the upper bound is 448 or 288, and the node fills it. The box's width changes with the screen width; in its closed state it is 120, which moves on no screen. The two states of the same node correspond to two different owners of the result: when closed, its own request (chosen); when open, the parent's upper bound (stretched).

#### Summary

- This lesson's sizing: stretched. The open search box reports no request, fills the parent's upper bound, and its width is 448 on a 480 screen and 288 on a 320 screen.
- A stateless widget is built only from parameters and gives the same recipe with the same parameters.
- A stateful widget carries its immutable recipe and its changing state in two separate objects; when the state changes, the recipe is rebuilt.
- If the rebuilt recipe reports the same request, the widths do not move; even when the hint text changes, the two measurements are the same.
- The same box is chosen when closed and stretched when open: when the state changes, the owner of the result can change too.

#### Next Step

When a widget's state changes, the framework rebuilds the tree, but it does not create the objects on screen from scratch. The **Three-Tree Model** lesson explains how the recipe, the living object, and the layout on screen connect to one another as three separate trees.

### The Three-Tree Model

Source: https://academia.sh/en/courses/flutter/the-three-tree-model

The framework keeps the same interface in three separate trees: the widget tree of recipes, the living element tree, and the render object tree that does the layout. The lesson explains what each tree does and measures why the layout result belongs only to the last tree.

In the previous lesson we saw that the recipe is rebuilt when the state changes. But if the objects on screen were created from scratch every time, a scroll position or the cursor in a text field would be lost at every rebuild. This is why the framework keeps the same interface in **three trees**, and each tree's lifetime runs at a different speed.

#### The Widget Tree

The first tree is the tree of the widgets you write. This tree consists of immutable recipes and is produced from scratch at every rebuild. It is cheap, because it answers only the question "what do I want": let there be a padding on screen, and inside it a centered logo.

The core of the lesson is again the first lesson's `sizing.dart` file; the model is the counterpart of this tree.

```dart
// 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 call `pad('Screen', 16, center('Center', fixed('Logo', 96, leaf('Image', 200))))` in the model is a widget tree. The nodes carry only a name, a kind, and a request; they carry no position or lasting state.

**DC1.** The model's tree is a single tree. The three trees in the framework are not built separately in the model; the work of the third tree, which is to pass the constraint down and the size up, is the `measure` call.

#### The Element Tree

The second tree is the **element tree**. When the framework sees a recipe for the first time, it creates an element that corresponds to it. The element holds that recipe's place in the tree and lives on: state objects are attached to elements. At a rebuild, the framework compares the new recipe with the old element. If the type and the key are the same, the element is kept and receives the new recipe; if they differ, the element is discarded and a new one is created.

This comparison explains why state survives. The state object of a stateful widget is attached to the element; as long as the element is kept, the state is kept. The `expanded` value of the search box in the previous lesson keeps living because the box's element lives.

#### The Render Object Tree

The third tree is the **render object tree**. The objects that compute the size, position the node, and paint it on screen are here. The flow in which the constraint goes down and the size comes up works in this tree. The piece below shows how a render object does the layout; it is outside the model and is not measured.

```dart
// Platform code: render object
class RenderLogo extends RenderBox {
  @override
  void performLayout() {
    size = constraints.constrain(const Size(96, 96));
  }
}
```

`performLayout` is where the object takes the constraint and picks its size. `constraints.constrain` squeezes the request into the constraint; if the request is inside the range, it stays as it is. The model's `classify` rule simplifies this behavior.

The three trees share the work like this: the widget says "what it wants", the element carries the information "who is who, whose state is whose", and the render object answers the question "how much space does it take". The tree that determines the width is always the last one.

#### The Same Node, a Different Constraint

Now we measure the same tree. `center` loosens the constraint, while `fixed` gives its child a tight width.

```dart
// main.dart
import 'sizing.dart';

Part logoTree(int imageWidth) {
  return pad('Screen', 16, center('Center', fixed('Logo', 96, leaf('Image', imageWidth))));
}

void main() {
  printSizes(measure(logoTree(200), 360));
}
```

```text
forced: /Screen width=360 constraint=360..360
forced: /Screen/Center width=328 constraint=328..328
chosen: /Screen/Center/Logo width=96 constraint=0..328
forced: /Screen/Center/Logo/Image width=96 constraint=96..96
```

`Center` receives a tight constraint of 328 from its parent and loosens it to `0..328` for `Logo`. `Logo` requests 96 and, since that is inside the range, chooses 96. `Image` inside `Logo` requests 200, but `fixed` gives it 96..96 and its width is forced.

Now we compare two things. First the recipe changes: the `Image` request falls from 200 to 120 and the paths stay the same, meaning the element tree is kept. Then the recipe stays the same and the screen falls from 360 to 320.

```dart
// main.dart
import 'sizing.dart';

Part logoTree(int imageWidth) {
  return pad('Screen', 16, center('Center', fixed('Logo', 96, leaf('Image', imageWidth))));
}

void main() {
  final first = measure(logoTree(200), 360);
  final newRecipe = measure(logoTree(120), 360);
  final newScreen = measure(logoTree(200), 320);
  final samePaths = [for (final e in first) e.path].join(',') ==
      [for (final e in newRecipe) e.path].join(',');
  print('kept paths: $samePaths');
  print('Image, new recipe: ${first[3].width} -> ${newRecipe[3].width}');
  print('Center, new screen: ${first[1].width} -> ${newScreen[1].width}');
  print('Logo, new screen: ${first[2].width} -> ${newScreen[2].width}');
}
```

```text
kept paths: true
Image, new recipe: 96 -> 96
Center, new screen: 328 -> 288
Logo, new screen: 96 -> 96
```

When the recipe changes, the nodes' paths stay the same, meaning the framework keeps the elements. `Image` has lowered its request to 120, but since `fixed` still forces 96, its width does not change. When the screen changes, the tree is the same and yet `Center` falls from 328 to 288: a kept node receives a different width when its constraint changes. `Logo` is 96 in both.

In the React Native course, what an update does to a node in the view tree is asked: was it kept, updated, mounted, or unmounted. That fate is a comparison of two trees. Sizing, on the other hand, is the layout of a single tree and can be asked without knowing the result of that comparison. In the output above we know that `Center` was kept; yet its width changes with the screen. Being kept does not guarantee the width.

**DC2.** An element being kept means only that its state and identity are kept. It does not mean that a node will keep the width from the previous measurement; the width is determined at every measurement by the constraint of that moment.

#### The Bounding Measurement

The focus node is `/Screen/Center/Logo`. We measure the same tree at two screen widths.

```dart
// main.dart
import 'sizing.dart';

Part logoTree() {
  return pad('Screen', 16, center('Center', fixed('Logo', 96, leaf('Image', 200))));
}

void main() {
  for (final width in [480, 320]) {
    print('screen: $width');
    printSizes(measure(logoTree(), width));
  }
}
```

```text
screen: 480
forced: /Screen width=480 constraint=480..480
forced: /Screen/Center width=448 constraint=448..448
chosen: /Screen/Center/Logo width=96 constraint=0..448
forced: /Screen/Center/Logo/Image width=96 constraint=96..96
screen: 320
forced: /Screen width=320 constraint=320..320
forced: /Screen/Center width=288 constraint=288..288
chosen: /Screen/Center/Logo width=96 constraint=0..288
forced: /Screen/Center/Logo/Image width=96 constraint=96..96
```

**This lesson's sizing: chosen.** `/Screen/Center/Logo` requests 96; because `Center` loosens the constraint, the range is `0..448` or `0..288`, and 96 is inside it. The node itself chooses the width. Even when the screen changes, `Logo` stays at 96, while `Center` gets 448 and 288. `Image` inside it is forced in this case: `fixed` gives it its width.

#### Summary

- This lesson's sizing: chosen. `/Screen/Center/Logo` requests 96 in a loosened range and chooses 96 at both screen widths.
- The framework keeps the same interface in three trees: the widget tree of immutable recipes, the element tree that holds the recipe's place, and the render object tree that does the layout.
- At a rebuild the recipe arrives new; if the type and key are the same, the element is kept, and the state is kept with it.
- The width is always determined by the render object tree; being kept does not guarantee the width.
- In the same tree `Center` gets 328, 448, and 288 while `Logo` stays at 96: a kept node can receive a different width when its constraint changes.

#### Next Step

In the render object tree, most of the widgets that pass the constraint on are layout widgets. The **Layout Widgets** lesson takes up column, row, and flexible children, and measures who gives the width to a widget inside a flexible child.

### Layout Widgets

Source: https://academia.sh/en/courses/flutter/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.

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.

```dart
// 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.

```dart
// 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.

```dart
// 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));
}
```

```text
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.

```dart
// 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));
}
```

```text
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.

```dart
// 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));
}
```

```text
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.

```dart
// 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));
  }
}
```

```text
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..inf` and 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.

### Platform Widget Families

Source: https://academia.sh/en/courses/flutter/platform-widget-families

The framework offers separate widget sets for the design languages of two platforms. The lesson measures the difference between the families, the choice by platform, and how the same action button, although it reports a different request in each family, gets the same width in a fixed slot.

In the previous lessons we saw that a widget is an immutable recipe, that the constraint comes down, and that layout widgets share the space. In this last lesson we look at which **set** the widgets come from. The framework offers separate widget sets that belong to the design languages of two platforms. The form of an action button in one set looks different from its form in the other, and it can have different content and a different request.

#### Two Design Languages, Two Sets

The first set implements the Material design language and is the default look on Android; the second set implements the design language of iOS. Both sets have counterparts of widgets such as the button, the switch, the dialog, and the navigation bar, but each carries the habits of its own platform: spacing, corner roundness, and touch behavior differ from set to set.

This difference also shows up in size. Two buttons built for the same job can report a different width request. The core of the lesson is again the first lesson's `sizing.dart` file.

```dart
// 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, a family is represented as the request the button reports: 88 in one family, 64 in the other. The button's shape, color, and touch behavior are not modeled; only the width request is measured.

#### Choosing by Platform

To show an interface naturally on both platforms, the app picks at run time which set to use. The framework supports this choice in two ways. The first is to ask for the platform and return the suitable widget. The second is **adaptive** widgets that wrap the two sets together: these widgets produce the counterpart of the suitable set by platform themselves.

```dart
// Platform code: choosing by platform
Widget buildAction(BuildContext context) {
  final platform = Theme.of(context).platform;
  return switch (platform) {
    TargetPlatform.iOS => CupertinoButton(onPressed: () {}, child: const Text('Save')),
    _ => ElevatedButton(onPressed: () {}, child: const Text('Save')),
  };
}
```

The choice is a condition, not a class hierarchy: if the platform is iOS, a widget comes back from the second set; otherwise, from the first. Its counterpart in the model is an ordinary function that takes the platform name and returns the family name.

```dart
// main.dart
String familyFor(String platform) => platform == 'ios' ? 'ios' : 'android';

void main() {
  for (final platform in ['android', 'ios', 'linux']) {
    print('$platform: ${familyFor(platform)}');
  }
}
```

```text
android: android
ios: ios
linux: android
```

For an unrecognized platform the default family is `android`; the function returns a family for every platform name. The selection code does not measure anything; it only says whose widget will be used.

How shared code and platform code are divided is asked in the Cross-Platform Approaches course: how is this feature's code shared across platforms. In this lesson the question is different. A family can report a different request under the same constraint; sizing, on the other hand, classifies whether the request is reflected in the result, that is, who gives the width. The difference of a family shows in the width only if nothing limits that difference.

#### A Slot Erases the Difference

First we put the action button inside a fixed-width **slot**. The slot is a cell 120 wide built with `fixed`, and the button is its child.

```dart
// main.dart
import 'sizing.dart';

Part buildBar(String family) {
  final requested = family == 'android' ? 88 : 64;
  return pad('Screen', 16, column('Bar', [
    fixed('Action', 120, leaf('Button', requested)),
  ]));
}

void main() {
  for (final family in ['android', 'ios']) {
    print('family: $family');
    printSizes(measure(buildBar(family), 360));
  }
}
```

```text
family: android
forced: /Screen width=360 constraint=360..360
forced: /Screen/Bar width=328 constraint=328..328
chosen: /Screen/Bar/Action width=120 constraint=0..328
forced: /Screen/Bar/Action/Button width=120 constraint=120..120
family: ios
forced: /Screen width=360 constraint=360..360
forced: /Screen/Bar width=328 constraint=328..328
chosen: /Screen/Bar/Action width=120 constraint=0..328
forced: /Screen/Bar/Action/Button width=120 constraint=120..120
```

The buttons of the two families come with different requests, 88 and 64. The slot, on the other hand, gives its child its own width as a tight constraint: `Button` gets 120..120 in both families and its width is forced. The difference in the request is not reflected in the width.

If we remove the slot, the situation changes.

```dart
// main.dart
import 'sizing.dart';

Part buildBar(String family) {
  final requested = family == 'android' ? 88 : 64;
  return pad('Screen', 16, column('Bar', [leaf('Button', requested)]));
}

void main() {
  for (final family in ['android', 'ios']) {
    print('family: $family');
    printSizes(measure(buildBar(family), 360));
  }
}
```

```text
family: android
forced: /Screen width=360 constraint=360..360
forced: /Screen/Bar width=328 constraint=328..328
chosen: /Screen/Bar/Button width=88 constraint=0..328
family: ios
forced: /Screen width=360 constraint=360..360
forced: /Screen/Bar width=328 constraint=328..328
chosen: /Screen/Bar/Button width=64 constraint=0..328
```

Without the slot, the column loosens the constraint, the button reports its own request, and the two families give two separate **chosen** widths: 88 and 64. The family difference is now visible in the width. The slot is the tight constraint that erases this difference.

**DC2.** The slot being 120 is a design decision; the model does not explain this number. The only thing measured is that a tight constraint erases the request from the width.

#### The Bounding Measurement

The focus node is the button inside the slot: `/Screen/Bar/Action/Button`. We measure the bar built with the slot at two screen widths and in two families.

```dart
// main.dart
import 'sizing.dart';

Part buildBar(String family) {
  final requested = family == 'android' ? 88 : 64;
  return pad('Screen', 16, column('Bar', [
    fixed('Action', 120, leaf('Button', requested)),
  ]));
}

void main() {
  for (final width in [480, 320]) {
    for (final family in ['android', 'ios']) {
      print('screen: $width, family: $family');
      final log = measure(buildBar(family), width);
      print('${log[3].sizing}: ${log[3].path} width=${log[3].width}');
    }
  }
}
```

```text
screen: 480, family: android
forced: /Screen/Bar/Action/Button width=120
screen: 480, family: ios
forced: /Screen/Bar/Action/Button width=120
screen: 320, family: android
forced: /Screen/Bar/Action/Button width=120
screen: 320, family: ios
forced: /Screen/Bar/Action/Button width=120
```

**This lesson's sizing: forced.** `/Screen/Bar/Action/Button` is forced to 120 in both families and at both screen widths. The button requesting 88 or 64 does not change the result; the slot gives the width. The choice of the family determines the width only when there is no slot.

#### Summary

- This lesson's sizing: forced. `/Screen/Bar/Action/Button` is forced to 120 in both families and at every screen width; the difference in the request is not reflected in the width.
- The framework offers separate widget sets that belong to the design languages of two platforms; adaptive widgets choose the suitable set by platform themselves.
- Choosing by platform is a condition: the platform is asked, and a widget comes back from the suitable set.
- Families can report different requests under the same constraint: without a slot, 88 and 64, two separate chosen widths.
- A fixed slot gives a tight constraint and erases the request difference of the families.

#### Next Step

This topic started from the Dart language and came as far as the place of widgets in the tree and their sets. The next topic takes up how the state of widgets is managed across the app. The **State Management Approaches** lesson compares the provider, event-based, and reactive models.

## Application Structure

State management approaches, navigation and routing, networking and serialization, local storage, assets and fonts, animations, platform channels.

### State Management Approaches

Source: https://academia.sh/en/courses/flutter/state-management-approaches

Shared state changes what a widget requests, not who determines its width. The lesson measures the width of a badge fed by a counter and shows that the badge stays chosen under the row's unbounded constraint.

The previous lesson measured that widget families that vary by platform ask for different widths on an action button, yet produce the same result in a fixed slot. This lesson moves the source of the request to the state of the app. A counter, a session record, or a cart total is a value that several widgets read together, and such a value can change a widget's request. The course question stays the same: **who determines this widget's width?** The lesson looks for the answer on a badge fed by a shared counter.

#### Three Ways to Share State

When the state of an app does not fit inside a single widget, the state is kept in a shared place near the top of the tree and the widgets below read it. This sharing comes in three forms. Library names are not written in this lesson; each one is referred to by its function.

In the **provider** approach, the value sits at a node high in the tree. The widgets below read the value from the context, and when the value changes, only the widgets that read it are rebuilt. Shared state libraries use this pattern.

In the **event-based** approach, a widget does not change the state directly; it sends an event. A handler receives the event and produces the new state. Event-based state libraries gather the path of change into a single point in this way.

In the **reactive** approach, the state is an observable value. Listeners subscribe to the value and are notified on every change; the value can be a stream or a notifier object.

All three approaches arrive at the same place: the state changes, the `build` method of the affected widget produces a new widget subtree from the new state, and the framework runs layout again.

#### 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.

```dart
// 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 the framework side of the provider pattern. The snippet is not measured; the model does not run the framework's code.

```dart
// Platform code: provider widget
class CounterScope extends InheritedNotifier<ValueNotifier<int>> {
  const CounterScope({super.key, required ValueNotifier<int> counter, required super.child})
      : super(notifier: counter);

  static int of(BuildContext context) {
    final scope = context.dependOnInheritedWidgetOfExactType<CounterScope>();
    return scope!.notifier!.value;
  }
}
```

In the snippet, the counter value sits in a notifier object at the top of the tree. A widget that calls `of` becomes dependent on that value; when the value changes, the framework rebuilds only these dependent widgets. Whichever approach is chosen, the input of the layout phase does not change: the constraint comes from above, and the widget's request is now computed from the state.

#### From the Counter to the Badge

The state of the model is a shared counter. A helper file holds the counter: it stores the value, calls the listeners, and prints nothing to the output. This is the smallest form of the reactive pattern.

```dart
// store.dart — shared counter store; notifies listeners on every change
class CounterStore {
  int count;
  final List<void Function(int)> _listeners = [];
  CounterStore(this.count);

  void listen(void Function(int) listener) => _listeners.add(listener);

  void update(int value) {
    count = value;
    for (final listener in _listeners) {
      listener(count);
    }
  }
}
```

The request of the badge depends on the number of digits in the counter: each digit adds 12 units, and the badge starts with a fixed gap of 18 units. `buildHeader` produces the tree from the store: a column inside the screen, and inside the column a row that carries the title and the badge. Widths are units of the model.

```dart
// main.dart
import 'sizing.dart';
import 'store.dart';

Part buildHeader(CounterStore store, {int title = 120}) {
  final digits = '${store.count}'.length;
  return pad('Screen', 16, column('Body', [
    row('Header', [
      leaf('Title', title),
      leaf('Badge', 18 + 12 * digits),
    ]),
  ]));
}

void main() {
  final store = CounterStore(9);
  final log = measure(buildHeader(store), 360);
  printSizes(log);
  countSizings(log);
}
```

```text
forced: /Screen width=360 constraint=360..360
forced: /Screen/Body width=328 constraint=328..328
stretched: /Screen/Body/Header width=328 constraint=0..328
chosen: /Screen/Body/Header/Title width=120 constraint=0..inf
chosen: /Screen/Body/Header/Badge width=30 constraint=0..inf
forced: 2
chosen: 2
clamped: 0
stretched: 1
parent: 3
```

When the counter is 9, the badge has one digit and asks for 30 units. The row receives a bounded constraint, so it does not report a request; but it gives its two non-flexible children an unbounded constraint. `Title` asks for 120 and `Badge` asks for 30, and since both stay inside the unbounded range, both are **chosen**. The title and the badge determine the width themselves; the parent is counted only in the screen, column, and row records.

#### When the Counter Changes

Now add a listener that subscribes to the counter. On every change, the listener rebuilds the tree, measures it again, and prints only the record of the badge. As the counter moves from 9 to 1000, 10 and 99 will give the same width, while 100 and 1000 move to different digit counts.

```dart
// main.dart
import 'sizing.dart';
import 'store.dart';

Part buildHeader(CounterStore store, {int title = 120}) {
  final digits = '${store.count}'.length;
  return pad('Screen', 16, column('Body', [
    row('Header', [
      leaf('Title', title),
      leaf('Badge', 18 + 12 * digits),
    ]),
  ]));
}

void main() {
  final store = CounterStore(9);
  store.listen((value) {
    final log = measure(buildHeader(store), 360);
    final badge = log.firstWhere((entry) => entry.path.endsWith('/Badge'));
    print('count=$value ${badge.sizing} width=${badge.width} constraint=${badge.limit.bounds()}');
  });
  for (final value in [9, 10, 99, 100, 1000]) {
    store.update(value);
  }
}
```

```text
count=9 chosen width=30 constraint=0..inf
count=10 chosen width=42 constraint=0..inf
count=99 chosen width=42 constraint=0..inf
count=100 chosen width=54 constraint=0..inf
count=1000 chosen width=66 constraint=0..inf
```

The badge is 30 units wide at 9, 42 up to 99, 54 at 100, and 66 at 1000. In every line the sizing stays the same: `chosen`. The width grew because the badge's request grew; but the badge still determines its width. Since the constraint is `0..inf`, no value can push the request outside the range. The rule does not see the state itself, only the part of the state that shows up in the request: 10 and 99 are different numbers, but because they give the same digit count, they produce the same record.

The result is the same with an event-based library and with a reactive library. The approaches change how the state is carried; what enters the layout phase is still the constraint and the request.

#### The Bounding Measurement

Because the row gives its non-flexible children an unbounded constraint, nothing stops the badge from growing. The total of the children can exceed the row's own range. To see this, narrow the screen to 200 units and ask for 160 units for the title. The counter is at 1000, and the badge is 66 units. The model does not show overflow: the language of the record is sizing, and each record looks only at its own constraint. The lesson computes the total of the children on a separate line and prints it.

```dart
// main.dart
import 'sizing.dart';
import 'store.dart';

Part buildHeader(CounterStore store, {int title = 120}) {
  final digits = '${store.count}'.length;
  return pad('Screen', 16, column('Body', [
    row('Header', [
      leaf('Title', title),
      leaf('Badge', 18 + 12 * digits),
    ]),
  ]));
}

void main() {
  final store = CounterStore(1000);
  final log = measure(buildHeader(store, title: 160), 200);
  printSizes(log);
  final header = log.firstWhere((entry) => entry.path == '/Screen/Body/Header');
  var total = 0;
  for (final entry in log.where((entry) => entry.path.startsWith('/Screen/Body/Header/'))) {
    total += entry.width;
  }
  print('children total: $total row: ${header.width}');
}
```

```text
forced: /Screen width=200 constraint=200..200
forced: /Screen/Body width=168 constraint=168..168
stretched: /Screen/Body/Header width=168 constraint=0..168
chosen: /Screen/Body/Header/Title width=160 constraint=0..inf
chosen: /Screen/Body/Header/Badge width=66 constraint=0..inf
children total: 226 row: 168
```

The focus node is `/Screen/Body/Header/Badge`. Its record is `chosen`, its width is 66, and its constraint is `0..inf`: the screen has narrowed, the title has grown, and the row has shrunk to 168 units, but the badge still chose its own request. The last line belongs to the sum, not to the measurement: the two children together ask for 226 units, while the row's range is 168 units. The overflow is here; in the real framework, because the children of a row are measured with an unbounded constraint, this total turns into an overflow warning. The rule does not see this: it records neither the warning of the overflow, nor its stripe on the screen, nor which child will be cut.

**AS1.** The model measures only the width. The height of the badge, the wrapping of the text onto a new line, and how many times a state change causes a rebuild are not in the model. The model builds this sizing's rule; the model is not the framework itself. Computing the width from the digit count is an assumption of the lesson; a real badge takes its width from the measurements of the font.

The way to prevent this kind of overflow is not in state management but in layout: the badge should be wrapped in a flexible child, or the title should be placed in an area that can be cut. State approaches change the request; whether the request fits is decided by the constraint.

#### Summary

- This lesson's sizing: chosen. The focus node is `/Screen/Body/Header/Badge`: the badge, which asks for 66 units under the row's unbounded constraint, chose its own request.
- The provider, event-based, and reactive approaches change how state is shared; the constraint and the request that enter the layout phase stay the same.
- As the counter went from 9 to 1000, the badge's width grew from 30 to 66, and the sizing stayed `chosen` at every step; numbers that give the same digit count gave the same record.
- The row gives its non-flexible children an unbounded constraint; the total of the children (226) can exceed the row's range (168), and the model does not show this.
- The rule does not see a value that does not enter the request: differences in the state that do not show up in the width do not change the record.

#### Next Step

In this lesson, a value that comes from the state changed the width, but the constraint was always unbounded or loose. The next lesson, `Navigation and Routing`, asks who gives a page its width: whichever route is opened, it measures what the navigator imposes on the page.

### Navigation and Routing

Source: https://academia.sh/en/courses/flutter/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.

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.

```dart
// 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.

```dart
// 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.

```dart
// 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.

```dart
// 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);
}
```

```text
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.

```dart
// 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);
}
```

```text
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.

```dart
// 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()}');
    }
  }
}
```

```text
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 the `360..360` constraint 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.

### Networking and Serialization

Source: https://academia.sh/en/courses/flutter/networking-and-serialization

A width request that comes from the server, the cache, or a default is clamped if it falls outside the range. The lesson decodes the request of a banner from JSON and measures that the origin does not change the sizing.

The previous lesson showed that the navigator gives a page a tight constraint at the screen width and that the page's request does not enter the result. In this lesson the request comes from the network. The server reports the width of a banner; the app reads it from a response, decodes it, and hands it to the tree. The width of the banner cannot be known before the response arrives, and the decoded value is not under the control of the app. The course question is the same again: **who determines this widget's width?** The lesson looks for the answer through a request that comes from the server.

#### Request Layer, JSON, and Model

An app that takes data from the network separates three jobs. The **request layer** calls the address and returns the response as a string. **JSON decoding** turns the string into a key-value structure. **Model conversion** transfers that structure to the app's own type, for example an object that carries the width of a banner. The request is asynchronous; in Dart this job is expressed with a `Future` and awaited with `await`. There is no network in this lesson: the response is a fixed string, and the `Future` only shows the order.

#### 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.

```dart
// 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 is the framework side of these three jobs. The snippet is not measured; the model does not run the framework's code.

```dart
// Platform code: HTTP client
abstract class ApiClient {
  Future<String> get(String path);
}

class BannerDto {
  final int width;
  const BannerDto(this.width);

  factory BannerDto.fromJson(Map<String, dynamic> json) =>
      BannerDto(json['width'] as int);
}

Future<BannerDto> loadBanner(ApiClient client) async {
  final body = await client.get('/banner');
  return BannerDto.fromJson(jsonDecode(body) as Map<String, dynamic>);
}
```

`ApiClient` fetches the response, `fromJson` turns the structure into the type, and `loadBanner` connects the two. The question of this lesson comes after that: once the decoded width enters the tree, what the rule does to that value.

#### From Response to Banner

The responses live in a helper file. `fetchBanner` takes the name of a source and returns the response of that source, or `null` if the source is offline. There are four sources: `network` is the current response from the server, `cache` is a copy stored earlier, `default` is the value embedded in the app, and `narrow` is a small banner. The helper file prints nothing to the output.

```dart
// api.dart — fixed responses; every source returns a Future, there is no network
Future<String?> fetchBanner(String source) async {
  switch (source) {
    case 'network':
      return '{"banner": {"width": 1200}}';
    case 'cache':
      return '{"banner": {"width": 1000}}';
    case 'default':
      return '{"banner": {"width": 400}}';
    case 'narrow':
      return '{"banner": {"width": 300}}';
    default:
      return null;
  }
}
```

`decodeBanner` decodes the response with `jsonDecode` and hands the width of the banner to the tree. The tree is a column inside the screen, and inside the column a title and the banner. Decoding comes from the `dart:convert` library and needs no network or file access.

```dart
// main.dart
import 'dart:convert';
import 'sizing.dart';
import 'api.dart';

Part decodeBanner(String json) {
  final data = jsonDecode(json) as Map<String, dynamic>;
  final banner = data['banner'] as Map<String, dynamic>;
  final width = banner['width'] as int;
  return pad('Screen', 16, column('Body', [
    leaf('Title', 120),
    leaf('Banner', width),
  ]));
}

Future<void> main() async {
  final json = await fetchBanner('network');
  print('response: $json');
  final log = measure(decodeBanner(json!), 360);
  printSizes(log);
  countSizings(log);
}
```

```text
response: {"banner": {"width": 1200}}
forced: /Screen width=360 constraint=360..360
forced: /Screen/Body width=328 constraint=328..328
chosen: /Screen/Body/Title width=120 constraint=0..328
clamped: /Screen/Body/Banner width=328 constraint=0..328
forced: 2
chosen: 1
clamped: 1
stretched: 0
parent: 3
```

The server has reported a banner 1200 units wide. The column gives its children a horizontally bounded range; the constraint is `0..328`. `/Screen/Body/Banner` brought a request outside this range, and the rule pulls the request to the upper bound: the record is **clamped**, and the width is 328. The title asks for 120 and stays inside the range, so it is **chosen**. The count shows this: two nodes are forced, one is chosen, one is clamped; the parent is three.

The server asked for the width of the banner, but the server did not give the width. The request came from the network, the tree carried it as it was, and the constraint cut it down.

#### Changing the Origin

Now read the same banner from four sources and print only the record of the banner each time. The first three sources bring a request outside the range, and the fourth brings a request that stays inside.

```dart
// main.dart
import 'dart:convert';
import 'sizing.dart';
import 'api.dart';

Part decodeBanner(String json) {
  final data = jsonDecode(json) as Map<String, dynamic>;
  final banner = data['banner'] as Map<String, dynamic>;
  final width = banner['width'] as int;
  return pad('Screen', 16, column('Body', [
    leaf('Title', 120),
    leaf('Banner', width),
  ]));
}

Future<void> main() async {
  for (final source in ['network', 'cache', 'default', 'narrow']) {
    final json = await fetchBanner(source);
    final log = measure(decodeBanner(json!), 360);
    final banner = log.firstWhere((entry) => entry.path == '/Screen/Body/Banner');
    print('source=$source ${banner.sizing} width=${banner.width} constraint=${banner.limit.bounds()}');
  }
}
```

```text
source=network clamped width=328 constraint=0..328
source=cache clamped width=328 constraint=0..328
source=default clamped width=328 constraint=0..328
source=narrow chosen width=300 constraint=0..328
```

In all three sources that ask for 1200, 1000, and 400, the record is `clamped` and the width is 328. In the `narrow` source, which asks for 300, the record is `chosen` and the width is 300. The name of the source does not determine the sizing; the relation of the request to the range does. The question **where does this read get its response from** chooses an origin among memory, disk, network, or default; this lesson does not ask it. Whatever the origin, a request outside the range is clamped.

#### The Bounding Measurement

Whether the origin is only the network, or the cache, or the default, the rule stays the same. Take an edge case: the network is offline, `fetchBanner` returns `null`, and the app falls back first to the cache, then to the default. Measure the response that is used at two screen widths. The range is `0..328` at 360 and `0..448` at 480.

```dart
// main.dart
import 'dart:convert';
import 'sizing.dart';
import 'api.dart';

Part decodeBanner(String json) {
  final data = jsonDecode(json) as Map<String, dynamic>;
  final banner = data['banner'] as Map<String, dynamic>;
  final width = banner['width'] as int;
  return pad('Screen', 16, column('Body', [
    leaf('Title', 120),
    leaf('Banner', width),
  ]));
}

Future<void> main() async {
  final order = ['offline', 'cache', 'default'];
  var used = '';
  String? json;
  for (final source in order) {
    json = await fetchBanner(source);
    if (json != null) {
      used = source;
      break;
    }
  }
  for (final screen in [360, 480]) {
    final log = measure(decodeBanner(json!), screen);
    final banner = log.firstWhere((entry) => entry.path == '/Screen/Body/Banner');
    print('screen=$screen source=$used ${banner.sizing} width=${banner.width} constraint=${banner.limit.bounds()}');
  }
}
```

```text
screen=360 source=cache clamped width=328 constraint=0..328
screen=480 source=cache clamped width=448 constraint=0..448
```

The focus node is `/Screen/Body/Banner`. Because the network is offline, the response came from the cache, and the request is 1000. On the screen of 360 the record is `clamped` and the width is 328; on the screen of 480 the range widened, so the width is 448, but the record is `clamped` again. The value of the request stayed 1000, while the clamped width changed with the screen: the upper bound of the constraint determines the width of a clamped node. The banner coming down to 328 or to 448 is not the decision of the server, the cache, or the default.

**AS1.** The model measures only the width. The delay of the response, the handling of a faulty response, the height of the banner, and the image itself are not in the model. The model builds this sizing's rule; the model is not the framework itself. That the response always carries a valid `banner` key is an assumption of the lesson; a real client handles a missing or corrupt response separately.

The practical consequence is this: trusting a width that comes from the server does not mean assuming that the width fits the screen. The decision to fit is on the side of the constraint; the request layer and the model conversion carry the request as it is. However large the value the server sends, while the node is clamped the other records in the tree do not change.

#### Summary

- This lesson's sizing: clamped. The focus node is `/Screen/Body/Banner`: the 1200 reported by the server falls outside the `0..328` range the column gives and is pulled to 328.
- The request layer fetches the response, JSON decoding extracts the structure, and model conversion transfers it to the type; all three carry the request, none gives the width.
- The requests of 1200, 1000, and 400 from the network, the cache, and the default all came out clamped; the source that asked for 300 stayed inside the range and was chosen.
- The 1000 that came from the cache while the network was offline was clamped to 328 on the screen of 360 and to 448 on the screen of 480; the constraint determines the clamped width.
- The origin does not enter the record: the rule sees only the request and the range.

#### Next Step

In this lesson the request came from outside and the rule pulled it into the range. The next lesson, `Local Storage`, measures the case where the request comes from a preference stored on the device: it looks at what happens to the sizing when a stored preference makes a widget flexible and when the preference goes stale.

### Local Storage

Source: https://academia.sh/en/courses/flutter/local-storage

A saved preference can make a card flexible, and a stale preference keeps producing the same record. The lesson reads the preference from a key-value store and measures who owns the width of the card.

The previous lesson showed that a width request that comes from the server is clamped when it falls outside the range, and that the origin does not change the result. In this lesson the request comes from the phone: a preference the user chose earlier and had stored. Everything an app must remember when it is closed and opened again lives in local storage. The course question is the same again: **who determines this widget's width?** The lesson looks for the answer around a preference read from the store.

#### Preference, Key-Value, and Database

Local storage comes in three forms. A **preference** is a small setting: the theme, the language, or the dense and wide view of a screen. A **key-value store** is the simplest arrangement that keeps preferences under a name; writing and reading are bound to a name. An **embedded database** stores larger and related data that needs tables and queries. The first two are the model of this lesson; the third is only mentioned.

Access to the store is asynchronous, because the data is on a persistent medium, not in memory. In this lesson the store is an in-memory `Map`, and access is expressed with a `Future`; there is no real file or database.

#### 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.

```dart
// 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 the framework side of reading and writing a preference. The snippet is not measured; the model does not run the framework's code.

```dart
// Platform code: preference store
Future<String> loadMode(SharedPreferencesAsync prefs) async {
  final saved = await prefs.getString('mode');
  return saved ?? 'compact';
}

Future<void> saveMode(SharedPreferencesAsync prefs, String mode) async {
  await prefs.setString('mode', mode);
}
```

`loadMode` reads the saved preference and returns `compact` if there is no record; `saveMode` writes the new preference. The result of the read changes how a tree is built. The question of this lesson is exactly in that construction: how the card's request comes out.

#### From Preference to Card

The preference store lives in a helper file. An in-memory `Map` holds the key-value mapping; reading and writing each return a `Future`. The helper file prints nothing to the output.

```dart
// prefs.dart — in-memory preference store; reading and writing return a Future
class PrefStore {
  final Map<String, String> _data;
  PrefStore([Map<String, String>? initial]) : _data = {...?initial};

  Future<String?> read(String key) async => _data[key];

  Future<void> write(String key, String value) async {
    _data[key] = value;
  }
}
```

`buildPrefs` builds the card according to the preference. In the `full` preference the card has no width it asks for, and the card spreads into the space it is given: `fill('Card')`. In the `compact` preference the card asks for 200 units. The tree is a column inside the screen, and inside the column a title and the card.

```dart
// main.dart
import 'sizing.dart';
import 'prefs.dart';

Part buildPrefs(String mode) {
  final card = mode == 'full' ? fill('Card') : leaf('Card', 200);
  return pad('Screen', 16, column('Body', [
    leaf('Title', 120),
    card,
  ]));
}

Future<void> main() async {
  final store = PrefStore({'mode': 'full'});
  final mode = await store.read('mode') ?? 'compact';
  print('preference=$mode');
  final log = measure(buildPrefs(mode), 360);
  printSizes(log);
  countSizings(log);
}
```

```text
preference=full
forced: /Screen width=360 constraint=360..360
forced: /Screen/Body width=328 constraint=328..328
chosen: /Screen/Body/Title width=120 constraint=0..328
stretched: /Screen/Body/Card width=328 constraint=0..328
forced: 2
chosen: 1
clamped: 0
stretched: 1
parent: 3
```

The preference `full` was read, and the card has no request. The column gives its children the range `0..328`; because the upper bound is finite, the card spreads over this range. The `/Screen/Body/Card` record is **stretched**: the width is 328, but the card did not ask for this width; it is the upper bound of the range. The count shows one stretched, one chosen, and two forced; the parent is three.

#### When the Preference Changes

Now read the store in three situations: when the preference has never been written, when `compact` has been written, and when `full` has been written. In each situation, rebuild the tree and print only the record of the card.

```dart
// main.dart
import 'sizing.dart';
import 'prefs.dart';

Part buildPrefs(String mode) {
  final card = mode == 'full' ? fill('Card') : leaf('Card', 200);
  return pad('Screen', 16, column('Body', [
    leaf('Title', 120),
    card,
  ]));
}

Future<void> main() async {
  final store = PrefStore();
  for (final saved in [null, 'compact', 'full']) {
    if (saved != null) {
      await store.write('mode', saved);
    }
    final mode = await store.read('mode') ?? 'compact';
    final log = measure(buildPrefs(mode), 360);
    final card = log.firstWhere((entry) => entry.path == '/Screen/Body/Card');
    print('saved=${saved ?? 'none'} preference=$mode ${card.sizing} width=${card.width} constraint=${card.limit.bounds()}');
  }
}
```

```text
saved=none preference=compact chosen width=200 constraint=0..328
saved=compact preference=compact chosen width=200 constraint=0..328
saved=full preference=full stretched width=328 constraint=0..328
```

When there is no record, the preference falls back to `compact` and the card asks for 200; because this request stays inside the range `0..328`, the record is **chosen**. When `compact` is written, the result does not change. When `full` is written, the card turns stretched and fills 328. The sizing is determined not by the name of the preference itself, but by whether the preference makes the card's request exist or not. When the request is removed, the owner of the width stops being the card and moves to the constraint.

#### The Bounding Measurement

A value in the store can differ from what the screen read: the user changed the preference somewhere else, and the screen has not been rebuilt yet. The measurement below builds the screen with the `full` preference, then writes `compact` to the store. The tree the screen holds does not change; the tree changes only when the preference is read again and rebuilt.

```dart
// main.dart
import 'sizing.dart';
import 'prefs.dart';

Part buildPrefs(String mode) {
  final card = mode == 'full' ? fill('Card') : leaf('Card', 200);
  return pad('Screen', 16, column('Body', [
    leaf('Title', 120),
    card,
  ]));
}

void show(String label, String shown, String stored, Part tree) {
  final log = measure(tree, 360);
  final card = log.firstWhere((entry) => entry.path == '/Screen/Body/Card');
  print('$label shown=$shown stored=$stored ${card.sizing} width=${card.width}');
}

Future<void> main() async {
  final store = PrefStore({'mode': 'full'});
  final shown = (await store.read('mode'))!;
  final onScreen = buildPrefs(shown);
  show('read', shown, shown, onScreen);

  await store.write('mode', 'compact');
  final stored = (await store.read('mode'))!;
  show('stale', shown, stored, onScreen);

  final fresh = buildPrefs(stored);
  show('refreshed', stored, stored, fresh);
}
```

```text
read shown=full stored=full stretched width=328
stale shown=full stored=compact stretched width=328
refreshed shown=compact stored=compact chosen width=200
```

The focus node is `/Screen/Body/Card`. In the first line the card is stretched and 328. In the second line the store has become `compact`, but the tree the screen holds is the same, and the record comes out exactly the same: `stretched`, 328. A stale preference also yields a stretched width. The rule does not see the store; it sees only the tree it measures. In the third line the tree has been rebuilt, and the card becomes `chosen` with a width of 200.

The question **does this screen match the model** asks whether the screen matches the current state of the data. This lesson does not ask it: a stale tree also produces a record that is consistent within itself, and the rule classifies that record.

**AS1.** The model measures only the width. Whether a write is persistent, the order of two simultaneous writes, the size of the data in the store, and the content of the card are not in the model. The model builds this sizing's rule; the model is not the framework itself. That the preference carries one of two values, `full` or `compact`, is an assumption of the lesson; a real store can also carry other values and version differences.

A practical consequence: the on-screen counterpart of a preference does not have to be identical to the record in the store. To see the owner of the width, one must look not at the store but at the tree that is on the screen at that moment.

#### Summary

- This lesson's sizing: stretched. The focus node is `/Screen/Body/Card`: in the `full` preference the card has no request and takes the upper bound of the `0..328` range the column gives, 328.
- A preference is a small setting in a key-value store; an embedded database stores larger, related data. Access is asynchronous.
- With no record and with `compact` written, the card asked for 200 and came out chosen; when `full` was written the request was removed and the card became stretched.
- With the screen built from `full` and the store later changed to `compact`, the measurement gave the same record: `stretched`, 328. In the rebuilt tree the card became chosen with 200.
- The rule classifies the tree it measures, not the store; a stale preference does not write into the record that it is stale.

#### Next Step

In this lesson, what removed the request was a saved preference. The next lesson, `Assets and Fonts`, measures an input that enlarges the request: when the font scale increases the request of a button, it looks at whether the button still fits inside the range.

### Assets and Fonts

Source: https://academia.sh/en/courses/flutter/assets-and-fonts

The font scale enlarges the request of a button, and when the growing request exceeds the range the button is clamped. The lesson gives the scale as 1, 2, and 3 and measures how widening the range changes the result.

The previous lesson showed that a saved preference removed the request of a card and made the card stretched. This lesson measures an input that does the opposite: an input that enlarges the request. When the user enlarges the text size in the system settings, every text in the app and every button that carries text grows. The course question is the same again: **who determines this widget's width?** The lesson looks for the answer through a button that grows with the scale.

#### Assets, Resolution Variants, and Fonts

In this lesson an **asset** is a file packaged together with the app: an image, a font file, or a sound. It has nothing to do with an entity in a data model. The app configuration declares which files are assets; a file that is not declared does not enter the package.

Images come with resolution variants according to the density of the screen: enlarged files of the same image, such as 1.0x and 2.0x, stand side by side, and the framework chooses the one that fits the screen. Fonts are also assets; they are declared under a family name.

#### 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.

```dart
// 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 the configuration. The package name is fictional, and the snippet is not measured.

```yaml
# Platform code: asset declaration
name: demo_shop
flutter:
  assets:
    - assets/images/logo.png
  fonts:
    - family: Body
      fonts:
        - asset: assets/fonts/body-regular.ttf
```

`assets` declares the image files, and `fonts` declares the font family and its file. The resolution variants stand side by side in a subdirectory such as `assets/images/2.0x/logo.png` and are not listed separately. After the declaration, the code reads the scale of the font from the user's setting.

```dart
// Platform code: font scale
double labelSize(BuildContext context) {
  final scaler = MediaQuery.textScalerOf(context);
  return scaler.scale(14);
}
```

`textScalerOf` gives the scale the user has set, and `scale` multiplies the base size by that scale. When the text grows, the request of the button that carries the text grows too. The question of this lesson is the place where that growth meets the range.

#### From Scale to Request

The button of the model carries the width of a label and adds the frame allowance around it. The width of the label is an integer multiplied by the scale: if the base width is 160 units, the label is 160 units wide at scale 1 and 320 units wide at scale 2. The frame allowance is a fixed 24 units. The function that computes the request and the base widths live in a helper file; the helper file prints nothing to the output.

```dart
// fonts.dart — label width and the request that depends on the scale
const labelBase = {'Save': 160, 'OK': 80};

int frameWidth(int base, int scale) => base * scale + 24;
```

`buildForm` takes the scale and builds the button with this request. The tree is a column inside the screen, and inside the column a title and the button. The first measurement is at scale 1.

```dart
// main.dart
import 'sizing.dart';
import 'fonts.dart';

Part buildForm(String label, int scale) {
  return pad('Screen', 16, column('Body', [
    leaf('Title', 120),
    leaf('Button', frameWidth(labelBase[label]!, scale)),
  ]));
}

void main() {
  final log = measure(buildForm('Save', 1), 360);
  printSizes(log);
  countSizings(log);
}
```

```text
forced: /Screen width=360 constraint=360..360
forced: /Screen/Body width=328 constraint=328..328
chosen: /Screen/Body/Title width=120 constraint=0..328
chosen: /Screen/Body/Button width=184 constraint=0..328
forced: 2
chosen: 2
clamped: 0
stretched: 0
parent: 2
```

At scale 1 the button asks for 184; because the range is `0..328`, the request stays inside the range, and `/Screen/Body/Button` is **chosen**. The title is also chosen. The count shows two forced and two chosen; there is no clamped or stretched, and the parent is two.

#### As the Scale Grows

Now raise the scale from 1 to 3 and compare two labels. `Save` is a long label; `OK` is short.

```dart
// main.dart
import 'sizing.dart';
import 'fonts.dart';

Part buildForm(String label, int scale) {
  return pad('Screen', 16, column('Body', [
    leaf('Title', 120),
    leaf('Button', frameWidth(labelBase[label]!, scale)),
  ]));
}

void main() {
  for (final label in ['Save', 'OK']) {
    for (final scale in [1, 2, 3]) {
      final log = measure(buildForm(label, scale), 360);
      final button = log.firstWhere((entry) => entry.path == '/Screen/Body/Button');
      print('$label scale=$scale request=${frameWidth(labelBase[label]!, scale)} ${button.sizing} width=${button.width}');
    }
  }
}
```

```text
Save scale=1 request=184 chosen width=184
Save scale=2 request=344 clamped width=328
Save scale=3 request=504 clamped width=328
OK scale=1 request=104 chosen width=104
OK scale=2 request=184 chosen width=184
OK scale=3 request=264 chosen width=264
```

`Save` asks for 184 at scale 1 and stays chosen. At scale 2 the request rises to 344, exceeds 328, and the record becomes `clamped`; the width is 328. At scale 3 the request is 504, and the record is again clamped, and the width is again 328. `OK` asks for 104, 184, and 264 at the three scales, and stays chosen in all three. The scale alone does not determine the sizing: the scale enlarges the request, and the relation of the request to the range determines the sizing.

The question **if this operation is repeated or its order changes, does the result change** looks for the sensitivity of an operation. This lesson does not ask it. The font scale is an input, and it enlarges the request; the sizing classifies not the resistance of the result to repetition, but who fits the growing request into the range.

#### The Bounding Measurement

At scale 2, `Save` was clamped. If the range widens, can the same request fit? Measure the same tree at two screen widths: at 360 the range is `0..328`, and at 480 it is `0..448`. The request stays fixed at 344.

```dart
// main.dart
import 'sizing.dart';
import 'fonts.dart';

Part buildForm(String label, int scale) {
  return pad('Screen', 16, column('Body', [
    leaf('Title', 120),
    leaf('Button', frameWidth(labelBase[label]!, scale)),
  ]));
}

void main() {
  for (final screen in [360, 480]) {
    final log = measure(buildForm('Save', 2), screen);
    final button = log.firstWhere((entry) => entry.path == '/Screen/Body/Button');
    print('screen=$screen scale=2 request=344 ${button.sizing} width=${button.width} constraint=${button.limit.bounds()}');
  }
}
```

```text
screen=360 scale=2 request=344 clamped width=328 constraint=0..328
screen=480 scale=2 request=344 chosen width=344 constraint=0..448
```

The focus node is `/Screen/Body/Button` at scale 2. On the screen of 360, the request is 344 and the range is `0..328`, so the record is `clamped` and the width is 328. On the screen of 480, the range becomes `0..448`, the request stays inside, and the record is `chosen` with a width of 344. When the screen or the scale changes, the result can change; what changes the result is not the font scale itself, but whether the growing request fits inside the range.

**AS1.** The model measures only the width. The wrapping of the label onto a new line, how overflowing text looks, the height of the button, and the real letter measurements of the font are not in the model. The model builds this sizing's rule; the model is not the framework itself. That the request grows linearly with the scale, and the fixed frame allowance of 24 units, are assumptions of the lesson; the width of a real label comes from the measurements of the font.

The consequence is this: if a button is clamped for a user with a large text setting, the fix is not to suppress the scale. The options go in two directions: reduce the request by shortening the label, or widen the range by moving the button to a place where the range is wide.

#### Summary

- This lesson's sizing: clamped. The focus node is `/Screen/Body/Button` at scale 2: the request of 344 exceeds the `0..328` range and is pulled to 328.
- The asset declaration says which files will be packaged; the font scale is read from the user's setting and enlarges the width of the label.
- `Save` came out chosen with 184 at scale 1 and clamped with 344 and 504 at scales 2 and 3; `OK` stayed chosen at all three scales.
- When the screen went up to 480, the range became `0..448` and the request of 344 came out chosen again; the width was determined not by the scale but by the relation of the request to the range.
- The rule sees not the scale itself, but whether the growing request fits.

#### Next Step

In this lesson the scale enlarged the request, and the result changed between two measurements. The next lesson, `Animations`, measures the case where the request grows over time in small steps: the tree is measured again on every frame, and on every frame it looks at the owner of the width.

### Animations

Source: https://academia.sh/en/courses/flutter/animations

An animation runs the layout again on every frame, but the owner of the width may not change on every frame. The lesson moves the width of a panel from 80 to 240 in five frames and measures the sizing of each frame.

The previous lesson measured that the font scale enlarges the request of a button, and whether the growing request fits inside the range. In this lesson the request changes over time: the width of a panel grows a little more on every frame. An animation stops the layout from being a one-time computation and runs it again for every frame. The course question is the same again: **who determines this widget's width?** The lesson looks for the answer through a panel that grows across frames.

#### Controller, Curve, and Tween

An animation has three parts. The **animation controller** carries the progress from 0 to 1; who starts it and stops it, and when, is up to the controller. The **curve** shapes the progress: a linear curve moves in equal steps, and an accelerating curve moves slowly first and quickly later. The **tween** turns the progress into a value between a start and an end. In this lesson the tween is the width from 80 to 240.

In an **implicit** animation the widget takes the new value and builds the transition itself; the framework manages the controller. In an **explicit** animation the controller is held in code and the progress is driven by hand.

#### 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.

```dart
// 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 the framework side of an explicit animation. The snippet is not measured; the model does not run the code of the framework.

```dart
// Platform code: explicit animation
class _PanelState extends State<PanelView> with SingleTickerProviderStateMixin {
  late final AnimationController _controller = AnimationController(vsync: this);
  late final Animation<double> _width = Tween<double>(begin: 80, end: 240)
      .animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));

  void open() => _controller.animateTo(1.0, duration: widget.span);
}
```

The controller drives the progress, `CurvedAnimation` applies the curve, and `Tween` produces the width. Every frame means a new width, and a new width means the layout runs again. The question of this lesson is the owner of the width as a result of that run.

#### From Frame to Tree

The model has no time; it has only the order of frames. Five frames are numbered from 0 to 4, and the tween produces an integer on every frame. The helper file carries two curves and a frame counter. The linear curve takes equal steps; the accelerating curve scales the step with its square. The counter is lesson-local and counts the frames. The helper file prints nothing to the output.

```dart
// tween.dart — tweens and the frame counter
int linearWidth(int from, int to, int step, int steps) =>
    from + (to - from) * step ~/ steps;

int easedWidth(int from, int to, int step, int steps) =>
    from + (to - from) * step * step ~/ (steps * steps);

class FrameCounter {
  int frames = 0;
  void tick() => frames++;
}
```

`buildPanel` takes the width of the panel and builds the tree: a column inside the screen, and inside the column a title and the panel. The first measurement is the start frame of the animation.

```dart
// main.dart
import 'sizing.dart';
import 'tween.dart';

Part buildPanel(int width) {
  return pad('Screen', 16, column('Body', [
    leaf('Title', 120),
    leaf('Panel', width),
  ]));
}

void main() {
  final log = measure(buildPanel(linearWidth(80, 240, 0, 4)), 360);
  printSizes(log);
  countSizings(log);
}
```

```text
forced: /Screen width=360 constraint=360..360
forced: /Screen/Body width=328 constraint=328..328
chosen: /Screen/Body/Title width=120 constraint=0..328
chosen: /Screen/Body/Panel width=80 constraint=0..328
forced: 2
chosen: 2
clamped: 0
stretched: 0
parent: 2
```

In the start frame the panel asks for 80, and the range is `0..328`. `/Screen/Body/Panel` is **chosen** and its width is 80. The title is also chosen. The animation has not changed anything yet; the tree at frame zero would be the same without the animation.

#### Five Frames

Now measure the five frames in order with two curves. Each frame is a separate `measure` call, and from each frame only the record of the panel is printed.

```dart
// main.dart
import 'sizing.dart';
import 'tween.dart';

Part buildPanel(int width) {
  return pad('Screen', 16, column('Body', [
    leaf('Title', 120),
    leaf('Panel', width),
  ]));
}

void main() {
  final curves = {'linear': linearWidth, 'eased': easedWidth};
  curves.forEach((name, curve) {
    for (var step = 0; step <= 4; step++) {
      final width = curve(80, 240, step, 4);
      final log = measure(buildPanel(width), 360);
      final panel = log.firstWhere((entry) => entry.path == '/Screen/Body/Panel');
      print('$name frame=$step request=$width ${panel.sizing} width=${panel.width}');
    }
  });
}
```

```text
linear frame=0 request=80 chosen width=80
linear frame=1 request=120 chosen width=120
linear frame=2 request=160 chosen width=160
linear frame=3 request=200 chosen width=200
linear frame=4 request=240 chosen width=240
eased frame=0 request=80 chosen width=80
eased frame=1 request=90 chosen width=90
eased frame=2 request=120 chosen width=120
eased frame=3 request=170 chosen width=170
eased frame=4 request=240 chosen width=240
```

The linear curve produces 80, 120, 160, 200, and 240; the accelerating curve produces 80, 90, 120, 170, and 240. The two curves give different widths in between, and both end at 240. But in ten of the ten lines the sizing is `chosen`, and the width is equal to the request. The curve shapes the value of the request; it does not change the relation of the request to the range.

The question **how many times did it run** counts how many times a job ran. An animation runs the layout again on every frame, and the number of runs really does grow; this lesson does not take it as a measure. What is measured on every frame is the owner of the width in that frame.

#### The Bounding Measurement

Count the frames with the counter and fold the sizing of each frame into a tally. If all five frames give the same sizing, the tally comes down to a single line. Also print the line of the panel in the last frame as the focus.

```dart
// main.dart
import 'sizing.dart';
import 'tween.dart';

Part buildPanel(int width) {
  return pad('Screen', 16, column('Body', [
    leaf('Title', 120),
    leaf('Panel', width),
  ]));
}

void main() {
  final counter = FrameCounter();
  final tally = <String, int>{};
  String last = '';
  for (var step = 0; step <= 4; step++) {
    counter.tick();
    final log = measure(buildPanel(linearWidth(80, 240, step, 4)), 360);
    final panel = log.firstWhere((entry) => entry.path == '/Screen/Body/Panel');
    tally[panel.sizing] = (tally[panel.sizing] ?? 0) + 1;
    last = '${panel.path} ${panel.sizing} width=${panel.width} constraint=${panel.limit.bounds()}';
  }
  print('frames=${counter.frames}');
  tally.forEach((sizing, count) => print('$sizing=$count'));
  print('last frame: $last');
}
```

```text
frames=5
chosen=5
last frame: /Screen/Body/Panel chosen width=240 constraint=0..328
```

The counter prints 5, and the tally is a single line: five of the five frames are `chosen`. The focus node is `/Screen/Body/Panel` in the last frame. Its width is 240, its constraint is `0..328`, and the panel has chosen its own request. While the panel grew from 80 to 240, the tree was measured again five times; but on every frame the panel determined the width. A growing width does not change the owner of the width.

**AS1.** The model measures only the width. The duration of a frame, the progress of the curve in real time, starting and cancelling the animation, and the height of the panel are not in the model. The model builds this sizing's rule; the model is not the framework itself. The integer progress of five frames is the assumption of the lesson; a real animation produces fractional values.

The result has a limit, and it is not measured in this lesson: by the definition of the rule, after a frame whose request exceeds 328 the panel becomes clamped. Whether the end value fits on the screen is not the job of the tween, but of the side that knows the range.

#### Summary

- This lesson's sizing: chosen. The focus node is `/Screen/Body/Panel` in the last frame: the request of 240 stays inside the `0..328` range, and the panel chooses its own request.
- The controller drives the progress, the curve shapes the progress, and the tween produces the width; in an implicit animation the framework manages the controller.
- The linear and accelerating curves gave different widths in the in-between frames (160 and 120 at frame 2); in ten of ten measurements the sizing came out chosen.
- The five frames were measured separately, the counter printed 5, and all five were chosen.
- The rule sees not the number of runs, but the owner of the width in that frame.

#### Next Step

In this lesson the request changed on every frame and the owner did not change. The next lesson, `Platform Channels`, measures the case where the request comes from outside the framework, from the side of the platform itself: it looks at how the width that an embedded platform view asks for is pulled to the width the slot gives, however many messages arrive.

### Platform Channels

Source: https://academia.sh/en/courses/flutter/platform-channels

However many messages arrive from the platform, the framework forces the width of the embedded view. The lesson measures this with three different requests and with repeated messages.

The previous lesson measured the animation frames of the framework and the panel chosen in each frame. This lesson reaches outside the tree, to the platform itself: the app sometimes wants to embed a screen part of the platform or to receive a piece of information from the platform. This conversation passes through a channel. A message that arrives from the channel can also propose the width of an embedded view. The course question is the same again: **who determines this widget's width?** The lesson looks for the answer in the slot that wraps the view.

#### Channel, Message, and Embedded View

A **platform channel** is a named line of conversation between the Dart side of the app and the platform code. The Dart side sends a call with a method name and arguments, the platform side receives the call, and a response returns. The platform can also send a message on its own. The two ends of the channel are in two separate languages; data travels in both directions in simple types: numbers, strings, lists, and maps.

An **embedded platform view** is a screen part of the platform placed into the Dart tree. In the tree it is a single node, but the platform draws its content. The width of this node also obeys the rules of the tree: the framework puts it into a slot, and the slot gives it a constraint.

#### 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.

```dart
// 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 two snippets below show the two ends of the channel. The snippets are not measured; the model does not run the code of the framework or of the platform.

```dart
// Platform code: Dart end
const channel = MethodChannel('app/embed');

Future<int?> askWidth() {
  return channel.invokeMethod<int>('preferredWidth');
}
```

```swift
// Platform code: platform end
let channel = FlutterMethodChannel(
  name: "app/embed",
  binaryMessenger: messenger
)
channel.setMethodCallHandler { call, result in
  if call.method == "preferredWidth" {
    result(180)
  } else {
    result(FlutterMethodNotImplemented)
  }
}
```

The Dart end calls the method named `preferredWidth`, and the platform end returns 180. This 180 is a **request**: it says how many units of width the platform view wants. The question of the lesson is what this request does to the width of the view.

#### From Channel to Slot

In the model the channel is a function that brings the messages as a list. Every message is a map: an `id` and a `width` that the platform wants. The helper file carries two groups of messages, `first` and `repeat`, and provides the function that reads the width from a message. The file prints nothing to the output.

```dart
// channel.dart — messages from the channel and reading the request
const _batches = <String, List<Map<String, Object>>>{
  'first': [
    {'id': 1, 'width': 180},
    {'id': 2, 'width': 400},
    {'id': 3, 'width': 96},
  ],
  'repeat': [
    {'id': 1, 'width': 180},
    {'id': 1, 'width': 180},
    {'id': 2, 'width': 400},
    {'id': 3, 'width': 96},
    {'id': 3, 'width': 96},
  ],
};

Future<List<Map<String, Object>>> receiveMessages(String batch) async {
  return _batches[batch]!;
}

int widthOf(Map<String, Object> message) => message['width'] as int;
```

The tree of the screen is a padding, a column, a title, and the slot of the embedded view. The slot has a fixed width of 240: `fixed('Embed', 240, ...)`. The view is the only child of the slot and asks for the width that comes from the message. The first measurement uses the first message of the `first` group.

```dart
// main.dart
import 'sizing.dart';
import 'channel.dart';

Part buildScreen(int requested) {
  return pad('Screen', 16, column('Body', [
    leaf('Title', 120),
    fixed('Embed', 240, leaf('PlatformView', requested)),
  ]));
}

Future<void> main() async {
  final messages = await receiveMessages('first');
  final log = measure(buildScreen(widthOf(messages.first)), 360);
  printSizes(log);
  countSizings(log);
}
```

```text
forced: /Screen width=360 constraint=360..360
forced: /Screen/Body width=328 constraint=328..328
chosen: /Screen/Body/Title width=120 constraint=0..328
chosen: /Screen/Body/Embed width=240 constraint=0..328
forced: /Screen/Body/Embed/PlatformView width=240 constraint=240..240
forced: 3
chosen: 2
clamped: 0
stretched: 0
parent: 3
```

The first message asks for 180, but the record `/Screen/Body/Embed/PlatformView` is **forced**: its width is 240 and its constraint is `240..240`. The slot gives the view a tight constraint; the 180 in the message hits the tight constraint of the slot, and the width stays at 240. The slot itself is a node that asks for 240 inside a column of width 328 and stays inside the range; it is the slot that chooses, not the view. What the channel says changes nothing in the sizing of the view.

#### Whatever the Channel Says

Now measure each of the three messages in the `first` group separately: 180, 400, and 96. All three give the same tree a different request.

```dart
// main.dart
import 'sizing.dart';
import 'channel.dart';

Part buildScreen(int requested) {
  return pad('Screen', 16, column('Body', [
    leaf('Title', 120),
    fixed('Embed', 240, leaf('PlatformView', requested)),
  ]));
}

Future<void> main() async {
  final messages = await receiveMessages('first');
  for (final message in messages) {
    final log = measure(buildScreen(widthOf(message)), 360);
    final view = log.firstWhere((entry) => entry.path == '/Screen/Body/Embed/PlatformView');
    print('message=${message['id']} request=${widthOf(message)} ${view.sizing} width=${view.width} constraint=${view.limit.bounds()}');
  }
}
```

```text
message=1 request=180 forced width=240 constraint=240..240
message=2 request=400 forced width=240 constraint=240..240
message=3 request=96 forced width=240 constraint=240..240
```

The three requests are 180, 400, and 96, and in all three lines the sizing is `forced` and the width is 240. The message that asks for 400 does not enlarge the view inside the slot, and the message that asks for 96 does not shrink it. Because the message comes from the platform, it carries a value, but in the tree this value is only a request; under a tight constraint a request is not something that affects the width.

This is a different question from **how many copies of the sent event reached the receiver** on the channel. That question counts how many times an event passed through the channel; this lesson asks, as the result of a message, who holds the width of the view. However many messages arrive, the framework forces the width of the embedded view.

#### The Bounding Measurement

The channel can also send a message more than once. The `repeat` group has five messages: `1` twice, `2` once, and `3` twice. The measurement below gives all five to the tree and merges the total table in one place. Then the screen width goes down to 200.

```dart
// main.dart
import 'sizing.dart';
import 'channel.dart';

Part buildScreen(int requested) {
  return pad('Screen', 16, column('Body', [
    leaf('Title', 120),
    fixed('Embed', 240, leaf('PlatformView', requested)),
  ]));
}

Future<void> main() async {
  final messages = await receiveMessages('repeat');
  final all = <Sizing>[];
  for (final message in messages) {
    all.addAll(measure(buildScreen(widthOf(message)), 360));
  }
  print('message count=${messages.length}');
  countSizings(all);
  final narrow = measure(buildScreen(widthOf(messages.first)), 200);
  final view = narrow.firstWhere((entry) => entry.path == '/Screen/Body/Embed/PlatformView');
  print('screen=200 ${view.sizing} width=${view.width} constraint=${view.limit.bounds()}');
}
```

```text
message count=5
forced: 15
chosen: 10
clamped: 0
stretched: 0
parent: 15
screen=200 forced width=168 constraint=168..168
```

The focus node is `/Screen/Body/Embed/PlatformView`. Five messages measure the tree five times, and each measurement gives three `forced` records: `/Screen`, `/Screen/Body`, and the view. The totals `forced: 15` and `chosen: 10` come out for this reason: 5 times 3 and 5 times 2. Repeated messages increase the number of records, but they do not change the sizing of any record: the view is forced in five of the five measurements. What counts the copies is the counter; what tells the owner of the width is the rule.

When the screen goes down to 200, after the padding the body stays at a width of 168. The slot asks for 240, but because the constraint is 0..168 the slot comes down to 168; the view also takes 168 with the tight constraint the slot gives. The last line of the output shows this: the sizing is again `forced`, and the constraint is `168..168`. The result does not change when the message asks for 180 either, because a request does not change a tight constraint.

**AS1.** The model measures only the width. The view of the platform, what it draws inside itself, the height of the view, touch events, and the timing of the channel calls are not in the model. The model builds this sizing's rule; the model is not the framework itself. Counting the slot as a fixed 240 is the assumption of the lesson; a real slot may give the constraint to the view in a different way.

#### Summary

- This lesson's sizing: forced. The focus node is `/Screen/Body/Embed/PlatformView`, and with the `240..240` constraint that the slot gives it takes 240 whether the request in the message is 180, 400, or 96.
- The platform channel is a named line of conversation between Dart and the platform; the width it carries is only a request in the tree.
- Three different requests came out forced in all three lines; repeated messages increased only the number of records, not the sizing.
- The owner of the width of the embedded view is not the side that listens to the channel, but the framework that puts the view into the slot.

#### Next Step

In this lesson the request that came from the channel did not enter the result, and the slot forced the width of the view. The next topic, `Test Types`, measures the same screen on two test surfaces: at widths of 400 and 320. The `/Screen/Body/Chart` node that asks for 300 is chosen at 400 and clamped at 320, and every node measured has entered the layout.

## Quality and Release

Test types, developer tools, performance tuning that cuts unnecessary rebuilds, package management and version constraints, per-platform build and release.

### Test Types

Source: https://academia.sh/en/courses/flutter/test-types

A test compares the expected sizing with the measured record. The lesson measures the same screen on two test surfaces and shows what a test cannot see.

The earlier lessons built the structure of the app: widgets were arranged into a tree, and state and a platform channel were connected. There is a working screen, but one person looking at it and saying that it works is not enough. This lesson explains how to write down the expected behavior of a screen and have the machine check it on every change. The subject is test types; the question that drives the course, who determines this widget's width, also becomes a test expectation in this lesson.

#### Three Test Types

Tests are separated by what they bring together. A **unit test** takes a single function or class, isolates it from its surroundings, and checks the output for a given input. There is no tree, no screen, and no platform; the test is fast, and it is easy for it to say why it failed. A **widget test** builds a single widget or a small widget tree with the real framework, but does not open a real device. The test builds the tree, simulates a tap, and then queries the tree for the presence or the value of a widget. An **integration test** runs the whole app, or a large part of it, on a device or an emulator and walks the path the user would follow from start to finish.

The trade-off among the three is simple. A unit test runs quickly but does not see how widgets fit together. An integration test sees everything but is slow, its setup is heavy, and when it fails it does not say where the error is. A widget test sits between the two: it sees the tree and does not see the platform. Sizing, the axis of this course, lives in exactly this middle layer. The width of a widget depends on the constraint from its parent and on its own request; these two inputs appear when a tree is built, not by calling a single function and not by walking the app from start to finish.

#### The Measured Layer

Every lesson of the course uses the same plain Dart model. The model gives a widget tree the screen width and records who determines each node's width with four names: **forced**, **chosen**, **clamped**, **stretched**. The file below is the core of this lesson; it produces no output, and the measured blocks import it.

```dart
// 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}');
}
```

#### Unit Test: No Tree

The snippet below is a unit test. It checks a counter function and asks the framework for only a test definition and an expectation. Because it is framework code, this snippet does not run; the lesson claims no output for it.

```dart
// Platform code: unit test
void main() {
  test('counter increments by one', () {
    final counter = Counter();
    counter.increment();
    expect(counter.value, 1);
  });
}
```

The test has three parts: setup, action, expectation. A `Counter` is built, `increment` is called, and the value is expected to be one. No width, constraint, or screen appears anywhere. A unit test cannot answer the question of this course, because the question requires a widget to stand inside a parent.

#### Widget Test: A Tree, No Platform

A widget test builds the tree. The framework offers a test environment for this: the widget is placed on a fake screen, one frame is drawn, and the queries of the test walk the tree.

```dart
// Platform code: widget test
void main() {
  testWidgets('title is visible', (tester) async {
    await tester.pumpWidget(const ExampleScreen());
    expect(find.text('Report'), findsOneWidget);
    expect(tester.getSize(find.byType(Chart)).width, 300);
  });
}
```

The last line matters. The test asks for the width of a widget and expects 300. This expectation assumes that nobody will change the widget's own request of 300 units. The screen width on which the test runs carries this assumption, but the test does not write it down. The test surface, that is, the width of the fake screen of the test, is the hidden input of a widget test.

The sections below make this input visible. In the model, a test is a small function that finds a path in the measured record list and compares the sizing name with the expected one.

#### The Screen Under Test

The screen to be checked is a column of two widgets: a title that requests 120 units and a chart whose width can be set. The screen is built by a function that takes the width the chart requests; so the tree depends on state, and each measurement builds its own tree. The function that builds the tree sits in the model file.

```dart
// screen_model.dart — the widget tree of the screen under test
import 'sizing.dart';

Part buildScreen(int chartWant) => pad('Screen', 16, column('Body', [
      leaf('Title', 120),
      leaf('Chart', chartWant),
    ]));
```

The function that compares the expectation also sits in a separate file. It looks for the path in the measured record list; if the path exists, it compares the sizing name with the expected one, and if it does not exist, it says so. The expected value is not given to `printSizes`; only this function writes the output.

```dart
// check_sizing.dart — compares the expected sizing with the measured record
import 'sizing.dart';

void checkSizing(List<Sizing> log, String path, String expected) {
  final found = log.where((entry) => entry.path == path).toList();
  if (found.isEmpty) {
    print('FAIL: $path no record');
    return;
  }
  final actual = found.first.sizing;
  if (actual == expected) {
    print('PASS: $path $expected');
  } else {
    print('FAIL: $path expected=$expected measured=$actual');
  }
}
```

#### A Test That Passes on a 400-Unit Surface

First the screen is placed on a test surface of 400 units. The chart requests 300 units, and the test expects the chart's request to be **chosen**. The block below prints the whole record and then runs the check.

```dart
// main.dart
import 'sizing.dart';
import 'screen_model.dart';
import 'check_sizing.dart';

void main() {
  final log = measure(buildScreen(300), 400);
  printSizes(log);
  checkSizing(log, '/Screen/Body/Chart', 'chosen');
}
```

```text
forced: /Screen width=400 constraint=400..400
forced: /Screen/Body width=368 constraint=368..368
chosen: /Screen/Body/Title width=120 constraint=0..368
chosen: /Screen/Body/Chart width=300 constraint=0..368
PASS: /Screen/Body/Chart chosen
```

The root receives the tight screen constraint, the padding takes 16 units from each side, and the body sees a tight constraint of 368 units. The column loosens the constraint; the chart's request of 300 units fits the range 0..368. The chart chose its own width, and the test passes.

#### A Test That Fails on a 320-Unit Surface

The same screen and the same expectation, with only the test surface at 320 units. A new developer wants to run the same test for a narrower device; nothing changes in the code.

```dart
// main.dart
import 'sizing.dart';
import 'screen_model.dart';
import 'check_sizing.dart';

void main() {
  final log = measure(buildScreen(300), 320);
  printSizes(log);
  checkSizing(log, '/Screen/Body/Chart', 'chosen');
}
```

```text
forced: /Screen width=320 constraint=320..320
forced: /Screen/Body width=288 constraint=288..288
chosen: /Screen/Body/Title width=120 constraint=0..288
clamped: /Screen/Body/Chart width=288 constraint=0..288
FAIL: /Screen/Body/Chart expected=chosen measured=clamped
```

The body now sees a tight constraint of 288 units. The chart requests 300, but the most it is allowed is 288; the resulting width is 288 and the sizing is **clamped**. The expectation was "**chosen**", so the test fails. The failing test has found an error: on the narrow surface, the screen shows its chart smaller than its request. Where the error is can also be read from the output, because the `expected` and `measured` fields stand side by side.

There are two ways forward here, and which one to take is a design decision. The screen can shrink the chart on the narrow surface; if this is the intended behavior, the expectation changes. Or the screen can put the chart into a scrollable area; in this case the tree changes and the test passes again. The test does not say which is right; it only makes the difference between the two cases visible.

#### Correcting the Expectation

If being **clamped** on the narrow surface is the intended behavior, the expectation is written accordingly. The block below checks two expectations together. The first correctly expects **clamped**. The second asks for a node that does not exist in the tree.

```dart
// main.dart
import 'sizing.dart';
import 'screen_model.dart';
import 'check_sizing.dart';

void main() {
  final log = measure(buildScreen(300), 320);
  checkSizing(log, '/Screen/Body/Chart', 'clamped');
  checkSizing(log, '/Screen/Body/Legend', 'chosen');
}
```

```text
PASS: /Screen/Body/Chart clamped
FAIL: /Screen/Body/Legend no record
```

The second line shows an important rule: a node that is not in the tree has no record. A silent situation in which a test is written but checks nothing arises from here. A test that misspells the name of a node would end with "no record" instead of "**chosen**"; this is why the check function does not count a missing path as passed but as failed.

#### What an Integration Test Adds

The test surface of a widget test is the screen width in the model; it is a number, and the author of the test chooses it. An integration test runs on a real device and uses the device's own screen width, font size, and safe-area inset. The same screen runs on two different devices with two different widths; an integration test sees whichever one it runs on.

This does not mean that an integration test replaces a widget test. A widget test chooses the surface on purpose: two numbers such as 400 and 320 represent two classes. An integration test takes the surface from the device and checks whatever device is at hand. Running the two together checks the tree on a few meaningful surfaces and the whole app on a few real devices.

#### Common Misconception

**If the tests pass, the screen is correct under every condition.** This is not so. The two measurements of this lesson show it: a test that passes at 400 units fails at 320 units. The test is the same and the screen is the same; the surface changed. A passing test does not show that the screen is correct under every condition; it shows only that it is correct under the condition the test chose.

This question must not be confused with the one in M12/K04. There, the question of a test was "did this piece run on this request", and the answer was coverage. Here, the question is who determines the width of a widget that ran. Every measured node has entered the layout, that is, it has run; sizing classifies only a node that enters the layout, and having run does not mean having been sized correctly.

#### The Bounding Measurement

The last measurement shows what a test cannot see. The test surface is given to the widget tree of the screen as an input; the test cannot change this input, and it cannot know in advance what will happen when the input changes. The block below looks only at the chart's record on the 320-unit surface and then gives the count of the whole record.

```dart
// main.dart
import 'sizing.dart';
import 'screen_model.dart';

void main() {
  final log = measure(buildScreen(300), 320);
  printSizes(log.where((entry) => entry.path == '/Screen/Body/Chart').toList());
  countSizings(log);
}
```

```text
clamped: /Screen/Body/Chart width=288 constraint=0..288
forced: 2
chosen: 1
clamped: 1
stretched: 0
parent: 3
```

The focus node is `/Screen/Body/Chart`. On the 320-unit surface the chart came out **clamped**; in three of the four records, the width is determined not by the widget itself but by the parent or the range. The title is still **chosen**, but the chart can no longer fulfill its request. This lesson's sizing: **clamped**. The narrow test surface is the place where the chart's request does not fit the range.

**QR1.** The model builds this sizing's rule; the model is not the framework itself. In the model, a test finds a path in the measured record list and compares the sizing name. A real widget test queries the size of a widget with the real measurements of the device and is affected by inputs the model does not have, such as the font size. The expectations measured in the model do not match the queries of a real test one to one.

#### Summary

- This lesson's sizing: **clamped**. On the narrow test surface, `/Screen/Body/Chart` fell short of its request, and the expected "**chosen**" did not hold for this reason.
- A unit test isolates a function, a widget test builds a tree, and an integration test walks the app from start to finish on a device.
- Sizing is a property of the tree; so a unit test cannot see it, and a widget test sees it by choosing the test surface.
- `checkSizing` compares the expected name with the measured record and gives `PASS` or `FAIL`; a path that is not in the tree is not counted as passed.
- The same screen passes at 400 units and fails at 320 units: the test surface is the hidden input of a widget test.

#### Next Step

When a test fails, saying where the error is is one thing; seeing why it happens is another. The next lesson, `Developer Tools`, covers the tools that inspect the tree while it runs, and shows the constraint a widget received, its width, and which widget a piece of output came from. The third lesson of the topic, `Performance Tuning`, explains how to reduce unnecessary changes in the tree.

### Developer Tools

Source: https://academia.sh/en/courses/flutter/developer-tools

The widget inspector, layout inspection, and the performance view show a tree while it runs. The lesson matches the constraint and width the inspector shows with the measured record.

In the previous lesson a test failed, and the path where the error occurred was named. Seeing why it happens is the subject of a different task: opening the tree while it runs and looking at it. This lesson reads the development tools of the framework along the sizing axis. When a tool shows the width of a widget on the screen, it is in fact answering the question of the course: who determined this width.

#### Four Tools

**The widget inspector** shows the widget tree of the running app. When a widget is selected, you see not only its properties but also the constraint it received and the resulting size. The documentation of the framework says this on the subject: "The constraints flow down the tree, and the sizes flow back up." That is, constraints go down the tree and sizes come back up; the two fields to look at in layout problems are `size` and `constraints`.

**Layout inspection** is a separate view inside the inspector for flexible widgets. When a widget related to a row, a column, or flexible children is selected, the flex factor, the flex fit, and the constraints are visualized; constraint violations are shown in red, and overflow errors are shown with a pattern of yellow stripes. The values you change in this view are not written to the source code and are reverted on hot reload.

**The performance view** shows time, not width: it lines frames up on a chart, marks slow frames in red, and opens what work was done in a frame on the event chart. There is also an option that shows at what moment the widget structures were built. The documentation recommends taking the measurement in the performance measurement mode, not in development mode, because frame times in development mode do not reflect the performance of the release version. The numbers of this lesson contain no time; the performance view is the subject of the next lesson.

**Hot reload** sends the changed source code to the running app and rebuilds the tree. `main` and `initState` do not run again; the state of the app is preserved. According to the documentation, this works only in development mode. Because the state is preserved, a change in the source code may not be visible on the screen: for example, changing an initial value has no effect if the value has already been set. Then a hot restart, which starts the app from the beginning, is needed.

#### The Measured Layer

Every lesson of the course uses the same plain Dart model. The model gives a widget tree the screen width and records who determines each node's width with four names. The file below produces no output; the measured blocks import it.

```dart
// 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}');
}
```

In this lesson the model builds this sizing's rule; the model is not the framework itself. In the model, the two values the inspector shows for a widget are the `width` and `constraint` fields in the `printSizes` output. The model does not carry the performance view or hot reload; the second is approached by measuring the tree again.

#### The Inspected Screen

The tree of the screen is a column: a label that requests 150 units and a `Chip` that requests 80 units inside a `Center`. The tree is built by a function that takes the width of the chip.

```dart
// panel_model.dart — the widget tree of the inspected screen
import 'sizing.dart';

Part buildPanel(int chipWidth) => pad('Screen', 16, column('Body', [
      leaf('Label', 150),
      center('Center', leaf('Chip', chipWidth)),
    ]));
```

In the source counterpart of this tree, `Center` only wraps the chip. The developer knows that the chip is 80 units and expects the space given to it to be about that much as well. This expectation lasts until the inspector is consulted.

#### A Widget That Looks Unexpectedly Wide

The screen is 360 units wide. The block below prints the whole record; each line holds the values the inspector would show for a widget.

```dart
// main.dart
import 'sizing.dart';
import 'panel_model.dart';

void main() {
  final log = measure(buildPanel(80), 360);
  printSizes(log);
}
```

```text
forced: /Screen width=360 constraint=360..360
forced: /Screen/Body width=328 constraint=328..328
chosen: /Screen/Body/Label width=150 constraint=0..328
stretched: /Screen/Body/Center width=328 constraint=0..328
chosen: /Screen/Body/Center/Chip width=80 constraint=0..328
```

`Chip` requests 80 units and receives 80 units: **chosen**. `Center`, on the other hand, is 328 units, much wider than the chip. Under a bounded constraint, a centering widget does not report a request; it spreads to the widest value given to it. In the language of the course, this is **stretched** sizing. When `Center` is selected in the inspector, the `size` you see is 328 and the `constraints` are between 0 and 328. The developer looked at the chip and expected `Center` to be narrow too; but its width was determined not by the chip but by the constraint of its parent.

#### The Constraint Reaches the Sibling Unchanged

In M11/K05 the question was what an operation leaves for the resource: was the resource consumed once, and what is its state afterward. For a layout constraint this question is invalid, because a layout constraint is not consumed. The column loosens the constraint that comes to the body and gives the same loose constraint to every child; when the label takes its width, the share of `Center` does not shrink. The block below lists all widgets that receive the constraint `0..328`.

```dart
// main.dart
import 'sizing.dart';
import 'panel_model.dart';

void main() {
  final log = measure(buildPanel(80), 360);
  printSizes(log.where((entry) => entry.limit.bounds() == '0..328').toList());
}
```

```text
chosen: /Screen/Body/Label width=150 constraint=0..328
stretched: /Screen/Body/Center width=328 constraint=0..328
chosen: /Screen/Body/Center/Chip width=80 constraint=0..328
```

`Label` and `Center` receive the same constraint. `Chip` receives the same constraint too, because the centering widget loosens the constraint but keeps the upper bound. This is why the constraint fields of two siblings side by side in the inspector look the same. The only thing that narrows the constraint is the padding, and in this tree it is only at the `Screen` level.

#### Overflow and Layout Inspection

What happens if the chip's request exceeds the constraint? The block below makes the chip 400 units and looks at the records under `Center`.

```dart
// main.dart
import 'sizing.dart';
import 'panel_model.dart';

void main() {
  final log = measure(buildPanel(400), 360);
  printSizes(log.where((entry) => entry.path.startsWith('/Screen/Body/Center')).toList());
}
```

```text
stretched: /Screen/Body/Center width=328 constraint=0..328
clamped: /Screen/Body/Center/Chip width=328 constraint=0..328
```

The chip requests 400, and the most that is allowed is 328. Its width is pulled to 328 and it is **clamped**. In the real framework the same situation appears as an overflow error: the striped yellow pattern of layout inspection is useful exactly here. In the model, overflow is not an image but a class; the record says only that the request falls outside the range.

#### Hot Reload Measures the Tree Again

Hot reload rebuilds the tree. In the model, its counterpart is calling the same tree function with the new state and measuring the tree again. The block below changes the chip width from 80 to 400 and prints the counts of the two measurements side by side.

```dart
// main.dart
import 'sizing.dart';
import 'panel_model.dart';

void main() {
  for (final chip in [80, 400]) {
    print('chip=$chip');
    countSizings(measure(buildPanel(chip), 360));
  }
}
```

```text
chip=80
forced: 2
chosen: 2
clamped: 0
stretched: 1
parent: 3
chip=400
forced: 2
chosen: 1
clamped: 1
stretched: 1
parent: 4
```

When the chip value changes, one widget moves from **chosen** to **clamped**; the parent count rises from three to four. In real hot reload, the state of the app is preserved; in the model, measuring again builds a tree from scratch each time. The model has no preserved state.

#### Reading What the Tool Shows

The order to follow when looking at an inspector is simple. First select the widget. Then look at the constraint it received: if it is tight, the parent has imposed the size. If it is bounded and loose, look at the widget's own request: if the request fits the range, it is **chosen**; if it does not fit, it is **clamped**. If there is no request and the upper bound is finite, the width is the upper bound: **stretched**. These three questions separate all four sizings.

The two snippets below are the framework code counterpart of the same question. The first turns on a flag that draws the bounds of every widget on the screen; the second shows a centered wrapper.

```dart
// Platform code: drawing layout bounds
void main() {
  debugPaintSizeEnabled = true;
  runApp(const ExampleScreen());
}
```

This flag draws the bounds of every widget and is effective only in development mode.

```dart
// Platform code: centered wrapper
Column(
  children: [
    const Text('Label'),
    Center(child: Chip(label: Text('Label'))),
  ],
)
```

In this two-line layout, `Center` spreads to the width given to it. The inspector shows this spreading; in the model, the same spreading has been measured for the `Center` node.

#### Common Misconception

**The content of a widget that looks wide in the inspector is wide too.** This is not so. `Center` is 328 units, but the chip inside it is 80 units. The bounds of a widget and its content are separate things; the inspector shows the bounds, and the content's own request must be read separately.

#### The Bounding Measurement

The last measurement shows that the rule, like the inspector, sees only the current constraint. When the screen goes from 360 units down to 240 units, the width of `Center` stays tied to the screen, not to the chip.

```dart
// main.dart
import 'sizing.dart';
import 'panel_model.dart';

void main() {
  for (final screen in [360, 240]) {
    final log = measure(buildPanel(80), screen);
    printSizes(log.where((entry) => entry.path == '/Screen/Body/Center').toList());
  }
}
```

```text
stretched: /Screen/Body/Center width=328 constraint=0..328
stretched: /Screen/Body/Center width=208 constraint=0..208
```

The focus node is `/Screen/Body/Center`. It comes out **stretched** on both screens; its width falls from 328 to 208, while the chip is 80 units in both measurements. This lesson's sizing: **stretched**. The screen determines the width of the centering widget.

**QR1.** The model builds this sizing's rule; the model is not the framework itself. In the model, the inspector's value is one record line. A real inspector shows not only the constraint and size of a widget but also the properties of the render object and the flex factor; these are not in the model. The frame chart of the performance view and the preserved state of hot reload are not measured in the model either.

#### Summary

- This lesson's sizing: **stretched**. `Center` did not report a request under a bounded constraint and spread to 328 units; the chip inside it stayed **chosen** at 80 units.
- The widget inspector shows the constraint and size, layout inspection shows flexible widgets, and the performance view shows frames.
- The constraint and width the inspector shows are, in the model, the `constraint` and `width` fields of a `printSizes` line.
- A layout constraint is not consumed: the same loose constraint came down as `0..328` for `Label`, `Center`, and `Chip` alike.
- Hot reload rebuilds the tree and preserves the state; its counterpart in the model is measuring the tree again with the new state.

#### Next Step

The inspector shows a problem, but most of the time the problem is not a wrong width but unnecessary work. The next lesson, `Performance Tuning`, covers the methods that reduce unnecessary rebuilds in the tree, and the relayout boundary, where the change of a widget does not leak upward.

### Performance Tuning

Source: https://academia.sh/en/courses/flutter/performance-tuning

Unnecessary rebuilds and relayouts are the most common source of performance problems. The lesson shows with a measured record how high a change climbs in the tree.

In the previous lesson the inspector showed a problem. Most of the time the problem is not a wrong width but unnecessary work: the unchanging subtree of a widget is rebuilt or measured again on every change. This lesson looks at two questions. How high does the change of one widget climb in the tree? How much work is being done in the tree?

Performance itself is a number, and this lesson writes no number; it measures no time. What is written is how many nodes a change touches. Time and the frame budget are the subject of other work.

#### The Relayout Boundary

The first question is how high the change climbs in the tree. If the request of a widget changes and its parent reads the size of the child, the parent must be laid out again as well; if the parent of that parent also reads the size of its child, the change continues upward. According to the documentation of the framework, if the parent does not read the size of the child, or if the constraint given to the child is tight, the change stops there: the child can have only one size, and the parent knows it. This point is called a relayout boundary.

The measurable claim of this lesson is this: a widget that gives a tight constraint stops the change beneath it from climbing. In the model, this claim is tested by counting which nodes change width when the request of a widget changes.

#### The Measured Layer

Every lesson of the course uses the same plain Dart model. The model gives a widget tree the screen width and records who determines each node's width with four names. The file below produces no output; the measured blocks import it.

```dart
// 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}');
}
```

In this lesson the model builds this sizing's rule; the model is not the framework itself. In the model, a change is calling the same tree function again with another request and comparing the two records. The model does not carry the rebuild work or the time of the framework.

#### The Counted Screen

Two trees carry the same counter, that is, the leaf whose request will change. In the first, the counter is inside a fixed panel of 200 units; the panel gives its child a tight constraint. In the second, the panel is only padding, and the request of the counter passes to the width of the panel.

```dart
// counter_model.dart — the two environments of the counter
import 'sizing.dart';

Part buildBounded(int want) => pad('Screen', 16, column('Body', [
      leaf('Title', 120),
      fixed('Panel', 200, leaf('Counter', want)),
    ]));

Part buildLoose(int want) => pad('Screen', 16, column('Body', [
      leaf('Title', 120),
      pad('Panel', 8, leaf('Counter', want)),
    ]));
```

The helper that counts the change matches the two records by path. It counts every node that is new or whose width changed.

```dart
// change_count.dart — counts the nodes whose width changed between two measurements
import 'sizing.dart';

int countChanged(List<Sizing> before, List<Sizing> after) {
  var changed = 0;
  for (final entry in after) {
    final match = before.where((old) => old.path == entry.path).toList();
    if (match.isEmpty || match.first.width != entry.width) {
      changed++;
    }
  }
  return changed;
}
```

#### The Panel With a Tight Constraint

The first measurement prints the whole record of the bounded environment. The screen is 360 units and the counter requests 100 units.

```dart
// main.dart
import 'sizing.dart';
import 'counter_model.dart';

void main() {
  final log = measure(buildBounded(100), 360);
  printSizes(log);
}
```

```text
forced: /Screen width=360 constraint=360..360
forced: /Screen/Body width=328 constraint=328..328
chosen: /Screen/Body/Title width=120 constraint=0..328
chosen: /Screen/Body/Panel width=200 constraint=0..328
forced: /Screen/Body/Panel/Counter width=200 constraint=200..200
```

The panel requests 200 units and receives 200 units: **chosen**. What the counter requests does not matter, because the panel gives it a tight constraint between 200 and 200. The width of the counter is determined not by the counter's request but by the panel: **forced**.

#### When the Request Changes

Now the request of the counter rises from 100 to 300. In the bounded environment we compare the two records.

```dart
// main.dart
import 'sizing.dart';
import 'counter_model.dart';
import 'change_count.dart';

void main() {
  final before = measure(buildBounded(100), 360);
  final after = measure(buildBounded(300), 360);
  printSizes(after.where((entry) => entry.path.endsWith('Counter')).toList());
  print('changed: ${countChanged(before, after)}');
}
```

```text
forced: /Screen/Body/Panel/Counter width=200 constraint=200..200
changed: 0
```

The counter is 200 units in both measurements, and the width of no node has changed. The change stopped at the panel. The same idea holds in the real framework as well: a widget that gives a tight constraint does not carry the change beneath it upward. Which nodes will actually be laid out again is a separate matter and is not measured in this lesson.

#### The Panel That Gives No Tight Constraint

We try the same change on a panel made of padding. Here the panel gives its child no tight constraint.

```dart
// main.dart
import 'sizing.dart';
import 'counter_model.dart';
import 'change_count.dart';

void main() {
  final before = measure(buildLoose(100), 360);
  final after = measure(buildLoose(300), 360);
  printSizes(after.where((entry) => entry.path.contains('Panel')).toList());
  print('changed: ${countChanged(before, after)}');
}
```

```text
chosen: /Screen/Body/Panel width=316 constraint=0..328
chosen: /Screen/Body/Panel/Counter width=300 constraint=0..312
changed: 2
```

The counter's request of 300 units fits between 0 and 312 and stays **chosen**. The panel wraps the width of its child with 8 units of padding on each side; when the counter changes, the width of the panel also rises from 116 to 316. Two nodes changed: the counter and the panel. Because the column body receives a tight constraint, the change did not climb as far as the body and the screen. The change climbed one step above the counter, to the panel, and stopped at the tight constraint in the body.

#### Reading the Path of the Change

The two measurements send the same change down different paths. In the panel with a tight constraint, the path of the counter ends at the panel: the parent never asks for the request of the child, because it has already imposed the answer. In the loose panel, the parent depends on the size of the child; the width of the panel derives from the width of the counter, and the change climbs one step. The body cuts this chain, because the screen forces the width of the body.

This reading is useful when looking for a performance problem. If a widget changes often, first look at how far up it is depended on. If every parent above reads the size of its child, a small change can cause a large subtree to be laid out again. Placing a widget that gives a tight constraint in between shortens this chain; the price is that the width of the child is now determined by that widget, not by the child. Even if the counter requests 300 units, it stays at 200 units. So there is a trade-off between the performance gain and sizing freedom, and the way to see this trade-off is to look at which node is **forced**.

#### Unnecessary Rebuilds and the Const Constructor

When a widget changes, the framework rebuilds the tree beneath it. If the subtree stays the same, this work is wasted. The documentation of the framework says that most of the rebuild work can be skipped for a widget built with an unchanging const constructor. The idea is simple: the part that does not change is not produced as a new object every time; the same constant object is used.

The snippet below shows the two forms side by side; it is framework code outside the measured model.

```dart
// Platform code: const constructor and a new object every time
Column(
  children: [
    const Text('Title'),
    Text('Counter: $counter'),
  ],
)
```

The first child never changes and is built with a const constructor. The second changes as the counter changes, so it cannot be constant. A const constructor reduces the rebuild work; it does not change the size, that is, the width. The question of who determines the width stays the same in this snippet.

#### A Performance Problem Versus a Budget Overrun

In M31/K06, performance is asked as whether a budget has been exceeded; here the question is who determines the width, and there is no budget. A **clamped** widget is not counted as having exceeded a budget; its request does not fit the allowed range. In this lesson no time was written and none will be; what is written is how many nodes a change touches.

#### Common Misconception

**A const constructor changes or preserves the width of a widget.** Both are wrong. A const constructor reduces the rebuild work; the question of who determines the width stays the same. Even if the counter were built with a const constructor, it would be 200 **forced** in `buildBounded` and 300 **chosen** in `buildLoose`.

#### The Bounding Measurement

The last measurement ties the boundary rule to the difference between a tight constraint and a loose constraint. The same change runs in two environments, and only the number of changed nodes is printed.

```dart
// main.dart
import 'sizing.dart';
import 'counter_model.dart';
import 'change_count.dart';

void main() {
  final bounded = countChanged(measure(buildBounded(100), 360), measure(buildBounded(300), 360));
  final loose = countChanged(measure(buildLoose(100), 360), measure(buildLoose(300), 360));
  print('bounded: $bounded');
  print('unbounded: $loose');
}
```

```text
bounded: 0
unbounded: 2
```

The focus node is `/Screen/Body/Panel/Counter`. In the environment with a tight constraint it is **forced**, and the number of changed nodes is zero; in the loose environment it is **chosen**, and two nodes change. This lesson's sizing: **forced**. The width of the counter was determined not by the changing request but by the tight constraint of the panel.

**QR1.** The model builds this sizing's rule; the model is not the framework itself. In the model, a change is the comparison of two records and carries no time. The rebuild work, the object lifetime, and the relayout order of the framework are not in the model. The exact conditions of the relayout boundary are in the documentation of the framework; the model only illustrates the effect of a tight constraint.

#### Summary

- This lesson's sizing: **forced**. The width of the counter was determined not by the changing request but by the tight constraint of the panel.
- A const constructor reduces the rebuild work; it does not change the rule that determines the size.
- A widget that gives a tight constraint stops the change beneath it from climbing; in the model the number of changed nodes was 0.
- Under a loose constraint, the same change altered two nodes.
- Clamped sizing is not a budget overrun; the records of this lesson carry no time.

#### Next Step

Performance tuning targets the app's own tree. But an app does not consist only of its own code most of the time: packages written by others also enter the tree. The next lesson, `Package Management`, covers what happens in the tree when the version of a package changes.

### Package Management

Source: https://academia.sh/en/courses/flutter/package-management

An app grows with packages written by others. The lesson explains the package repository, the version constraint, and the lock file; it measures how the new version of a package affects the width of a widget.

The previous lesson targeted the tree of the app itself. But an app does not consist only of its own code: a rating bar, an image cache, or a date picker comes from packages written by others. When a new version of a package is released, what changes in the app? This lesson first lists the four parts of package management, and then measures how a version upgrade shows up in the width of a widget.

#### Four Parts

**The package repository** is the shared place where packages are published with their versions and from which tools download them. **A dependency** is the declaration that says an app needs a package. **A version constraint** writes, as a range, which versions of the dependency are accepted. **The lock file** stores the result of resolution: whichever version the tool chose once, it records, and later installs bring the same version.

According to the documentation of the framework, the declaration of an app is the `pubspec.yaml` file, and the resolved versions stay in the `pubspec.lock` file. For apps, the documentation recommends adding the lock file to version control; this way every developer and every build works with the same versions.

#### The Measured Layer

Every lesson of the course uses the same plain Dart model. The model gives a widget tree the screen width and records who determines each node's width with four names. The file below produces no output; the measured blocks import it.

```dart
// 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}');
}
```

In this lesson the model builds this sizing's rule; the model is not the framework itself. In the model, a package version is a number that gives the default width of a widget. The model does not carry package resolution, version ranges, or downloading.

#### The Declaration

The snippet below shows the dependency in the declaration. The package name is fictional.

```yaml
# Platform code: package declaration
dependencies:
  example_package: ^1.3.0
```

`^1.3.0` accepts version 1.3.0 and the later versions compatible with it. This is a version constraint. A version constraint and a layout constraint are two separate things: the first says which package version will be installed, the second says in which width range a widget will be measured, and there is no link between them.

#### Two Versions of the Package

The rating widget of the fictional package arrives with a default width of 96 units in version 1 and 112 units in version 2. The app puts the widget under a title and does not give the width itself. The tree is built by a function that takes the version.

```dart
// package_model.dart — the default width given by the two versions of the package
import 'sizing.dart';

int ratingWidth(int version) => version == 1 ? 96 : 112;

Part buildRating(int version) => pad('Screen', 16, column('Body', [
      leaf('Title', 120),
      leaf('Rating', ratingWidth(version)),
    ]));
```

#### The Old Version

The first measurement prints the whole record for version 1. The screen is 360 units.

```dart
// main.dart
import 'sizing.dart';
import 'package_model.dart';

void main() {
  final log = measure(buildRating(1), 360);
  printSizes(log);
}
```

```text
forced: /Screen width=360 constraint=360..360
forced: /Screen/Body width=328 constraint=328..328
chosen: /Screen/Body/Title width=120 constraint=0..328
chosen: /Screen/Body/Rating width=96 constraint=0..328
```

The title requests 120 and the rating requests 96 units, and both fit the range between 0 and 328 that they receive. Both of them chose their own width.

#### The New Version

Now the same tree is built with version 2. Nothing has changed in the code of the app; only the lock file points to the new version. The block below measures the two versions side by side and prints only the line of the rating.

```dart
// main.dart
import 'sizing.dart';
import 'package_model.dart';

void main() {
  for (final version in [1, 2]) {
    print('version=$version');
    final log = measure(buildRating(version), 360);
    printSizes(log.where((entry) => entry.path == '/Screen/Body/Rating').toList());
  }
}
```

```text
version=1
chosen: /Screen/Body/Rating width=96 constraint=0..328
version=2
chosen: /Screen/Body/Rating width=112 constraint=0..328
```

In the new version the rating is 112 units and is again **chosen**. The constraint it receives is the same: `0..328`. The only thing that changed is the widget's own request. The version upgrade did not change the constraint of any node in the tree.

#### Where This Change Reaches

In M12/K02 the question was where a code change spreads: who depends on a part that changed, and where does the effect reach. In sizing there is no question of spreading. When the version of the package changes, the layout rule does not know it; the rule sees only the new request, not which version the request came from. We can verify this with the counts as well.

```dart
// main.dart
import 'sizing.dart';
import 'package_model.dart';

void main() {
  for (final version in [1, 2]) {
    print('version=$version');
    countSizings(measure(buildRating(version), 360));
  }
}
```

```text
version=1
forced: 2
chosen: 2
clamped: 0
stretched: 0
parent: 2
version=2
forced: 2
chosen: 2
clamped: 0
stretched: 0
parent: 2
```

The counts of the two versions are the same. On a 360-unit screen, the version upgrade did not change the class of any node.

#### Locking the Declaration

The lock file picks a single version from the range the version constraint allows and stores it. If there is no lock file, or if it was not added to version control, the same `^1.3.0` constraint can bring different versions at different times. In the rating example, this means the same code shows a widget of 96 units in one build and 112 units in another. So the lock file is not only a build convenience; it is also a condition for the screen to look the same.

This does not contradict the rule that a version constraint and a layout constraint are separate. The lock file fixes which version will be installed; once the version is installed, the request of the widget is just a number and is measured at its place in the tree.

#### Common Misconception

**Upgrading a package breaks or fixes the layout on the screen.** An upgrade does not change the layout rule; it changes only the request of the widget. What happens on the screen is again determined afterward by the constraint. If the request fits the range, the result makes no difference; if it does not fit, it is **clamped**. The narrow-screen measurement in the next section shows this second case.

#### On a Narrow Screen

The screen shrinks to 130 units wide. The padding takes 16 units from each side; a range of 98 units is left for the body. The request of 96 units of version 1 fits the range, and the request of 112 units of version 2 does not.

```dart
// main.dart
import 'sizing.dart';
import 'package_model.dart';

void main() {
  for (final version in [1, 2]) {
    print('version=$version');
    final log = measure(buildRating(version), 130);
    printSizes(log.where((entry) => entry.path == '/Screen/Body/Rating').toList());
  }
}
```

```text
version=1
chosen: /Screen/Body/Rating width=96 constraint=0..98
version=2
clamped: /Screen/Body/Rating width=98 constraint=0..98
```

The same new version becomes **clamped** on a 130-unit screen. The tree is the same and the rule is the same; only whether the request fits the range changed. The risk of a version upgrade for the app is here: when the request grows, clamping can be seen on a narrow screen.

#### The Bounding Measurement

The last measurement compares the two versions only by the class name. The screen is 360 units.

```dart
// main.dart
import 'sizing.dart';
import 'package_model.dart';

void main() {
  for (final version in [1, 2]) {
    final log = measure(buildRating(version), 360);
    final entry = log.firstWhere((item) => item.path == '/Screen/Body/Rating');
    print('version=$version ${entry.sizing}');
  }
}
```

```text
version=1 chosen
version=2 chosen
```

The focus node is `/Screen/Body/Rating` in the new version. It is **chosen** in both versions: the rule sees only the new request, not which version it came from. This lesson's sizing: **chosen**.

**QR1.** The model builds this sizing's rule; the model is not the framework itself. In the model, the package version is an integer function. Real package resolution, the intersection of version ranges, the generation of the lock file, and downloading from the package repository are not in the model. The real default width of a package is in the package's own documentation.

#### Summary

- This lesson's sizing: **chosen**. The rating in the new version requests 112 units on a 360-unit screen and chooses its own width in the range 0..328.
- The package repository publishes packages, a dependency declares, a version constraint writes a range, and the lock file stores the **chosen** version.
- A version constraint says which package version will be installed, a layout constraint says in which width range a widget will be measured; the two are separate.
- The version upgrade did not change the constraint of any node; it only raised the request of the widget from 96 to 112.
- The rule sees only the new request: at 360 units both versions come out **chosen**, and at 130 units the new version comes out **clamped**.

#### Next Step

Quality, tests, tools, performance, and packages have settled into place. The next lesson, which is the last of the topic, `Build and Release`, looks at whether the same image is **clamped** on both targets when the same app is released together to two targets, a phone and a tablet.

### Build and Release

Source: https://academia.sh/en/courses/flutter/build-and-release

After an app is written, it is built, signed, and distributed through a channel until it reaches the user. The lesson lists this path per platform, measures the same release for a phone and a tablet, and gathers the sizing of nineteen lessons in one table.

The previous lesson covered packages. What remains is delivering the app to the user: building it, signing it, and distributing it through a channel. This lesson first lists the parts of this path, then measures the same release for a phone and a tablet and looks at whether the same image is **clamped** on both targets. The lesson is the last lesson of the course; at the end, the sizing of nineteen lessons is gathered in one table.

#### Build Modes

According to the documentation of the framework, there are three build modes. **Debug** mode is for development: hot reload works only in this mode, and assertions are enabled. **Profile** mode is for performance analysis; the documentation says this mode is disabled on the emulator and the simulator, because the behavior there does not represent real performance. **Release** mode is for distribution: assertions are disabled, debugging information is removed, and the build is optimized for fast startup and a small package.

#### Output Per Platform

Each platform asks for its own package format. The Android documentation says the app store prefers the app bundle format; for stores that do not support the bundle, install files split per processor architecture are recommended. An iOS build produces an archive and a package to be uploaded to the store. The version number is declared in a single place for both platforms: three dotted numbers give the version, and the number after the plus sign gives the build number.

```yaml
# Platform code: version number
version: 1.4.0+12
```

`1.4.0` is the version visible to the user, and `12` is the build number. The iOS documentation says every upload to the store requires a unique build number.

#### Signing

Signing proves who a package came from and that it was not changed on the way. The Android documentation separates two keys: the **upload key** the developer uses when uploading, and the **app signing key** that signs the package reaching the user. The file in which the upload key is stored and the configuration file that gives it the key information are kept secret; the documentation asks that neither be added to public version control. On the iOS side, the development environment can manage signing and the provisioning profile; according to the documentation, this setting is on by default, and the developer chooses a team tied to a registered account.

#### Release Channel

A signed package does not have to go straight to everyone. The publishing consoles of both stores offer an internal testing channel that opens the package first to a small group. If the group likes the release, the package is submitted for review and is published after approval. The review period is not written in this lesson.

The channel determines **who** the same build reaches. It does not determine at what width it is drawn; the screen of the target and the constraints in the tree determine that.

#### The Measured Layer

Every lesson of the course uses the same plain Dart model. The model gives a widget tree the screen width and records who determines each node's width with four names. The file below produces no output; the measured blocks import it.

```dart
// 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}');
}
```

In this lesson the model builds this sizing's rule; the model is not the framework itself. In the model, a release is a tree measured at two screen widths.

#### Two Targets

The release is measured for two targets: a phone of 360 units and a tablet of 768 units. The tree has a large `Hero` image that requests 900 units. The function takes the build mode and looks only at whether it is valid; the mode does not change the tree.

```dart
// release_model.dart — builds the same tree for the release target
import 'sizing.dart';

const modes = ['debug', 'profile', 'release'];
const targetOrder = ['phone', 'tablet'];
const targets = {'phone': 360, 'tablet': 768};

Part buildRelease(String mode) {
  if (!modes.contains(mode)) throw ArgumentError('unknown mode');
  return pad('Screen', 16, column('Body', [
    leaf('Title', 120),
    leaf('Banner', 500),
    leaf('Hero', 900),
    row('Toolbar', [
      leaf('Back', 48),
      flex('Search', 1, fill('Field')),
      leaf('Menu', 48),
    ]),
  ]));
}
```

The first measurement prints the phone with the whole record.

```dart
// main.dart
import 'sizing.dart';
import 'release_model.dart';

void main() {
  printSizes(measure(buildRelease('release'), targets['phone']!));
}
```

```text
forced: /Screen width=360 constraint=360..360
forced: /Screen/Body width=328 constraint=328..328
chosen: /Screen/Body/Title width=120 constraint=0..328
clamped: /Screen/Body/Banner width=328 constraint=0..328
clamped: /Screen/Body/Hero width=328 constraint=0..328
stretched: /Screen/Body/Toolbar width=328 constraint=0..328
chosen: /Screen/Body/Toolbar/Back width=48 constraint=0..inf
forced: /Screen/Body/Toolbar/Search width=232 constraint=232..232
forced: /Screen/Body/Toolbar/Search/Field width=232 constraint=232..232
chosen: /Screen/Body/Toolbar/Menu width=48 constraint=0..inf
```

When the padding is taken from the screen, 328..328 comes down to the body. `Title`, which requests 120, fits the range and becomes **chosen**. `Banner`, which requests 500, and `Hero`, which requests 900, are pulled to 328 and become **clamped**. The toolbar reports no request and spreads, and it becomes **stretched**.

Now the same tree is measured at tablet width.

```dart
// main.dart
import 'sizing.dart';
import 'release_model.dart';

void main() {
  printSizes(measure(buildRelease('release'), targets['tablet']!));
}
```

```text
forced: /Screen width=768 constraint=768..768
forced: /Screen/Body width=736 constraint=736..736
chosen: /Screen/Body/Title width=120 constraint=0..736
chosen: /Screen/Body/Banner width=500 constraint=0..736
clamped: /Screen/Body/Hero width=736 constraint=0..736
stretched: /Screen/Body/Toolbar width=736 constraint=0..736
chosen: /Screen/Body/Toolbar/Back width=48 constraint=0..inf
forced: /Screen/Body/Toolbar/Search width=640 constraint=640..640
forced: /Screen/Body/Toolbar/Search/Field width=640 constraint=640..640
chosen: /Screen/Body/Toolbar/Menu width=48 constraint=0..inf
```

The body becomes 736. This time `Banner` fits the range with 500 and becomes **chosen**. `Hero` again exceeds the range and is pulled to 736. The tree and the rule are the same; only the constraint changed.

#### The Count for Two Targets

The count is printed separately for the two targets.

```dart
// main.dart
import 'sizing.dart';
import 'release_model.dart';

void main() {
  for (final name in targetOrder) {
    print('target=$name');
    countSizings(measure(buildRelease('release'), targets[name]!));
  }
}
```

```text
target=phone
forced: 4
chosen: 3
clamped: 2
stretched: 1
parent: 7
target=tablet
forced: 4
chosen: 4
clamped: 1
stretched: 1
parent: 6
```

On the phone there are 2 **clamped** nodes, on the tablet 1; the parent count falls from 7 to 6. The difference comes from `Banner`. The class of `Hero` does not change on either target.

#### Build Mode

For the three modes, the same tree is measured on two targets; each record is compared with that of debug mode.

```dart
// main.dart
import 'sizing.dart';
import 'release_model.dart';

List<String> lines(List<Sizing> log) =>
    [for (final entry in log) '${entry.sizing} ${entry.path} ${entry.width}'];

void main() {
  for (final name in targetOrder) {
    final base = lines(measure(buildRelease('debug'), targets[name]!));
    for (final mode in modes) {
      final same = lines(measure(buildRelease(mode), targets[name]!));
      print('target=$name mode=$mode record=${same.length} same=${same.join('|') == base.join('|')}');
    }
  }
}
```

```text
target=phone mode=debug record=10 same=true
target=phone mode=profile record=10 same=true
target=phone mode=release record=10 same=true
target=tablet mode=debug record=10 same=true
target=tablet mode=profile record=10 same=true
target=tablet mode=release record=10 same=true
```

In every mode and on every target, the record is the same as that of debug mode.

#### Common Misconception

**Building in release mode changes the width of widgets.** In the model it does not: the inputs of the rule are the constraint and the request, not the mode. What changes the class is the screen width of the target.

#### The Bounding Measurement

The last measurement prints only the `Hero` node on the two targets.

```dart
// main.dart
import 'sizing.dart';
import 'release_model.dart';

void main() {
  for (final name in targetOrder) {
    final log = measure(buildRelease('release'), targets[name]!);
    final hero = log.firstWhere((entry) => entry.path == '/Screen/Body/Hero');
    print('target=$name ${hero.sizing} width=${hero.width} constraint=${hero.limit.bounds()}');
  }
}
```

```text
target=phone clamped width=328 constraint=0..328
target=tablet clamped width=736 constraint=0..736
```

The focus node is `/Screen/Body/Hero`. The request of 900 units exceeds the range `0..328` on the phone and `0..736` on the tablet. On both targets, the width is determined not by the request of the image but by the upper bound of the range. This lesson's sizing: **clamped**.

**QR1.** The model builds this sizing's rule; the model is not the framework itself. In the model, the build mode is only a name. The real build, signing, package formats, and store upload are not in the model; their details are in the documentation of the framework and of the stores.

#### The Closing Table

The table gathers the nineteen lessons of the course in order; the sizings are taken from the summaries of the source lessons.

| Lesson | Measured layout | Sizing | Bounding measurement |
|---|---|---|---|
| Dart Fundamentals | A title, a banner, and a toolbar | **clamped** | `Banner` requests 400 and gets 328 on a 360 screen |
| Asynchronous Dart | The tree built before and after the value arrives | **chosen** | `Avatar` chooses its request when 200 arrives |
| The Widget Concept | A card inside padding | **forced** | `Card` requests 200 and gets 328..328 |
| Stateless and Stateful Widgets | A closed and an open search box | **stretched** | The open `Search` becomes 448 at 480 and 288 at 320 |
| The Three-Tree Model | A centered logo | **chosen** | `Logo` chooses 96 on both screens |
| Layout Widgets | A tight and a loose flexible child | **stretched** | The text of the loose child becomes 280 at 360, 400 at 480, and 240 at 320 |
| Platform Widget Families | An action button in a fixed slot | **forced** | `Button` gets 120 in both families |
| State Management Approaches | A counter badge in a title | **chosen** | `Badge` grows from 30 to 66 and stays **chosen** |
| Navigation and Routing | A navigator at screen width | **forced** | `Orders` requests 200 and gets 360 |
| Networking and Serialization | A banner from the server | **clamped** | `Banner`, which requests 1200, is pulled to 328 |
| Local Storage | A card built from a preference | **stretched** | In the `full` preference, `Card` gets 328 |
| Assets and Fonts | A scaled button | **clamped** | At scale 2, `Button` requests 344 and gets 328 |
| Animations | A panel progressing over five frames | **chosen** | In the last frame, `Panel` chooses 240 |
| Platform Channels | The slot of an embedded platform view | **forced** | `PlatformView` gets 240 regardless of the request in the message |
| Test Types | A test surface of 400 and 320 units | **clamped** | `Chart` requests 300 at 320 and gets 288 |
| Developer Tools | A `Chip` inside `Center` | **stretched** | `Center` spreads to 328, `Chip` chooses 80 |
| Performance Tuning | A counter in a fixed panel | **forced** | `Counter` gets 200, changed nodes 0 |
| Package Management | A rating with two versions | **chosen** | `Rating` chooses 112 in the new version |
| Build and Release | The same release for a phone and a tablet | **clamped** | `Hero` requests 900 and gets 328 and 736 |

The distribution across the lessons is as follows: **forced** 5, **chosen** 5, **clamped** 5, **stretched** 4. The total is nineteen lessons.

This count is not the same thing as the count at the opening of the course. At the opening, the core had printed a single layout with nine records: **forced** 4, **chosen** 3, **clamped** 1, **stretched** 1, and parent 6. The core counts the nine records of a single layout, and the table counts nineteen lessons.

#### Summary

- This lesson's sizing: **clamped**. The focus node is `/Screen/Body/Hero`: the image that requests 900 units is pulled to 328 on the phone and to 736 on the tablet.
- Debug mode is for development, profile mode is for performance analysis, and release mode is for distribution; per platform, the package, the signing, and the release channel carry the release to the user.
- The same release was measured on two targets: 2 **clamped** on the phone, 1 on the tablet. `Banner` was **clamped** on the phone and **chosen** on the tablet, and `Hero` stayed **clamped** on both targets.
- In all three build modes the record came out the same as that of debug mode.
- In the closing table, the distribution of the nineteen lessons is **forced** 5, **chosen** 5, **clamped** 5, **stretched** 4.

#### Next Step

This lesson is the last lesson of the course. Across nineteen lessons we asked a single question: who determines this widget's width. The next topic is the production practice of the cross-platform field.

