Here’s how you would use a method channel in the simple case of invoking a bit of platform code from Dart. The code is associated with the name bar which is not a method name in this case, but could have been. All it does is construct a greeting string and return it to the caller, so we can code this with the reasonable assumption that the platform invocation won’t fail (we’ll look at error handling further below):

// Invocation of platform methods, simple case. // Dart side. const channel = MethodChannel('foo');

final String greeting = await channel.invokeMethod('bar', 'world');

print(greeting);

// Android side. val channel = MethodChannel(flutterView, "foo")

channel.setMethodCallHandler { call, result ->

when (call.method) {

"bar" -> result.success("Hello, ${call.arguments}")

else -> result.notImplemented()

}

} // iOS side. let channel = FlutterMethodChannel(

name: "foo", binaryMessenger: flutterView)

channel.setMethodCallHandler {

(call: FlutterMethodCall, result: FlutterResult) -> Void in

switch (call.method) {

case "bar": result("Hello, \(call.arguments as! String)")

default: result(FlutterMethodNotImplemented)

}

}

By adding cases to the switch constructs, we can easily extend the above to handle multiple methods. The default clause handles the situation where an unknown method is called (most likely due to a programming error).

The Dart code above is equivalent to the following:

const codec = StandardMethodCodec(); final ByteData reply = await BinaryMessages.send(

'foo',

codec.encodeMethodCall(MethodCall('bar', 'world')),

);

if (reply == null)

throw MissingPluginException();

else

print(codec.decodeEnvelope(reply));

The Android and iOS implementations of method channels are similarly thin wrappers around calls to the binary messaging foundations. A null reply is used to represent a “not implemented” result. This conveniently makes the behavior at the receiving end indifferent to whether the invocation fell through to the default clause in the switch, or no method call handler had been registered with the channel at all.

The argument value in the example is the single string world . But the default method codec, aptly named the “standard method codec”, uses the standard message codec under the hood to encode payload values. This means that the “generalized JSON-like” values described earlier are all supported as method arguments and (successful) results. In particular, heterogeneous lists support multiple arguments, while heterogeneous maps support named arguments. The default arguments value is null. A few examples:

await channel.invokeMethod('bar');

await channel.invokeMethod('bar', <dynamic>['world', 42, pi]);

await channel.invokeMethod('bar', <String, dynamic>{

name: 'world',

answer: 42,

math: pi,

}));

The Flutter SDK includes two method codecs:

StandardMethodCodec which by default delegates the encoding of payload values to StandardMessageCodec . Because the latter is extensible, so is the former.

which by default delegates the encoding of payload values to . Because the latter is extensible, so is the former. JSONMethodCodec which delegates the encoding of payload values to JSONMessageCodec .

You can configure method channels with any method codec, including custom ones. To fully understand what is involved in implementing a codec, let’s look at how errors are handled at the method channel API level by extending the example above with a fallible baz method:

// Method calls with error handling. // Dart side. const channel = MethodChannel('foo'); // Invoke a platform method.

const name = 'bar'; // or 'baz', or 'unknown'

const value = 'world';

try {

print(await channel.invokeMethod(name, value));

} on PlatformException catch(e) {

print('$name failed: ${e.message}');

} on MissingPluginException {

print('$name not implemented');

} // Receive method invocations from platform and return results.

channel.setMethodCallHandler((MethodCall call) async {

switch (call.method) {

case 'bar':

return 'Hello, ${call.arguments}';

case 'baz':

throw PlatformException(code: '400', message: 'This is bad');

default:

throw MissingPluginException();

}

});

// Android side. val channel = MethodChannel(flutterView, "foo") // Invoke a Dart method.

val name = "bar" // or "baz", or "unknown"

val value = "world"

channel.invokeMethod(name, value, object: MethodChannel.Result {

override fun success(result: Any?) {

Log.i("MSG", "$result")

}

override fun error(code: String?, msg: String?, details: Any?) {

Log.e("MSG", "$name failed: $msg")

}

override fun notImplemented() {

Log.e("MSG", "$name not implemented")

}

}) // Receive method invocations from Dart and return results.

channel.setMethodCallHandler { call, result ->

when (call.method) {

"bar" -> result.success("Hello, ${call.arguments}")

"baz" -> result.error("400", "This is bad", null)

else -> result.notImplemented()

}

}

// iOS side. let channel = FlutterMethodChannel(

name: "foo", binaryMessenger: flutterView) // Invoke a Dart method.

let name = "bar" // or "baz", or "unknown"

let value = "world"

channel.invokeMethod(name, arguments: value) {

(result: Any?) -> Void in

if let error = result as? FlutterError {

os_log("%@ failed: %@", type: .error, name, error.message!)

} else if FlutterMethodNotImplemented.isEqual(result) {

os_log("%@ not implemented", type: .error, name)

} else {

os_log("%@", type: .info, result as! NSObject)

}

} // Receive method invocations from Dart and return results.

channel.setMethodCallHandler {

(call: FlutterMethodCall, result: FlutterResult) -> Void in

switch (call.method) {

case "bar": result("Hello, \(call.arguments as! String)")

case "baz": result(FlutterError(

code: "400", message: "This is bad", details: nil))

default: result(FlutterMethodNotImplemented)

}

Errors are triples (code, message, details) where the code and message are strings. The message is intended for human consumption, the code for, well, code. The error details is some custom value, often null, which is constrained only by the kinds of value that the codec supports.

The fine print. Exceptions. Any uncaught exception thrown in a Dart or Android method call handler is caught by the channel implementation, logged, and an error result is returned to the caller. Uncaught exceptions thrown in result handlers are logged.

Envelope encoding. How a method codec encodes its envelopes is an implementation detail just like how message codecs convert messages to bytes. As an example, a method codec might use lists: method calls can be encoded as a two-element lists [method name, arguments]; success results as one-element lists [result]; error results as three-element lists [code, message, details]. Such a method codec can then be implemented simply by delegation to an underlying message codec that supports at least lists, strings, and null. The method call arguments, success results, and error details would be arbitrary values supported by that message codec.

API differences. The code examples above highlight that method channels deliver results very differently across Dart, Android, and iOS:

On the Dart side, invocation is handled by a method returning a future. The future completes with the result of the call in success cases, with a PlatformException in error cases, and with a MissingPluginException in the not implemented case.

in error cases, and with a in the not implemented case. On Android, invocation is handled by a method taking a callback argument. The callback interface defines three methods of which one is called, depending on the outcome. Client code implements the callback interface to define what should happen on success, on error, and on not implemented.

On iOS, invocation is similarly handled by a method taking a callback argument. But here, the callback is a single-argument function which is given either a FlutterError instance, the FlutterMethodNotImplemented constant, or, in case of success, the result of the invocation. Client code provides a block with conditional logic to handle the different cases, as needed.

These differences, mirrored also in the way message call handlers are written, arose as concessions to the styles of the programming languages (Dart, Java, and Objective-C) used for the Flutter SDK method channel implementations. Redoing the implementations in Kotlin and Swift might remove some of the differences, but care must be taken to avoid making it harder to use method channels from Java and Objective-C.

Event channels: streaming

An event channel is a specialized platform channel intended for the use case of exposing platform events to Flutter as a Dart stream. The Flutter SDK currently has no support for the symmetrical case of exposing Dart streams to platform code, though that could be built, if the need arises.

Here’s how you would consume a platform event stream on the Dart side:

// Consuming events on the Dart side. const channel = EventChannel('foo'); channel.receiveBroadcastStream().listen((dynamic event) {

print('Received event: $event');

}, onError: (dynamic error) {

print('Received error: ${error.message}');

});

The code below shows how to produce events on the platform side, using sensor events on Android as an example. The main concern is to ensure that we are listening to events from the platform source (the sensor manager in this case) and sending them through the event channel precisely when 1) there is at least one stream listener on the Dart side and 2) the ambient Activity is running. Packaging up the necessary logic in a single class increases the chance of doing this correctly:

// Producing sensor events on Android. // SensorEventListener/EventChannel adapter.

class SensorListener(private val sensorManager: SensorManager) :

EventChannel.StreamHandler, SensorEventListener {

private var eventSink: EventChannel.EventSink? = null



// EventChannel.StreamHandler methods

override fun onListen(

arguments: Any?, eventSink: EventChannel.EventSink?) {

this.eventSink = eventSink

registerIfActive()

}

override fun onCancel(arguments: Any?) {

unregisterIfActive()

eventSink = null

}



// SensorEventListener methods.

override fun onSensorChanged(event: SensorEvent) {

eventSink?.success(event.values)

}

override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {

if (accuracy == SensorManager.SENSOR_STATUS_ACCURACY_LOW)

eventSink?.error("SENSOR", "Low accuracy detected", null)

} // Lifecycle methods.

fun registerIfActive() {

if (eventSink == null) return

sensorManager.registerListener(

this,

sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE),

SensorManager.SENSOR_DELAY_NORMAL)

}

fun unregisterIfActive() {

if (eventSink == null) return

sensorManager.unregisterListener(this)

}

} // Use of the above class in an Activity.

class MainActivity: FlutterActivity() {

var sensorListener: SensorListener? = null



override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

GeneratedPluginRegistrant.registerWith(this)

sensorListener = SensorListener(

getSystemService(Context.SENSOR_SERVICE) as SensorManager)

val channel = EventChannel(flutterView, "foo")

channel.setStreamHandler(sensorListener)

}



override fun onPause() {

sensorListener?.unregisterIfActive()

super.onPause()

}



override fun onResume() {

sensorListener?.registerIfActive()

super.onResume()

}

}

If you use the android.arch.lifecycle package in your app, you could make SensorListener more self-contained by making it a LifecycleObserver .

The fine print. Life of a stream handler. The platform side stream handler has two methods, onListen and onCancel , which are invoked whenever the number of listeners to the Dart stream goes from zero to one and back, respectively. This can happen multiple times. The stream handler implementation is supposed to start pouring events into the event sink when the former is called, and stop when the latter is called. In addition, it should pause when the ambient app component is not running. The code above provides a typical example. Under the covers, a stream handler is of course just a binary message handler, registered with the Flutter view using the event channel’s name.

Codec. An event channel is configured with a method codec, allowing us to distinguish between success and error events in the same way that method channels are able to distinguish between success and error results.

Stream handler arguments and errors. The onListen and onCancel stream handler methods are invoked via method channel invocations. So we have control method calls from Dart to the platform and event messages in the reverse direction, all on the same logical channel. This setup allows arguments to be relayed to both control methods and any errors to be reported back. On the Dart side, the arguments, if any, are given in the call to receiveBroadcastStream . This means they are specified only once, regardless of the number of invocations of onListen and onCancel happening during the lifetime of the stream. Any errors reported back are logged.

End of stream. An event sink has an endOfStream method that can be invoked to signal that no additional success or error events will be sent. The null binary message is used for this purpose. On receipt on the Dart side, the stream is closed.

Life of a stream. The Dart stream is backed by a stream controller fed from the incoming platform channel messages. A binary message handler is registered using the event channel’s name to receive incoming messages only while the stream has listeners.

Usage guidelines

Prefix channel names by domain for uniqueness

Channel names are just strings, but they have to be unique across all channel objects used for different purposes in your app. You can accomplish that using any suitable naming scheme. However, the recommended approach for channels used in plugins is to employ a domain name and plugin name prefix such as some.body.example.com/sensors/foo for the foo channel used by the sensors plugin developed by some.body at example.com . Doing so allows plugin consumers to combine any number of plugins in their apps without risk of channel name collisions.

Consider treating platform channels as intra-module communication

Code for invoking remote procedure calls in distributed systems look superficially similar to code using method channels: you invoke a method given by a string and serialize your arguments and results. Since distributed system components are often developed and deployed independently, robust request and reply checking is critical, and usually done in check-and-log style on both sides of the network.

Platform channels on the other hand glue together three pieces of code that are developed and deployed together, in a single component.

Java/Kotlin ↔ Dart ↔ Objective-C/Swift

In fact, it very often makes sense to package up a triad like this in a single code module, such as a Flutter plugin. This means that the need for arguments and results checking across method channel invocations should be comparable to the need for such checks across normal method calls within the same module.

Inside modules, our main concern is to guard against programming errors that are beyond the compiler’s static checks and go undetected at runtime until they blow things up non-locally in time or space. A reasonable coding style is to make assumptions explicit using types or assertions, allowing us to fail fast and cleanly, e.g. with an exception. Details vary by programming language of course. Examples:

If a value received over a platform channel is expected to have a certain type, immediately assign it to a variable of that type.

If a value received over a platform channel is expected to be non-null, either set things up to have it dereferenced immediately, or assert that it is non-null before storing it for later. Depending on your programming language, you may be able to assign it to a variable of a non-nullable type instead.

Two simple examples:

// Dart: we expect to receive a non-null List of integers.

for (final int n in await channel.invokeMethod('getFib', 100)) {

print(n * n);

} // Android: we expect non-null name and age arguments for

// asynchronous processing, delivered in a string-keyed map.

channel.setMethodCallHandler { call, result ->

when (call.method) {

"bar" -> {

val name : String = call.argument("name")

val age : Int = call.argument("age")

process(name, age, result)

}

else -> result.notImplemented()

}

}

:

fun process(name: String, age: Int, result: Result) { ... }

The Android code exploits the generically typed <T> T argument(String key) method of MethodCall which looks up the key in the arguments, assumed to be a map, and casts the value found to the target (call site) type. A suitable exception is thrown, if this fails for any reason. Being thrown from a method call handler, it would be logged, and an error result sent to the Dart side.

Don’t mock platform channels

(Pun intended.) When writing unit tests for Dart code that uses platform channels, a knee jerk reaction may be to mock the channel object, as you would a network connection.

You can certainly do that, but channel objects don’t actually need to be mocked to play nicely with unit tests. Instead, you can register mock message or method handlers to play the role of the platform during a particular test. Here is a unit test of a function hello that is supposed to invoke the bar method on channel foo :

test('gets greeting from platform', () async {

const channel = MethodChannel('foo');

channel.setMockMethodCallHandler((MethodCall call) async {

if (call.method == 'bar')

return 'Hello, ${call.arguments}';

throw MissingPluginException();

});

expect(await hello('world'), 'Platform says: Hello, world');

});

To test code that sets up message or method handlers, you can synthesize incoming messages using BinaryMessages.handlePlatformMessage . At present, this method is not mirrored on platform channels, though that could easily be done as indicated in the code below. The code defines a unit test of a class Hello that is supposed to collect incoming arguments of calls to method bar on channel foo , while returning greetings:

test('collects incoming arguments', () async {

const channel = MethodChannel('foo');

final hello = Hello();

final String result = await handleMockCall(

channel,

MethodCall('bar', 'world'),

);

expect(result, contains('Hello, world'));

expect(hello.collectedArguments, contains('world'));

}); // Could be made an instance method on class MethodChannel.

Future<dynamic> handleMockCall(

MethodChannel channel,

MethodCall call,

) async {

dynamic result;

await BinaryMessages.handlePlatformMessage(

channel.name,

channel.codec.encodeMethodCall(call),

(ByteData reply) {

if (reply == null)

throw MissingPluginException();

result = channel.codec.decodeEnvelope(reply);

},

);

return result;

}

Both examples above declare the channel object in the unit test. This works fine — unless you worry about the duplicated channel name and codec — because all channel objects with the same name and codec are equivalent. You can avoid the duplication by declaring the channel as a const somewhere visible to both your production code and the test.

What you don’t need is to provide a way to inject a mock channel into your production code.

Consider automated testing for your platform interaction

Platform channels are simple enough, but getting everything working from your Flutter UI via a custom Dart API backed by a separate Java/Kotlin and Objective-C/Swift implementation does takes some care. And keeping the setup working as changes are made to your app will, in practice, require automated testing to guard against regressions. This cannot be accomplished with unit testing alone because you need a real app running for platform channels to actually talk to the platform.

Flutter comes with the flutter_driver integration test framework that allows you to test Flutter applications running on real devices and emulators. But flutter_driver is not currently integrated with other frameworks to enable testing across Flutter and platform components. I am confident this is one area where Flutter will improve in the future.

In some situations, you can use flutter_driver as is to test your platform channel usage. This requires that your Flutter user interface can be used to trigger any platform interaction and that it is then updated with sufficient detail to allow your test to ascertain the outcome of the interaction.

If you are not in that situation, or if you are packaging up your platform channel usage as a Flutter plugin for which you want a module test, you can instead write a simple Flutter app for testing purposes. That app should have the characteristics above and can then be exercised using flutter_driver . You’ll find an example in the Flutter GitHub repo.

Keep platform side ready for incoming synchronous calls

Platform channels are asynchronous only. But there are quite a few platform APIs out there that make synchronous calls into your host app components, asking for information or help or offering a window of opportunity. One example is Activity.onSaveInstanceState on Android. Being synchronous means everything must be done before the incoming call returns. Now, you might like to include information from the Dart side in such processing, but it is too late to start sending out asynchronous messages once the synchronous call is already active on the main UI thread.

The approach used by Flutter, most notably for semantics/accessibility information, is to proactively send updated (or updates to) information to the platform side whenever the information changes on the Dart side. Then, when the synchronous call arrives, the information from the Dart side is already present and available to platform side code.

Resources

Flutter API documentation:

DartDoc for the services library which contains the Dart platform channel types.

JavaDoc for the io.flutter.plugins.common package which contains the Android platform channel types.

ObjcDoc for the iOS Flutter library.

Guides:

The flutter.io website documents how to use method channels and the Dart/Android/iOS value conversions involved in using the standard method codec.

The Boring Flutter Development Show, Episode 6: Packages and plugins is a YouTube video showing a Flutter plugin being implemented, live, using platform channels.

Code examples: