In the last article, we looked at how to use Platform Channel to communicate with platforms in official documentation. In this article, we will discuss the principle of Platform Channel and Platform communication.

Platform Channel profile

There are three common Platform channels used to transfer data between Flutter platforms:

  • BasicMessageChannel: Supports string and semi-structured data transfer. Two-way transmission.
  • MethodChannel: Supports method calls, both from the Flutter developer and from platform code to the Flutter. Two-way transmission.
  • EventChannel: supports data flow.

These three Platform channels have different functions. But in design, they all have the following three member variables:

  • Name: Represents the name of a Channel. Each Channel uses a unique name as its unique identifier
  • Codec: Represents a codec for a message, currently available as MethodCodec and MessageCodec
  • A messager is a tool for sending and receiving messages

Platform structure of the Channel

To get a feel for the three Platform channels, familiarize yourself with their structure with the following simplified code.

The source code of the three Platform Channel modes is as follows. Let’s take a look.

BasicMessageChannel

class BasicMessageChannel<T> { /// Creates a [BasicMessageChannel] with the specified [name], [codec] and [binaryMessenger]. /// /// The [name] and [codec] arguments cannot be null. The default [ServicesBinding.defaultBinaryMessenger] /// instance is used if [binaryMessenger] is null. const BasicMessageChannel(this.name, this.codec, { BinaryMessenger? binaryMessenger }) : assert(name ! = null), assert(codec ! = null), _binaryMessenger = binaryMessenger; /// The logical channel on which communication happens, not null. final String name; /// The message codec used by this channel, not null. final MessageCodec<T> codec; /// The messenger which sends the bytes for this channel, not null. BinaryMessenger get binaryMessenger => _binaryMessenger ?? ServicesBinding.instance! .defaultBinaryMessenger; final BinaryMessenger? _binaryMessenger; /// Codec encodes the message first. BinaryMessenger sends the encoded message to iOS and Android platforms and waits for the reply message from iOS and Android platforms. Future<T? > send(T message) async { return codec.decodeMessage(await binaryMessenger.send(name, codec.encodeMessage(message))); } void setMessageHandler(Future<T> Function(T? message)? handler) { if (handler == null) { binaryMessenger.setMessageHandler(name, null); } else { binaryMessenger.setMessageHandler(name, (ByteData? message) async { return codec.encodeMessage(await handler(codec.decodeMessage(message))); }); } } // Looking for setMockMessageHandler? // See this shim package: packages/flutter_test/lib/src/deprecated.dart }Copy the code

MethodChannel

class MethodChannel { /// Creates a [MethodChannel] with the specified [name]. /// /// The [codec] used will be [StandardMethodCodec], unless otherwise /// specified. /// /// The [name] and [codec] arguments cannot be null. The default [ServicesBinding.defaultBinaryMessenger] /// instance is used if [binaryMessenger] is null. const MethodChannel(this.name, [this.codec = const StandardMethodCodec(), BinaryMessenger? binaryMessenger ]) : assert(name ! = null), assert(codec ! = null), _binaryMessenger = binaryMessenger; /// The logical channel on which communication happens, not null. final String name; /// The message codec used by this channel, not null. final MethodCodec codec; BinaryMessenger get binaryMessenger => _binaryMessenger ?? ServicesBinding.instance! .defaultBinaryMessenger; final BinaryMessenger? _binaryMessenger; @optionalTypeArgs Future<T? > _invokeMethod<T>(String method, { required bool missingOk, dynamic arguments }) async { assert(method ! = null); final ByteData? result = await binaryMessenger.send( name, codec.encodeMethodCall(MethodCall(method, arguments)), ); if (result == null) { if (missingOk) { return null; } throw MissingPluginException('No implementation found for method $method on channel $name'); } return codec.decodeEnvelope(result) as T?; } d call arguments on Android. @optionalTypeArgs Future<T? > invokeMethod<T>(String method, [ dynamic arguments ]) { return _invokeMethod<T>(method, missingOk: false, arguments: arguments); } Future<List<T>? > invokeListMethod<T>(String method, [ dynamic arguments ]) async { final List<dynamic>? result = await invokeMethod<List<dynamic>>(method, arguments); return result? .cast<T>(); } Future<Map<K, V>? > invokeMapMethod<K, V>(String method, [ dynamic arguments ]) async{ final Map<dynamic, dynamic>? result = await invokeMethod<Map<dynamic, dynamic>>(method, arguments); return result? .cast<K, V>(); } void setMethodCallHandler(Future<dynamic> Function(MethodCall call)? handler) { binaryMessenger.setMessageHandler( name, handler == null ? null : (ByteData? message) => _handleAsMethodCall(message, handler), ); } Future<ByteData? > _handleAsMethodCall(ByteData? message, Future<dynamic> Function(MethodCall call) handler) async { final MethodCall call = codec.decodeMethodCall(message); try { return codec.encodeSuccessEnvelope(await handler(call)); } on PlatformException catch (e) { return codec.encodeErrorEnvelope( code: e.code, message: e.message, details: e.details, ); } on MissingPluginException { return null; } catch (e) { return codec.encodeErrorEnvelope(code: 'error', message: e.toString(), details: null); } } // Looking for setMockMethodCallHandler or checkMethodCallHandler? // See this shim package: packages/flutter_test/lib/src/deprecated.dart }Copy the code

EventChannel

class EventChannel { const EventChannel(this.name, [this.codec = const StandardMethodCodec(), BinaryMessenger? binaryMessenger]) : assert(name ! = null), assert(codec ! = null), _binaryMessenger = binaryMessenger; /// The logical channel on which communication happens, not null. final String name; /// The message codec used by this channel, not null. final MethodCodec codec; /// The messenger used by this channel to send platform messages, not null. BinaryMessenger get binaryMessenger => _binaryMessenger ?? ServicesBinding.instance! .defaultBinaryMessenger; final BinaryMessenger? _binaryMessenger; Stream<dynamic> receiveBroadcastStream([ dynamic arguments ]) { final MethodChannel methodChannel = MethodChannel(name, codec); late StreamController<dynamic> controller; controller = StreamController<dynamic>.broadcast(onListen: () async { binaryMessenger.setMessageHandler(name, (ByteData? reply) async { if (reply == null) { controller.close(); } else { try { controller.add(codec.decodeEnvelope(reply)); } on PlatformException catch (e) { controller.addError(e); } } return null; }); try { await methodChannel.invokeMethod<void>('listen', arguments); } catch (exception, stack) { FlutterError.reportError(FlutterErrorDetails( exception: exception, stack: stack, library: 'services library', context: ErrorDescription('while activating platform stream on channel $name'), )); } }, onCancel: () async { binaryMessenger.setMessageHandler(name, null); try { await methodChannel.invokeMethod<void>('cancel', arguments); } catch (exception, stack) { FlutterError.reportError(FlutterErrorDetails( exception: exception, stack: stack, library: 'services library', context: ErrorDescription('while de-activating platform stream on channel $name'), )); }}); return controller.stream; }}Copy the code

Name (Channel name), Codec (codec), messager(messenger)

  • Name (Channel name): Name is a unique identifier used to distinguish Platform channels. A Flutter application usually has multiple Platform channels, which are distinguished by name. For example, when making a method call using the MethodChannel platform, you need to specify a name for MethodChannel.
  • Messager: A messager is also called a messenger. Informally, a messenger is a modern Courier who is responsible for transporting data from Flutter to JAndroid/IOS, or from Android/IOS to Flutter. For the three channels ina Flutter, a messager is a BinaryMessager, although each has a different purpose. When we create a Channel and set up a message handler for it, we end up binding a BinaryMessagerHandler to the Channel. The Channel name is the key and stored in the Map structure. After receiving the sent message, the corresponding BinaryMessagerHandler is fetched according to the channel name carried in the message and sent to it for processing. On the Android platform,BinaryMessenger is an interface whose implementation class is FlutterNativeView
  • Codec: On the Platform In Channel, data carried by binaryMessenger needs to be transmitted between Dart layer,Native layer, and Android/IOS platforms. Therefore, a platform-independent data protocol needs to be considered that can support images/files and other resources. Therefore,binary byte stream is finally adopted as the data transmission protocol: transmission The sender needs to encode the data into binary data and the receiver decodes the data into raw data. Codec is responsible for encoding and decoding.

The three communication modes are different for each platform(iOS and Android) :

  • BasicMessageChannelUse:setMessageHandlerA callback receives a message from the platform.
  • MethodChannelUse:setMethodCallHandlerA callback receives a message from the platform.
  • EventChannelUse:receiveBroadcastStreamA callback receives a message from the platform.

In essence, Platform Channel uses BinaryMessenger to send and receive messages. The default messenger is Servicesbinding.instance! DefaultBinaryMessenger.

In conclusion, we can see that

  • Platform Channel communicates by CourierBinaryMessengerTo complete the operation of receiving and sending messages.
  • The default messenger isServicesBinding.instance! .defaultBinaryMessenger.
  • Codec encodes the message first, and binaryMessenger sends the encoded message to iOS and Android platforms, and waits for the reply message from iOS and Android platforms.

Focus on Codec

There are two codes in Flutter

  • MethodCodec: Used to codec methodCalls
  • MessageCodec: Used to encode and decode messages

MethodCodec

MethodCodec is used to codec binary data with method calls and returned results. The codec used by MethodChannel and EventChannel is MethodCodec.

  • Basic MethodCodec: Basic MethodCall, a MethodCall object that requires the method name and optional parameters
Abstract class MethodCodec {// code methodCall as binary ByteBuffer ByteData encodeMethodCall(methodCall methodCall); MethodCall decodeMethodCall(ByteData? methodCall); // Decode ByteBuffer into dynamic decodeEnvelope(ByteData envelope); ByteData ByteData encodeSuccessEnvelope(Object? result); // encode the error response as binary ByteData ByteData encodeErrorEnvelope({required String code, String? message, Object? details}); }Copy the code
  • JSONMethodCodec: The JSONMethodCodec depends on JSONMessageCodec. When you code a MethodCall object, you first turn it into a JSON object :{” method “:method,” args “:args}, such as the one that currently wants to call a ChannelsetVolum(5), whose corresponding MethodCall is converted to{"method":"setVolum","args":{"volum":5}}Next, it is encoded as binary data using JSONMessageCodec.
class JSONMethodCodec implements MethodCodec { const JSONMethodCodec(); @override ByteData encodeMethodCall(MethodCall call) { return const JSONMessageCodec().encodeMessage(<String, Object? >{ 'method': call.method, 'args': call.arguments, })! ; } @override MethodCall decodeMethodCall(ByteData? methodCall) { final Object? decoded = const JSONMessageCodec().decodeMessage(methodCall); if (decoded is! Map) throw FormatException('Expected method call Map, got $decoded'); final Object? method = decoded['method']; final Object? arguments = decoded['args']; if (method is String) return MethodCall(method, arguments); throw FormatException('Invalid method call: $decoded'); } @override dynamic decodeEnvelope(ByteData envelope) { final Object? decoded = const JSONMessageCodec().decodeMessage(envelope); if (decoded is! List) throw FormatException('Expected envelope List, got $decoded'); if (decoded.length == 1) return decoded[0]; if (decoded.length == 3 && decoded[0] is String && (decoded[1] == null || decoded[1] is String)) throw PlatformException( code: decoded[0] as String, message: decoded[1] as String? , details: decoded[2], ); if (decoded.length == 4 && decoded[0] is String && (decoded[1] == null || decoded[1] is String) && (decoded[3] == null || decoded[3] is String)) throw PlatformException( code: decoded[0] as String, message: decoded[1] as String? , details: decoded[2], stacktrace: decoded[3] as String? ,); throw FormatException('Invalid envelope: $decoded'); } @override ByteData encodeSuccessEnvelope(Object? result) { return const JSONMessageCodec().encodeMessage(<Object? >[result])! ; } @override ByteData encodeErrorEnvelope({ required String code, String? message, Object? details}) { assert(code ! = null); return const JSONMessageCodec().encodeMessage(<Object? >[code, message, details])! ; }}Copy the code
  • StandardMethodCodec: the codec for StandardMethodCodec relies on StandardMessageCodec, which is also the default implementation for MethodCodec. When its encoding encodes the MethodCall object, it encodes the MethoCall object’s method and args in turn using StandardMessageCodec, which is then written as binary data.
class StandardMethodCodec implements MethodCodec { const StandardMethodCodec([this.messageCodec = const StandardMessageCodec()]); /// The message codec that this method codec uses for encoding values. final StandardMessageCodec messageCodec; @override ByteData encodeMethodCall(MethodCall call) { final WriteBuffer buffer = WriteBuffer(); messageCodec.writeValue(buffer, call.method); messageCodec.writeValue(buffer, call.arguments); return buffer.done(); } @override MethodCall decodeMethodCall(ByteData? methodCall) { final ReadBuffer buffer = ReadBuffer(methodCall!) ; final Object? method = messageCodec.readValue(buffer); final Object? arguments = messageCodec.readValue(buffer); if (method is String && ! buffer.hasRemaining) return MethodCall(method, arguments); else throw const FormatException('Invalid method call'); } @override ByteData encodeSuccessEnvelope(Object? result) { final WriteBuffer buffer = WriteBuffer(); buffer.putUint8(0); messageCodec.writeValue(buffer, result); return buffer.done(); } @override ByteData encodeErrorEnvelope({ required String code, String? message, Object? details}) { final WriteBuffer buffer = WriteBuffer(); buffer.putUint8(1); messageCodec.writeValue(buffer, code); messageCodec.writeValue(buffer, message); messageCodec.writeValue(buffer, details); return buffer.done(); } @override dynamic decodeEnvelope(ByteData envelope) { // First byte is zero in success case, and non-zero otherwise. if (envelope.lengthInBytes == 0) throw const FormatException('Expected envelope, got nothing'); final ReadBuffer buffer = ReadBuffer(envelope); if (buffer.getUint8() == 0) return messageCodec.readValue(buffer); final Object? errorCode = messageCodec.readValue(buffer); final Object? errorMessage = messageCodec.readValue(buffer); final Object? errorDetails = messageCodec.readValue(buffer); final String? errorStacktrace = (buffer.hasRemaining) ? messageCodec.readValue(buffer) as String? : null; if (errorCode is String && (errorMessage == null || errorMessage is String) && ! buffer.hasRemaining) throw PlatformException(code: errorCode, message: errorMessage as String? , details: errorDetails, stacktrace: errorStacktrace); else throw const FormatException('Invalid envelope'); }}Copy the code

Above is the source code implementation of MethodCodec and its subclasses JSONMethodCodec and StandardMethodCodec. Next, let’s look at the source code implementation of MessageCodec.

MessageCodec

MessageCodec is used to encode and decode basic data and binary data. BasicMessageChannel uses it.

The subclasses of MessageCodec include BinaryCodec, StringCodec, JSONMessageCodec and StandardMessageCodec. There are altogether four kinds of MessageCodec. Here we read the source code to see how they are implemented.

  • Basic MessageCodec: MessageCodec is designed as a generic interface for converting binary data, ByteBuffer, to different types of data
Abstract class MessageCodec<T> {// encode the specified type of message as binary data ByteData? encodeMessage(T message); /// decode binary data to the specified type T? decodeMessage(ByteData? message); }Copy the code
  • BinaryCodec: Used to encode and decode binary data, doing nothing implementatively but returning binary data exactly as it is
class BinaryCodec implements MessageCodec<ByteData> {
  /// Creates a [MessageCodec] with unencoded binary messages represented using
  /// [ByteData].
  const BinaryCodec();

  @override
  ByteData? decodeMessage(ByteData? message) => message;

  @override
  ByteData? encodeMessage(ByteData? message) => message;
}
Copy the code
  • StringCodec: Codec between strings and binary data. Utf-8 encoding is used for strings
class StringCodec implements MessageCodec<String> { /// Creates a [MessageCodec] with UTF-8 encoded String messages. const StringCodec(); @override String? decodeMessage(ByteData? message) { if (message == null) return null; return utf8.decoder.convert(message.buffer.asUint8List(message.offsetInBytes, message.lengthInBytes)); } @override ByteData? encodeMessage(String? message) { if (message == null) return null; final Uint8List encoded = utf8.encoder.convert(message); return encoded.buffer.asByteData(); }}Copy the code
  • Between JSONMessageCodec: used for data types and binary data decoding, support base data types (Boolean, char, double, float, int, long, short, String) as well as a List, the Map.
class JSONMessageCodec implements MessageCodec<Object? > { // The codec serializes messages as defined by the JSON codec of the // dart:convert package. The format used must match the Android and // iOS counterparts. /// Creates a [MessageCodec] with UTF-8 encoded JSON messages. const JSONMessageCodec(); @override ByteData? encodeMessage(Object? message) { if (message == null) return null; /// string codec Encodes the string string as binary data return const StringCodec().encodeMessage(json.encode(message)); } @override dynamic decodeMessage(ByteData? message) { if (message == null) return message; /// StringCodec decodes binary data message to string string /// json Decodes string string to dart base data type, return. return json.decode(const StringCodec().decodeMessage(message)!) ; }}Copy the code
  • StandardMessageCodec: Codecs to be used between data types, and binary data, it is also used by default in BasicMessageChannel codec, support base data types (Boolean, char, double, float, int, long, short, String), List, Map as well Binary data
class StandardMessageCodec implements MessageCodec<Object? > { static const int _valueNull = 0; static const int _valueTrue = 1; static const int _valueFalse = 2; static const int _valueInt32 = 3; static const int _valueInt64 = 4; static const int _valueLargeInt = 5; static const int _valueFloat64 = 6; static const int _valueString = 7; static const int _valueUint8List = 8; static const int _valueInt32List = 9; static const int _valueInt64List = 10; static const int _valueFloat64List = 11; static const int _valueList = 12; static const int _valueMap = 13; static const int _valueFloat32List = 14; @override ByteData? encodeMessage(Object? message) { if (message == null) return null; final WriteBuffer buffer = WriteBuffer(); writeValue(buffer, message); return buffer.done(); } @override dynamic decodeMessage(ByteData? message) { if (message == null) return null; final ReadBuffer buffer = ReadBuffer(message); final Object? result = readValue(buffer); if (buffer.hasRemaining) throw const FormatException('Message corrupted'); return result; } void writeValue(WriteBuffer buffer, Object? value) { if (value == null) { buffer.putUint8(_valueNull); } else if (value is bool) { buffer.putUint8(value ? _valueTrue : _valueFalse); } else if (value is double) { buffer.putUint8(_valueFloat64); buffer.putFloat64(value); } else if (value is int) { if (-0x7fffffff - 1 <= value && value <= 0x7fffffff) { buffer.putUint8(_valueInt32); buffer.putInt32(value); } else { buffer.putUint8(_valueInt64); buffer.putInt64(value); } } else if (value is String) { buffer.putUint8(_valueString); final Uint8List bytes = utf8.encoder.convert(value); writeSize(buffer, bytes.length); buffer.putUint8List(bytes); } else if (value is Uint8List) { buffer.putUint8(_valueUint8List); writeSize(buffer, value.length); buffer.putUint8List(value); } else if (value is Int32List) { buffer.putUint8(_valueInt32List); writeSize(buffer, value.length); buffer.putInt32List(value); } else if (value is Int64List) { buffer.putUint8(_valueInt64List); writeSize(buffer, value.length); buffer.putInt64List(value); } else if (value is Float32List) { buffer.putUint8(_valueFloat32List); writeSize(buffer, value.length); buffer.putFloat32List(value); } else if (value is Float64List) { buffer.putUint8(_valueFloat64List); writeSize(buffer, value.length); buffer.putFloat64List(value); } else if (value is List) { buffer.putUint8(_valueList); writeSize(buffer, value.length); for (final Object? item in value) { writeValue(buffer, item); } } else if (value is Map) { buffer.putUint8(_valueMap); writeSize(buffer, value.length); value.forEach((Object? key, Object? value) { writeValue(buffer, key); writeValue(buffer, value); }); } else { throw ArgumentError.value(value); } } Object? readValue(ReadBuffer buffer) { if (! buffer.hasRemaining) throw const FormatException('Message corrupted'); final int type = buffer.getUint8(); return readValueOfType(type, buffer); } Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case _valueNull: return null; case _valueTrue: return true; case _valueFalse: return false; case _valueInt32: return buffer.getInt32(); case _valueInt64: return buffer.getInt64(); case _valueFloat64: return buffer.getFloat64(); case _valueLargeInt: case _valueString: final int length = readSize(buffer); return utf8.decoder.convert(buffer.getUint8List(length)); case _valueUint8List: final int length = readSize(buffer); return buffer.getUint8List(length); case _valueInt32List: final int length = readSize(buffer); return buffer.getInt32List(length); case _valueInt64List: final int length = readSize(buffer); return buffer.getInt64List(length); case _valueFloat32List: final int length = readSize(buffer); return buffer.getFloat32List(length); case _valueFloat64List: final int length = readSize(buffer); return buffer.getFloat64List(length); case _valueList: final int length = readSize(buffer); final List<Object? > result = List<Object? >.filled(length, null, growable: false); for (int i = 0; i < length; i++) result[i] = readValue(buffer); return result; case _valueMap: final int length = readSize(buffer); final Map<Object? , Object? > result = <Object? , Object? > {}; for (int i = 0; i < length; i++) result[readValue(buffer)] = readValue(buffer); return result; default: throw const FormatException('Message corrupted'); } } void writeSize(WriteBuffer buffer, int value) { assert(0 <= value && value <= 0xffffffff); if (value < 254) { buffer.putUint8(value); } else if (value <= 0xffff) { buffer.putUint8(254); buffer.putUint16(value); } else { buffer.putUint8(255); buffer.putUint32(value); } } int readSize(ReadBuffer buffer) { final int value = buffer.getUint8(); switch (value) { case 254: return buffer.getUint16(); case 255: return buffer.getUint32(); default: return value; }}}Copy the code

MessageCodec, MethodCodec, MessageCodec, MethodCodec, MessageCodec, MethodCodec, MessageCodec, MethodCodec The answer is yes. BinaryMessenge calls send to send binary data. Next, let’s go to BinaryMessenger source explore.

Focus on BinaryMessenger

Return to the beginning BasicMessageChannel where the message was sent

class BasicMessageChannel<T> { const BasicMessageChannel(this.name, this.codec, { BinaryMessenger? binaryMessenger }) : assert(name ! = null), assert(codec ! = null), _binaryMessenger = binaryMessenger; /// The logical channel on which communication happens, not null. final String name; /// The message codec used by this channel, not null. final MessageCodec<T> codec; /// The messenger which sends the bytes for this channel, not null. BinaryMessenger get binaryMessenger => _binaryMessenger ?? ServicesBinding.instance! .defaultBinaryMessenger; final BinaryMessenger? _binaryMessenger; Codec encodes the message first. BinaryMessenger will send the encoded message to iOS and Android platforms, and wait for the reply message from iOS and Android platforms. Future<T? > send(T message) async { return codec.decodeMessage(await binaryMessenger.send(name, codec.encodeMessage(message))); } void setMessageHandler(Future<T> Function(T? message)? handler) { if (handler == null) { binaryMessenger.setMessageHandler(name, null); } else { binaryMessenger.setMessageHandler(name, (ByteData? message) async { return codec.encodeMessage(await handler(codec.decodeMessage(message))); }); }}}Copy the code

Send sends a message in two steps:

  • Codec encodes the message message first
  • BinaryMessenger sends the encoded message to iOS and Android, and waits for the reply message from iOS and Android.

So how does BinaryMessenger work? We jumped into binaryMessenger.send() to find out.

BasicMessageChannel uses the default BinaryMessenger. Get BinaryMessenger => _binaryMessenger?? ServicesBinding.instance! .defaultBinaryMessenger;

The source code is as follows:

class _DefaultBinaryMessenger extends BinaryMessenger { const _DefaultBinaryMessenger._(); @override Future<void> handlePlatformMessage( String channel, ByteData? message, ui.PlatformMessageResponseCallback? callback, ) async { ui.channelBuffers.push(channel, message, (ByteData? data) { if (callback ! = null) callback(data); }); } @override Future<ByteData? > send(String channel, ByteData? message) { final Completer<ByteData? > completer = Completer<ByteData? > (); ui.PlatformDispatcher.instance.sendPlatformMessage(channel, message, (ByteData? reply) { try { completer.complete(reply); } catch (exception, stack) { FlutterError.reportError(FlutterErrorDetails( exception: exception, stack: stack, library: 'services library', context: ErrorDescription('during a platform message response callback'), )); }}); return completer.future; } @override void setMessageHandler(String channel, MessageHandler? handler) { if (handler == null) { ui.channelBuffers.clearListener(channel); } else { ui.channelBuffers.setListener(channel, (ByteData? data, ui.PlatformMessageResponseCallback callback) async { ByteData? response; try { response = await handler(data); } catch (exception, stack) { FlutterError.reportError(FlutterErrorDetails( exception: exception, stack: stack, library: 'services library', context: ErrorDescription('during a platform message callback'), )); } finally { callback(response); }}); }}}Copy the code

Can see the send method to implement the UI. Eventually PlatformDispatcher. Instance. SendPlatformMessage (). The parameter passed to the UI. PlatformDispatcher. Instance sendPlatformMessage method, and set up a reply. But far from the end, let’s take a look at uI. PlatformDispatcher source code:

void sendPlatformMessage(String name, ByteData? data, PlatformMessageResponseCallback? callback) { final String? error = _sendPlatformMessage(name, _zonedPlatformMessageResponseCallback(callback), data); if (error ! = null) throw Exception(error); } String? _sendPlatformMessage(String name, PlatformMessageResponseCallback? callback, ByteData? data) native 'PlatformConfiguration_sendPlatformMessage';Copy the code

The program finally executes the _sendPlatformMessage method, which has a native keyword. The interface is exported into dart from the native PlatformConfiguration_sendPlatformMessage method. In DART, The method name is _sendPlatformMessage. Finally we find the source of the Flutter engine for c++

void PlatformConfiguration::RegisterNatives( tonic::DartLibraryNatives* natives) { natives->Register({ {"PlatformConfiguration_defaultRouteName", DefaultRouteName, 1, true}, {"PlatformConfiguration_scheduleFrame", {"PlatformConfiguration_sendPlatformMessage", _SendPlatformMessage, 4, true}, {"PlatformConfiguration_respondToPlatformMessage", _RespondToPlatformMessage, 3, true}, {"PlatformConfiguration_render", Render, 3, true}, {"PlatformConfiguration_updateSemantics", UpdateSemantics, 2, true}, {"PlatformConfiguration_setIsolateDebugName", SetIsolateDebugName, 2, true}, {"PlatformConfiguration_reportUnhandledException", ReportUnhandledException, 2, true}, {"PlatformConfiguration_setNeedsReportTimings", SetNeedsReportTimings, 2, true}, {"PlatformConfiguration_getPersistentIsolateData", GetPersistentIsolateData, 1, true}, {"PlatformConfiguration_computePlatformResolvedLocale", _ComputePlatformResolvedLocale, 2, true}, }); Void _SendPlatformMessage(Dart_NativeArguments args) {// SendPlatformMessage is finally called tonic::DartCallStatic(&SendPlatformMessage, args); } // SendPlatformMessage Dart_Handle SendPlatformMessage(Dart_Handle window, const STD ::string& name, const STD ::string& name, Dart_Handle callback, Dart_Handle data_handle) { UIDartState* dart_state = UIDartState::Current(); Call platform method if (!) by main iOLATE dart_state->platform_configuration()) { return tonic::ToDart( "Platform messages can only be sent from the main isolate"); } fml::RefPtr<PlatformMessageResponse> response; if (! Dart_IsNull(callback)) { response = fml::MakeRefCounted<PlatformMessageResponseDart>( tonic::DartPersistentValue(dart_state, callback), dart_state->GetTaskRunners().GetUITaskRunner()); If (Dart_IsNull(data_handle)) {dart_state->platform_configuration()->client()->HandlePlatformMessage() std::make_unique<PlatformMessage>(name, response)); } else { tonic::DartByteData data(data_handle); const uint8_t* buffer = static_cast<const uint8_t*>(data.data()); dart_state->platform_configuration()->client()->HandlePlatformMessage( std::make_unique<PlatformMessage>( name, fml::MallocMapping::Copy(buffer, data.length_in_bytes()), response)); } return Dart_Null(); }Copy the code

dart_args.h

template <typename Sig> void DartCall(Sig func, Dart_NativeArguments args) { DartArgIterator it(args); using Indices = typename IndicesForSignature<Sig>::type; DartDispatcher<Indices, Sig> decoder(&it); if (it.had_exception()) return; decoder.Dispatch(func); }Copy the code

From c++ source code analysis, the SendPlatformMessage method, the implementation of HandlePlatformMessage class. Finally, the implementation of the iOS platform source

PlatformViewIOS complete code

// |PlatformView|
void PlatformViewIOS::HandlePlatformMessage(fml::RefPtr<flutter::PlatformMessage> message) {
  platform_message_router_.HandlePlatformMessage(std::move(message));
}
Copy the code

To be sure, PlatformViewIOS is the iOS counterpart. PlatformViewIOS continues distribution, and platform_message_Router_ executes the HandlePlatformMessage(STD :: Move (message)) method

PlatformMessageRouter

void PlatformMessageRouter::HandlePlatformMessage( fml::RefPtr<flutter::PlatformMessage> message) const { fml::RefPtr<flutter::PlatformMessageResponse> completer = message->response(); auto it = message_handlers_.find(message->channel()); if (it ! = message_handlers_.end()) { FlutterBinaryMessageHandler handler = it->second; NSData* data = nil; if (message->hasData()) { data = GetNSDataFromVector(message->data()); } handler(data, ^(NSData* reply) { if (completer) { if (reply) { completer->Complete(GetMappingFromNSData(reply)); } else { completer->CompleteEmpty(); }}}); } else { if (completer) { completer->CompleteEmpty(); }} // This SetMessageHandler is the interface that receives messages on iOS and Android. Very important!! void PlatformMessageRouter::SetMessageHandler(const std::string& channel, FlutterBinaryMessageHandler handler) { message_handlers_.erase(channel); if (handler) { message_handlers_[channel] = fml::ScopedBlock<FlutterBinaryMessageHandler>{handler, fml::OwnershipPolicy::Retain}; }}Copy the code

Finally, we see the familiar MM files for iOS development. From the above code:

  • This method receivesPlatformMessageType the message
  • frommessage_handlers_If there is any data, the handler will be converted to NSData. Finally, the handler will be executed. This is a block defined by OC codemessagethemessageSince the implementation of C++ code is encapsulatedfml::RefPtr<PlatformMessage>Type pointer, during which the Native level has not changed, second parameterFlutterBinaryReplyIt’s also a block whencompleterIt will be called if it existscompleter->Completeorcompleter->CompleteEmpty()To receive callbacks from native code.
  • PlatformMessageRouterIn the classSetMessageHandlerMethod is an important method for iOS platform to receive and reply messages, which has been implemented on iOS platform. The iOS platform defines oneFlutterBinaryMessageHandlerBlock method to perform the callback.

At this point you are done calling the Native layer.

The resources

Principle of Flutter Channel Communication (1)

Design and Implementation of Platform Channel

Flutter engine source code

PlatformMessageRouter