skip to content

In Flutter's StandardMessageCodec, which Dart values can cross a platform channel, and how do ints, maps and typed lists arrive on Android and iOS?

level: middleimportance: should knowfreq 45%

answer

  1. JSON-like values plus typed data
  2. int splits at 32 bits
  3. maps decode as Map<Object?, Object?>
  4. FlutterStandardTypedData on iOS
  5. writeValue / readValueOfType to extend

basics

~20 s

StandardMessageCodec carries null, bool, int, double, String, Uint8List, Int32List, Int64List, Float32List, Float64List, and Lists and Maps of those; anything else throws. Ints become 32- or 64-bit host numbers by size, and maps come back untyped.

solid answer

~40 s

`StandardMessageCodec` is a binary, JSON-like format: `null`, `bool`, `int`, `double`, `String`, the typed lists `Uint8List`, `Int32List`, `Int64List`, `Float32List`, `Float64List`, and heterogeneous `List`s and `Map`s of supported values. A Dart `int` that fits in 32 bits arrives as `Integer`/`Int` on Android and `NSNumber` built from a 32-bit int on iOS, and otherwise as `Long` or a 64-bit `NSNumber`, so host code must not blindly cast to one width. Typed lists map to primitive arrays on Android and `FlutterStandardTypedData` on iOS. Decoded collections are always `List<Object?>` and `Map<Object?, Object?>`, so Dart code casts them (`invokeMapMethod`, `Map.cast`). A custom class, `DateTime` or an enum throws `ArgumentError` on encode; send a map or primitive, or subclass the codec and override `writeValue` and `readValueOfType`.

code

dart · 10 lines
dart
import 'package:flutter/services.dart';

const _steps = MethodChannel('com.example.app/steps');

Future<Map<String, int>> dailyTotals() async {
  // invokeMethod<Map<String, int>> would throw a TypeError.
  final Map<String, int>? totals =
      await _steps.invokeMapMethod<String, int>('dailyTotals');
  return totals ?? const <String, int>{};
}

go deeper

for a junior

Recall the supported value kinds: null, bool, numbers, strings, typed byte and number lists, and lists and maps of those.

for a middle

Explain the 32/64-bit int split, why decoded maps are Map<Object?, Object?>, and when invokeListMethod or invokeMapMethod is needed.

for a senior

Show how you would carry custom types, either by converting to maps or by subclassing the codec on both sides, and how you would diagnose a width or codec mismatch.

for a principal

Weigh a hand-maintained codec subclass against generated bindings for a plugin with many data types, considering drift between Dart and host implementations.

## What a codec is A **codec** turns Dart values into bytes and back. Every platform channel has one; `MethodChannel` and `EventChannel` default to `StandardMethodCodec`, which delegates the values themselves to **`StandardMessageCodec`**. Its wire format is fixed: one **type byte** followed by the value, host-endian numbers, and a compact size prefix. The format must stay identical on the Dart, Android, iOS and desktop sides, which is why the supported set is small. ## The supported set `writeValue` in the framework source checks, in order: `null`, `bool`, `double`, `int`, `String`, `Uint8List`, `Int32List`, `Int64List`, `Float32List`, `Float64List`, `List`, `Map`. Anything else hits `throw ArgumentError.value(value)`. | Dart | Android (Kotlin / Java) | iOS (Swift) | |---|---|---| | `null` | `null` | `nil` (`NSNull` inside collections) | | `bool` | `Boolean` | `NSNumber(value: Bool)` | | `int` that fits 32 bits | `Int` / `java.lang.Integer` | `NSNumber(value: Int32)` | | `int` beyond 32 bits | `Long` | `NSNumber(value: Int)` | | `double` | `Double` | `NSNumber(value: Double)` | | `String` | `String` | `String` | | `Uint8List` / `Int32List` / `Int64List` / `Float32List` / `Float64List` | `ByteArray` / `IntArray` / `LongArray` / `FloatArray` / `DoubleArray` | `FlutterStandardTypedData` | | `List` | `List` (`java.util.ArrayList`) | `NSArray` | | `Map` | `HashMap` | `NSDictionary` | ## The traps interviewers probe 1. **The int width split.** The encoder writes an `int` as 4 bytes when it lies between -2^31 and 2^31-1, otherwise as 8 bytes. The same Dart field can therefore arrive as `Integer` for a small step count and as `Long` for a millisecond timestamp. Host code that does `call.argument<Int>("since")` crashes with a class-cast error once the value grows; read it as a `Number` and convert. 2. **Untyped collections on the way back.** Decoded lists and maps are always `List<Object?>` and `Map<Object?, Object?>`. Because Dart generics are reified, `invokeMethod<Map<String, int>>` throws a `TypeError`; the source says `T` cannot be a class with generics other than `dynamic`. `invokeListMethod<T>` and `invokeMapMethod<K, V>` exist precisely to call `cast` for you. 3. **No custom objects.** A `DateTime`, an enum value or a model class is not in the list. Convert first: `millisecondsSinceEpoch`, `enum.name`, or a `toMap()`. 4. **Doubles before ints.** The encoder tests `double` before `int` because on the web every number satisfies both checks; decoding uses the type byte, so it is unaffected. 5. **Big integers only inbound.** A Java `BigInteger` arrives in Dart as a hexadecimal `String`; the codec cannot send big integers from Dart. ## Extending the codec `StandardMessageCodec` is designed for subclassing: override `writeValue` to emit a new type byte for your class and `readValueOfType` to read it back, and mirror the same subclass on the host. This is what generated channel code does for data classes, and what a plugin can do by hand for one or two custom types. ```dart class StepSampleCodec extends StandardMessageCodec { const StepSampleCodec(); static const int _kStepSample = 128; @override void writeValue(WriteBuffer buffer, Object? value) { if (value is StepSample) { buffer.putUint8(_kStepSample); writeValue(buffer, <Object?>[value.count, value.at.millisecondsSinceEpoch]); } else { super.writeValue(buffer, value); } } @override Object? readValueOfType(int type, ReadBuffer buffer) { if (type == _kStepSample) { final List<Object?> f = readValue(buffer)! as List<Object?>; return StepSample(f[0]! as int, DateTime.fromMillisecondsSinceEpoch(f[1]! as int)); } return super.readValueOfType(type, buffer); } } ``` ## Other codecs - `JSONMessageCodec` / `JSONMethodCodec` send UTF-8 JSON text: slower, but readable and handy for a host library that already speaks JSON. - `StringCodec` sends UTF-8 strings; `BinaryCodec` passes a `ByteData` through untouched, the cheapest option for large blobs. Whatever codec you choose, both sides must use the same one; a mismatch shows up as a `FormatException` such as 'Message corrupted' or garbage values, not a compile error.

  • Why might a Kotlin handler crash only for some users when reading an int argument?
    `StandardMessageCodec` sends a Dart `int` as 32 bits when it fits and 64 bits otherwise, so the host sees `Integer` for small values and `Long` for large ones. Code that casts to `Int` works in testing with small numbers and throws once a value, such as a timestamp or a large counter, exceeds the 32-bit range.
  • How do you send a DateTime or an enum through a MethodChannel?
    Convert it to a supported value first: `millisecondsSinceEpoch` or an ISO-8601 string for a `DateTime`, `name` or `index` for an enum, and convert back on the other side. The alternative is a `StandardMessageCodec` subclass that adds a type byte, mirrored on the host.

saying these in an interview costs you the question

  • Says any Dart object can be sent because the codec uses reflection
  • Casts invokeMethod results straight to Map<String, dynamic>
  • Assumes a Dart int always arrives as a 64-bit Long
  • Thinks DateTime is a supported codec type
  • Believes a codec mismatch is caught at compile time