---
title: 'Dart Fundamentals'
source: 'https://academia.sh/en/courses/flutter/dart-fundamentals'
course: Flutter
language: en
updated: '2026-10-21T05:02:52+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Read Dart's types, null safety, and class structures on a layout core"
    - 'Explain the layout contract; constraints go down, sizes go up, the parent sets the position'
    - "Classify a widget's width as forced, chosen, clamped, or stretched"
    - 'Measure that the same tree gets different sizings at different screen widths'
---

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