Skip to content
academia.sh

Lesson 18 / 19

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.

Published Updated

In this lesson

  • Distinguishes the package repository, the dependency, the version constraint, and the lock file
  • States that a version constraint and a layout constraint are separate concepts
  • Compares, from the measured record, the default width that two versions of a package give the same widget
  • Explains that a version upgrade changes only the request of the widget, not the layout rule
Contents

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.

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

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

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

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

void main() {
  final log = measure(buildRating(1), 360);
  printSizes(log);
}
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.

// 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());
  }
}
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.

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

// 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());
  }
}
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.

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

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close