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?
answer
- JSON-like values plus typed data
- int splits at 32 bits
- maps decode as Map<Object?, Object?>
- FlutterStandardTypedData on iOS
- writeValue / readValueOfType to extend
basics
~20 sStandardMessageCodec 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 linesimport '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
Recall the supported value kinds: null, bool, numbers, strings, typed byte and number lists, and lists and maps of those.
Explain the 32/64-bit int split, why decoded maps are Map<Object?, Object?>, and when invokeListMethod or invokeMapMethod is needed.
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.
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