---
title: 'Build and Release'
source: 'https://academia.sh/en/courses/flutter/build-and-release'
course: Flutter
language: en
updated: '2026-10-21T05:02:53+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Distinguishes the purpose of the debug, profile, and release build modes'
    - 'Lists the release output, the signing, and the release channel for Android and iOS'
    - 'Measures the same release at phone width and tablet width and compares the sizings'
    - 'Reads the sizing of the nineteen lessons of the course from the closing table'
---

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