Lesson 14 / 19
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.
Published Updated
In this lesson
- Defines the platform channel and the message that arrives from the channel
- Measures that the slot forces the width of the embedded platform view
- Shows that the record stays forced when the request in the message changes
- Confirms that repeated messages do not change the sizing tally
Contents
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.
// 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.
// Platform code: Dart end
const channel = MethodChannel('app/embed');
Future<int?> askWidth() {
return channel.invokeMethod<int>('preferredWidth');
}
// 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.
// 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.
// 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);
}
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.
// 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()}');
}
}
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.
// 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()}');
}
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 the240..240constraint 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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.