BasicMessageChannel: Bidirectional Arbitrary Messaging
BasicMessageChannel: Bidirectional Arbitrary Messaging
BasicMessageChannel is the most flexible of Flutter's three platform channel types. Unlike MethodChannel (which follows a call-and-response RPC model) and EventChannel (which streams events from native to Dart), BasicMessageChannel allows either side — Dart or native — to initiate a message at any time, and the recipient may optionally send a reply. This makes it ideal for free-form, low-latency, bidirectional communication where neither side is a strict "caller" or "listener".
MethodChannel, Dart calls native and waits for a result. With BasicMessageChannel, both Dart and native can send messages to each other independently, and each message may carry an arbitrary reply.When to Choose BasicMessageChannel
Use BasicMessageChannel when:
- Native code needs to push data to Dart without Dart first invoking a method call
- Dart needs to send arbitrary structured data to native and receive a structured reply
- You want to exchange non-standard types (binary blobs, custom objects) using a custom MessageCodec
- You need a lightweight two-way pipe without the overhead of a full event stream
MessageCodec Options
Every BasicMessageChannel is parameterised with a MessageCodec<T> that serialises messages to bytes and deserialises them on the other side. Flutter ships four built-in codecs:
- StandardMessageCodec — the default; handles
null,bool,int,double,String,Uint8List,Int32List,Int64List,Float64List,List, andMap - JSONMessageCodec — encodes to/from JSON; convenient when native already speaks JSON
- BinaryCodec — passes raw
ByteDatawith zero encoding overhead; fastest for binary payloads - StringCodec — UTF-8 plain strings; simplest for text-only protocols
You can also implement MessageCodec<T> directly to support custom wire formats (e.g., protobuf or MessagePack).
Dart-Side: Sending and Receiving
Declare the channel once, then use send() to push a message (and await the optional reply) and setMessageHandler() to receive messages initiated by the native side.
Example 1 — Dart sends a message and receives a reply
import 'dart:async';
import 'package:flutter/services.dart';
// Declare a typed channel using the standard codec
const _channel = BasicMessageChannel<Object?>(
'com.example.app/settings',
StandardMessageCodec(),
);
class NativeSettingsService {
/// Called at startup: ask native for the current settings map.
Future<Map<Object?, Object?>?> fetchSettings() async {
final reply = await _channel.send({'action': 'getSettings'});
if (reply is Map) {
return reply as Map<Object?, Object?>;
}
return null;
}
/// Register a handler so native can PUSH updated settings to Dart.
void listenForUpdates(void Function(Map<Object?, Object?>) onUpdate) {
_channel.setMessageHandler((message) async {
if (message is Map) {
onUpdate(message as Map<Object?, Object?>);
}
// Return null or an acknowledgement string
return 'ack';
});
}
}
send() returns a Future that resolves to the native reply, or null if the native handler does not reply. Always handle the null case to avoid unexpected LateInitializationError bugs.Native-Side: Android (Kotlin)
On Android, mirror the channel declaration in MainActivity (or any FlutterEngine holder). Use setMessageHandler to receive from Dart, and call send on the channel instance to push unsolicited messages to Dart.
Example 2 — Android Kotlin handler + unsolicited push
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.StandardMessageCodec
class MainActivity : FlutterActivity() {
private lateinit var settingsChannel: BasicMessageChannel<Any?>
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
settingsChannel = BasicMessageChannel(
flutterEngine.dartExecutor.binaryMessenger,
"com.example.app/settings",
StandardMessageCodec.INSTANCE
)
// Handle messages FROM Dart
settingsChannel.setMessageHandler { message, reply ->
if (message is Map<*, *> && message["action"] == "getSettings") {
val settings = mapOf(
"theme" to "dark",
"fontSize" to 16,
"language" to "en"
)
reply.reply(settings) // send reply back to Dart
} else {
reply.reply(null)
}
}
}
// Call this from anywhere in your Android code to PUSH to Dart
fun pushSettingsUpdate(newSettings: Map<String, Any>) {
settingsChannel.send(newSettings) { reply ->
// optional: handle Dart's acknowledgement
println("Dart ack: $reply")
}
}
}
Native-Side: iOS (Swift)
The Swift API is symmetric. Register a FlutterBasicMessageChannel in AppDelegate and call sendMessage to initiate messages from iOS to Dart.
Example 3 — iOS Swift handler
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
var settingsChannel: FlutterBasicMessageChannel?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let messenger = controller.binaryMessenger
settingsChannel = FlutterBasicMessageChannel(
name: "com.example.app/settings",
binaryMessenger: messenger,
codec: FlutterStandardMessageCodec.sharedInstance()
)
// Receive messages from Dart
settingsChannel?.setMessageHandler { message, reply in
if let dict = message as? [String: Any],
dict["action"] as? String == "getSettings" {
reply(["theme": "dark", "fontSize": 16, "language": "en"])
} else {
reply(nil)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// Push unsolicited update from iOS to Dart
func pushSettingsUpdate(_ settings: [String: Any]) {
settingsChannel?.sendMessage(settings) { reply in
print("Dart ack: \(reply ?? "none")")
}
}
}
BasicMessageChannel calls must be made on the platform's main thread on the native side, and the channel must be set up after the Flutter engine is fully initialised. Calling send before the engine is ready silently drops the message.Comparing the Three Channel Types
Understanding when to use each channel type prevents over-engineering:
- MethodChannel — Dart calls a named method on native; native returns a result or throws. Best for discrete operations (get battery level, open camera).
- EventChannel — Native streams continuous events to Dart (sensor data, connectivity changes). Dart cannot send messages back through it.
- BasicMessageChannel — Full bidirectional messaging. Either side can initiate. Supports custom codecs. Best for peer-to-peer data exchange or custom protocols.
Summary
BasicMessageChannel fills the gap between the rigid RPC model of MethodChannel and the one-way stream of EventChannel. By choosing the right MessageCodec — standard, JSON, binary, or custom — you can efficiently exchange any structured data between Dart and native code, with either side able to initiate communication at any time.
BasicMessageChannel with StandardMessageCodec is your go-to tool. Register the handler early, keep channel names namespaced (e.g., com.yourapp/channelname), and always handle null replies gracefully.