---
title: 'Platform Widget Families'
source: 'https://academia.sh/en/courses/flutter/platform-widget-families'
course: Flutter
language: en
updated: '2026-10-21T05:02:52+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Explain the widget sets that belong to the design languages of two platforms and the adaptive widget'
    - 'Show how choosing a family by platform is done in code'
    - 'Measure that a button whose families report different requests gets the same width in a fixed slot'
---

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