Codecs & Data Serialization Across the Channel
Codecs & Data Serialization Across the Channel
Every message exchanged between Flutter and native code must be serialized into bytes before it crosses the platform boundary, and then deserialized back on the other side. The component responsible for this transformation is called a codec. Flutter ships four built-in codecs, each optimized for a different use case. Choosing the right one directly impacts both performance and type fidelity.
The Four Built-in Codecs
Flutter's services library provides these codecs out of the box:
- StandardMessageCodec — the default codec, supports rich Dart types (int, double, bool, String, Uint8List, List, Map) with an efficient binary encoding
- JSONMessageCodec — encodes messages as UTF-8 JSON strings; good for interoperability, but loses type precision (all numbers become doubles)
- BinaryCodec — passes raw
ByteDataunchanged; zero encoding overhead, but you own all serialization logic - StringCodec — encodes a single Dart
Stringas UTF-8; useful for simple text-only protocols
MethodChannel, BasicMessageChannel, or EventChannel. Only BasicMessageChannel lets you swap in a non-default codec; MethodChannel and EventChannel are fixed to StandardMessageCodec.StandardMessageCodec in Depth
StandardMessageCodec is the workhorse of platform channels. It encodes each value with a one-byte type tag followed by the payload. The type mapping between Dart and Java/Kotlin/Swift/ObjC is well-defined:
null→ nullbool→ Booleanint(32-bit) → Integer; (64-bit) → Longdouble→ DoubleString→ String (UTF-8)Uint8List/Int32List/Int64List/Float64List→ typed byte arraysList→ ArrayList (Java) / Array (Swift)Map→ HashMap (Java) / Dictionary (Swift)
BasicMessageChannel with StandardMessageCodec (default)
import 'package:flutter/services.dart';
// StandardMessageCodec is the default — you can omit it
const _channel = BasicMessageChannel<dynamic>(
'com.example.app/settings',
StandardMessageCodec(),
);
Future<void> sendPreferences() async {
// All these Dart types survive the round-trip intact
final reply = await _channel.send({
'userId': 42, // int → Integer on Android
'score': 3.14, // double → Double
'enabled': true, // bool → Boolean
'tags': ['a', 'b'], // List → ArrayList
'meta': <String, dynamic>{'key': 'value'}, // Map → HashMap
});
print('Native replied: $reply');
}
JSONMessageCodec
JSONMessageCodec serializes your message to a JSON string (via jsonEncode) before handing it to the native layer, and deserializes it on the way back. This makes it trivially interoperable with any platform that can parse JSON, but there is a significant trade-off: all numeric types collapse to double on deserialization because JSON has a single number type. Integers will arrive as doubles unless you cast them explicitly.
BasicMessageChannel with JSONMessageCodec
import 'package:flutter/services.dart';
const _jsonChannel = BasicMessageChannel<dynamic>(
'com.example.app/json',
JSONMessageCodec(),
);
Future<void> fetchConfig() async {
// Send: Dart Map serialized to JSON string automatically
final result = await _jsonChannel.send({'action': 'getConfig'});
// result is decoded from JSON — numeric values are doubles
if (result is Map) {
// CAUTION: 'version' was an int on native, arrives as double here
final version = (result['version'] as double).toInt();
print('Config version: $version');
}
}
BinaryCodec
BinaryCodec performs no encoding — it passes a ByteData object directly to the native side as raw bytes. This is the highest-performance option: there is zero overhead from type-tagging or JSON stringification. Use it when you are implementing your own binary protocol (e.g., Protocol Buffers, MessagePack, FlatBuffers) or streaming large binary payloads such as camera frames or audio buffers.
BinaryCodec for raw byte transfer
import 'dart:typed_data';
import 'package:flutter/services.dart';
const _binaryChannel = BasicMessageChannel<ByteData?>(
'com.example.app/binary',
BinaryCodec(),
);
Future<void> sendRawFrame(Uint8List pixels) async {
// Wrap Uint8List in a ByteData view — zero copy
final byteData = pixels.buffer.asByteData();
final response = await _binaryChannel.send(byteData);
if (response != null) {
final ack = response.getUint8(0);
print('Native ACK: $ack');
}
}
StringCodec
StringCodec encodes a single String as UTF-8 bytes. It is the simplest codec and a good choice for text-only channels — for example, sending log messages to the native side or receiving locale strings from the OS. It has negligible overhead but only handles String; passing any other type throws an assertion error in debug mode.
StringCodec when the channel carries simple commands or labels, and combine it with a lightweight parsing convention (e.g., pipe-separated values) rather than paying the full JSON encoding cost for a small payload.Choosing the Right Codec
Use the following decision guide when designing a new channel:
- Need rich types (int, List, Map, typed arrays) with accurate round-trips? →
StandardMessageCodec(default choice) - Integrating with a third-party native library that speaks JSON? →
JSONMessageCodec(watch out for int→double coercion) - Streaming raw bytes or using a custom binary protocol? →
BinaryCodec - Channel carries only a plain string? →
StringCodec
JSONMessageCodec when your protocol relies on integer precision (e.g., 64-bit IDs, Unix timestamps in milliseconds). JSON does not distinguish integers from floating-point numbers, and you will silently lose precision for large integers beyond 2^53.Custom Codecs
You can implement a custom codec by extending MessageCodec<T> and overriding encodeMessage and decodeMessage. This is the right path for Protocol Buffers or any schema-driven serialization format. The native counterpart must implement the corresponding codec on the Android (MessageCodec<T>) or iOS (FlutterMessageCodec) side.
Summary
Flutter's four built-in codecs cover the full spectrum from richly-typed binary encoding (StandardMessageCodec) to raw byte passthrough (BinaryCodec). StandardMessageCodec is the right default for most channels because it preserves Dart's native types across the boundary. Switch to JSONMessageCodec only when JSON interoperability is a hard requirement, and always account for the int-to-double coercion it introduces. Reach for BinaryCodec when performance is critical and you own the serialization format end-to-end.