“This is the second day of my participation in the Gwen Challenge in November. See details: The Last Gwen Challenge in 2021”

preface

In the previous article, we introduced how to start Flutter in Native (the Android project), showing the Flutter page. But a lot of times during development, it’s not just about showing a page, it’s about interacting, like passing messages.

This article will briefly introduce three interaction modes between Flutter and Native:

BasicMessageChannel, MethodChannel and EventChannel.

BasicMessageChannel

There are three ways of interacting, but they’re all essentially the same, which we’ll explain later.

Let’s start with the BasicMessageChannel. It can realize mutual interaction and send some simple messages. The message type is Object, but not all objects can be sent. Basic types and basic types of arrays, lists, and maps can be sent. BasicMessageChannel:

  public void send(@Nullable T message, @Nullable final Reply<T> callback) {
    messenger.send(
        name,
        codec.encodeMessage(message),
        callback == null ? null : new IncomingReplyHandler(callback));
  }
Copy the code

EncodeMessage encodeMessage encodeMessage

  public ByteBuffer encodeMessage(Object message) {
    if (message == null) {
      return null;
    }
    final ExposedByteArrayOutputStream stream = new ExposedByteArrayOutputStream();
    writeValue(stream, message);
    final ByteBuffer buffer = ByteBuffer.allocateDirect(stream.size());
    buffer.put(stream.buffer(), 0, stream.size());
    return buffer;
  }
Copy the code

Here is the writeValue source code:

protected void writeValue(ByteArrayOutputStream stream, Object value) {
    if (value == null || value.equals(null)) {
      stream.write(NULL);
    } else if (value == Boolean.TRUE) {
      stream.write(TRUE);
    } else if (value == Boolean.FALSE) {
      stream.write(FALSE);
    } else if (value instanceof Number) {
      if (value instanceof Integer || value instanceof Short || value instanceof Byte) {
        stream.write(INT);
        writeInt(stream, ((Number) value).intValue());
      } else if (value instanceof Long) {
        stream.write(LONG);
        writeLong(stream, (long) value);
      } else if (value instanceof Float || value instanceof Double) {
        stream.write(DOUBLE);
        writeAlignment(stream, 8);
        writeDouble(stream, ((Number) value).doubleValue());
      } else if (value instanceof BigInteger) {
        stream.write(BIGINT);
        writeBytes(stream, ((BigInteger) value).toString(16).getBytes(UTF8));
      } else {
        throw new IllegalArgumentException("Unsupported Number type: "+ value.getClass()); }}else if (value instanceof String) {
      stream.write(STRING);
      writeBytes(stream, ((String) value).getBytes(UTF8));
    } else if (value instanceof byte[]) {
      stream.write(BYTE_ARRAY);
      writeBytes(stream, (byte[]) value);
    } else if (value instanceof int[]) {
      stream.write(INT_ARRAY);
      final int[] array = (int[]) value;
      writeSize(stream, array.length);
      writeAlignment(stream, 4);
      for (final intn : array) { writeInt(stream, n); }}else if (value instanceof long[]) {
      stream.write(LONG_ARRAY);
      final long[] array = (long[]) value;
      writeSize(stream, array.length);
      writeAlignment(stream, 8);
      for (final longn : array) { writeLong(stream, n); }}else if (value instanceof double[]) {
      stream.write(DOUBLE_ARRAY);
      final double[] array = (double[]) value;
      writeSize(stream, array.length);
      writeAlignment(stream, 8);
      for (final doubled : array) { writeDouble(stream, d); }}else if (value instanceof List) {
      stream.write(LIST);
      finalList<? > list = (List) value; writeSize(stream, list.size());for (finalObject o : list) { writeValue(stream, o); }}else if (value instanceof Map) {
      stream.write(MAP);
      finalMap<? ,? > map = (Map) value; writeSize(stream, map.size());for (final Entry<?, ?> entry : map.entrySet()) {
        writeValue(stream, entry.getKey());
        writeValue(stream, entry.getValue());
      }
    } else {
      throw new IllegalArgumentException("Unsupported value: "+ value); }}Copy the code

Let’s take a look at how to use it, using Android as an example.

The Android end

(1) Do not use engine cache preheating

If engine cache is not used, override configureFlutterEngine in the FlutterActivity inheritance class:

class MainActivity : FlutterActivity() {
    var channel : BasicMessageChannel? = null
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        var channel = BasicMessageChannel<String>(flutterEngine.dartExecutor.binaryMessenger,"test" ,StringCodec.INSTANCE)
        channel.setMessageHandler { message, reply ->
            Log.e("recieve", message)
        }
    }
}
Copy the code

Note that the second parameter “test” is the name of the channel.

The third argument is the codec for the message, and here we use StringCodec because it’s a simple example and the message is a String.

StringCodec is an implementation of the MessageCodec interface, along with BinaryCodec, JsonMessageCodec, and StandardMessageCodec. In addition, we can also implement MessageCodec, implement its two functions, its source code is as follows:

public interface MessageCodec<T> {
  /**
   * Encodes the specified message into binary.
   *
   * @param message the T message, possibly null.
   * @return a ByteBuffer containing the encoding between position 0 and the current position, or
   *     null, if message is null.
   */
  @Nullable
  ByteBuffer encodeMessage(@Nullable T message);
 
  /**
   * Decodes the specified message from binary.
   *
   * @param message the {@link ByteBuffer} message, possibly null.
   * @return a T value representation of the bytes between the given buffer's current position and
   *     its limit, or null, if message is null.
   */
  @Nullable
  T decodeMessage(@Nullable ByteBuffer message);
}
Copy the code

Finally, a MessageHandler is used to receive messages from a Flutter. Here the message is simply printed out.

When a message needs to be sent to a flutter, execute channel? . Send (” android call “)

(2) Use engine Cache to warm up

Add cache to Application as follows:

class App : Application() {
    companion object{...lateinit var flutterEngine2 : FlutterEngine
    }
    override fun onCreate(a) {
        super.onCreate()
        ...
 
        flutterEngine2 = FlutterEngine(this)
        flutterEngine2.navigationChannel.setInitialRoute("second")
        flutterEngine2.dartExecutor.executeDartEntrypoint(
                DartExecutor.DartEntrypoint.createDefault()
        )
        FlutterEngineCache.getInstance().put("second", flutterEngine2)
    }
}
Copy the code

Here we create an engine for the Second flutter page and add a cache to warm it up.

If we want to use this engine to send messages, we can create BasicMessageChannel directly

var channel = BasicMessageChannel<String>(App.flutterEngine2.dartExecutor.binaryMessenger,"test" ,StandardMessageCodec.INSTANCE as MessageCodec<String>)
channel.setMessageHandler { message, reply ->
    Log.e("recieve", message)
}
Copy the code

The rest is the same as above.

The Flutter end

The procedure is the same

static const messageChannel = const BasicMessageChannel("test", StringCodec());
Copy the code

Here, the channel name remains the same as native.

Set the callback:

    messageChannel.setMessageHandler((message) async
      {
        print(message)
      }
    );
Copy the code

Send a message

messageChannel.send("flutter call");

This enables bidirectional message interaction between Native and Flutter.

MethodChannel

The method used for both function calls is similar to BasicMessageChannel, which is essentially the same. Let’s see how to use it first.

The Android end

As with BasicMessageChannel, there are two different methods of preheating and unpreheating, but they all end up with the FlutterEngine object, so I don’t need to talk about it and just use it. The code is as follows:

  / / create
  var channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger,"test")
  // The callback executes the native function according to call
  channel.setMethodCallHandler { call, result ->
      when(call.method){
          "flutterCall"- > {// Execute our custom corresponding function
              flutterCall(call.arguments)
          }
          else- > {}}}Copy the code

Here flutterCall is a response to the request sent by Flutter. We define a corresponding function to process it, as follows:

    fun flutterCall(arguments : Object){
        Log.e("flutterCall"."message:" + arguments.toString())
    }
Copy the code

We can then execute the Flutter function by using the invokeMethod function, as follows:

  // Execute the flutter function
  channel.invokeMethod("androidCall"."android message")
Copy the code

The Flutter end

The same process, the code is as follows:

/ / create
static const methodChannel = const MethodChannel("test");
// Execute the flutter function according to call
    methodChannel.setMethodCallHandler((call) async {
      switch(call.method){
        case "androidCall":
          // Execute custom corresponding functions
          androidCall(call.arguments);
          break; }});// Execute native functions
methodChannel.invokeMethod("flutterCall"."flutter message");
Copy the code

Source code analysis

BasicMessageChannel sends messenger.send(…) The messenger is BinaryMessenger, which is the first argument to the constructor. MethodCannel is the same with its invokeMethod function:

  @UiThread
  public void invokeMethod(String method, @Nullable Object arguments, @Nullable Result callback) {
    messenger.send(
        name,
        codec.encodeMethodCall(new MethodCall(method, arguments)),
        callback == null ? null : new IncomingResultHandler(callback));
  }
Copy the code

As you can see, BinaryMessenger’s send function is finally called. You simply wrap two arguments to invokeMethod (the function name of type String method and the argument arguments of type Object) in MethodCall.

IncomingResultHandler encapsulates callback (); callback (); callback ();

  private final class IncomingMethodCallHandler implements BinaryMessageHandler {
    private final MethodCallHandler handler;
 
    IncomingMethodCallHandler(MethodCallHandler handler) {
      this.handler = handler;
    }
 
    @Override
    @UiThread
    public void onMessage(ByteBuffer message, final BinaryReply reply) {
      final MethodCall call = codec.decodeMethodCall(message);
      try {
        handler.onMethodCall(
            call,
            new Result() {
              ...
            });
      } catch(RuntimeException e) { ... }}... }Copy the code

Callback = onMessage (); callback = onMessage ();

MethodChannel and BasicMessageChannel are essentially the same, but wrapped in a MethodCall to retrieve function names and parameters.

EventChannel

EventChannel is different from the above two. It starts a flutter, native processes and returns the result, and the flutter reprocesses the result. It’s not exactly a one-way channel, but Native can’t initiate it, so it’s more of a C/S structure.

Let’s see how it works.

The Android end

We also need the FlutterEngine object as follows:

/ / create
var channel = EventChannel(flutterEngine.dartExecutor.binaryMessenger,"test")
// Set up to handle handler
channel.setStreamHandler(object : StreamHandler(), EventChannel.StreamHandler {
    override fun onListen(arguments: Any? , events:EventChannel.EventSink?). {
        // handle according to argumentsarguments? .let { ...// Return the processing result, which may or may not be successfulevents? .success("android back")
            //events? .error("errorcode", "errormssage", null)
            
            // If the endOfStream is not returned, that is, success and error are not executed, you need to execute endOfStream
            //events? .endOfStream()}}override fun onCancel(arguments: Any?). {
      // Perform the cancel operation}})Copy the code

As mentioned above, Native cannot initiate actively, so there is no send or invokeMethod function similar to the above.

The Flutter end

ReceiveBroadcastStream is used to send event requests and Linsten is used to listen for returns.

/ / create
static const eventChannel = const EventChannel("test");
// Send arguments to native processing and listen for results
eventChannel.receiveBroadcastStream(["flutter event"]).listen((event) {
  // A successful result is returned
  print(event.toString());
}, onError: (event){
  // Return error result, processing
}, onDone: (){
  // Perform completion processing
});
Copy the code

Source code analysis

receiveBroadcastStream

  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{... });try {
        await methodChannel.invokeMethod<void> ('listen', arguments);
      } catch (exception, stack) {
        ...
      }
    }, onCancel: () async {
      binaryMessenger.setMessageHandler(name, null);
      try {
        await methodChannel.invokeMethod<void> ('cancel', arguments);
      } catch(exception, stack) { ... }});return controller.stream;
  }
Copy the code

You can see that the EventChannel is essentially a MethodChannel, except that it executes a few predefined functions, such as Listen and cancel. This rewraps the MethodChannel to make event passing much easier.

conclusion

Above we have shown the use of the three interaction modes and analyzed their internal relationships. You can see that all three methods end up using the default implementation of BinaryMessenger, _DefaultBinaryMessenger. So if we implement our own special messaging mechanism through BinaryMessenger.