preface

In the development of FLUTTER, it is inevitable that some functions need to be called native, such as scan code, Bluetooth and other functions that interact with hardware. Although there are third-party Libraries of Flutter available online, our product needs are unique. What if you want to customize the scan interface? What if you want to personalize the interaction, for example, by staying on the scan screen and continuously scanning? What if you want to scan the barcode in the scan interface and then connect the Bluetooth printer to print? It’s hard to find a library that meets my current needs, so make it your own.

Flutter calls Android native

Do this with a MethodChannel. Methodchannels act as message channels for the Flutter side and native code. Flutter calls native methods, which package method names and parameters into secondary data via MethodChannel and pass them to the Android end. The plugin code on the Android side receives a method call to the flutter using the MethodChannel. The name of the method can be used to determine which method is called. Android code can pass the result of method execution (return value, error message) back to the Flutter endpoint via BinaryMessenger.

  • Call the steps

    1. Specify the channel name for a MethodChannel.

      The flutter end

      static const MethodChannel _channel =
            const MethodChannel('com.niimbot.flutter.scan');
      Copy the code

      The android end

      MethodChannel methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.niimbot.flutter.scan")
      Copy the code

      Note that this channel name must be the same on both sides of flutter and Android.

    2. The Flutter side calls the specified native function

      static Future<String> scan(String title, String content) async { try { final Map<dynamic, dynamic> result = await _channel .invokeMethod('niimbot_scan', {'title': title, 'content': content}); String code = result['code']; return code; } on PlatformException catch (e) { return e.details['error']; }}Copy the code

      Niimbot_scan here is the identifier of the function name, and does not represent the specific function name. You can define it however you like, as long as the Android terminal uses this name as the corresponding function identifier, you can call the specified function. Because the call is an asynchronous operation, if you need the return value from this call, you need to return the result of the function of type Future.

    3. Write Android platform plug-ins

      Create a new class that inherits FlutterPlugin under the project Android folder

      public class FlutterScanPlugin : FlutterPlugin, ActivityAware, MethodCallHandler{
      
      }
      Copy the code

      We can look at the FlutterPlugin class

      public interface FlutterPlugin { /** * This {@code FlutterPlugin} has been associated with a {@link FlutterEngine} instance. * * <p>Relevant resources that this {@code FlutterPlugin} may need are provided via the {@code * binding}. The  {@code binding} may be cached and referenced until {@link * #onDetachedFromEngine(FlutterPluginBinding)} is invoked and  returns. */ void onAttachedToEngine(@NonNull FlutterPluginBinding binding); /** * This {@code FlutterPlugin} has been removed from a {@link FlutterEngine} instance. * * <p>The {@code binding} passed to this method is the same instance that was passed in {@link * #onAttachedToEngine(FlutterPluginBinding)}. It is  provided again in this method as a * convenience. The {@code binding} may be referenced during the execution of this method, but it * must not be cached or referenced after this method returns. * * <p>{@code FlutterPlugin}s should release all resources in this method. */ void onDetachedFromEngine(@NonNull FlutterPluginBinding binding); /** * Resources made available to all plugins registered with a given {@link FlutterEngine}. * * <p>The provided {@link BinaryMessenger} can be used to communicate with Dart code running in * the Flutter context associated with this plugin binding. * * <p>Plugins that need to respond to {@code Lifecycle} events should implement the additional * {@link ActivityAware} and/or {@link ServiceAware} interfaces, where a {@link Lifecycle} * reference can be obtained. */ class FlutterPluginBinding { private final Context applicationContext; private final FlutterEngine flutterEngine; private final BinaryMessenger binaryMessenger; private final TextureRegistry textureRegistry; private final PlatformViewRegistry platformViewRegistry; private final FlutterAssets flutterAssets; public FlutterPluginBinding( @NonNull Context applicationContext, @NonNull FlutterEngine flutterEngine, @NonNull BinaryMessenger binaryMessenger, @NonNull TextureRegistry textureRegistry, @NonNull PlatformViewRegistry platformViewRegistry, @NonNull FlutterAssets flutterAssets) { this.applicationContext = applicationContext; this.flutterEngine = flutterEngine; this.binaryMessenger = binaryMessenger; this.textureRegistry = textureRegistry; this.platformViewRegistry = platformViewRegistry; this.flutterAssets = flutterAssets; } @NonNull public Context getApplicationContext() { return applicationContext; } /** * @deprecated Use {@code getBinaryMessenger()}, {@code getTextureRegistry()}, or {@code * getPlatformViewRegistry()} instead. */ @Deprecated @NonNull public FlutterEngine getFlutterEngine() { return  flutterEngine; } @NonNull public BinaryMessenger getBinaryMessenger() { return binaryMessenger; } @NonNull public TextureRegistry getTextureRegistry() { return textureRegistry; } @NonNull public PlatformViewRegistry getPlatformViewRegistry() { return platformViewRegistry; } @NonNull public FlutterAssets getFlutterAssets() { return flutterAssets; } } /** Provides Flutter plugins with access to Flutter asset information. */ interface FlutterAssets { /** * Returns the relative file path to the Flutter asset with the given name, including the file's * extension, e.g., {@code "myImage.jpg"}. * * <p>The returned file path is relative to the Android app's standard assets directory. * Therefore, the returned path is appropriate to pass to Android's {@code AssetManager}, but * the path is not appropriate to load as an absolute path. */ String getAssetFilePathByName(@NonNull String assetFileName); /** * Same as {@link #getAssetFilePathByName(String)} but with added support for an explicit * Android {@code packageName}. */ String getAssetFilePathByName(@NonNull String assetFileName, @NonNull String packageName); /** * Returns the relative file path to the Flutter asset with the given subpath, including the * file's extension, e.g., {@code "/dir1/dir2/myImage.jpg"}. * * <p>The returned file path is relative to the Android app's standard assets directory. * Therefore, the returned path is appropriate to pass to Android's {@code AssetManager}, but * the path is not appropriate to load as an absolute path. */ String getAssetFilePathBySubpath(@NonNull String assetSubpath); /** * Same as {@link #getAssetFilePathBySubpath(String)} but with added support for an explicit * Android {@code packageName}. */ String getAssetFilePathBySubpath(@NonNull String assetSubpath, @NonNull String packageName); }}Copy the code

      This is an interface that defines the callback method for onAttachedToEngine to connect to the flutter Engine and the callback method for onDetachedFromEngine to disconnect from the flutter Engine. The flutter engine is the underlying layer of flutter. It is implemented in C++ and provides support for the flutter framework. It also acts as a bridge between the flutter and native code.

    4. To register the plugin

      class MainActivity: FlutterActivity() {
          override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
              super.configureFlutterEngine(flutterEngine)
              flutterEngine.plugins.add(FlutterScanPlugin())
          }
      }
      Copy the code

      If the plug-in is a separate plug-in project created, you can save this code, the registration is automatically registered by the GeneratedPluginRegistrant, there will be registered

    5. Establish a message channel for function calls in the onAttachedToEngine callback of the plug-in class

      override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { this.flutterPluginBinding = flutterPluginBinding channel = MethodChannel (flutterPluginBinding binaryMessenger, com. Niimbot. Flutter. "scan") / / set the callback function call, Flutter the called function will go this callback channel. SetMethodCallHandler (this); }Copy the code

      After the plugin connects to the Flutter Engine, onAttachedToEngine is called back to pass the FlutterPluginBinding object back. By getting this object, we can get BinaryMessenger, which is the message channel through which the Android side can establish communication with the Flutter side.

    6. Implement the onMethodCall callback method of the MethodCallHandler interface to implement the specific call

      override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
              this.result = result
              if (call.method == "getPlatformVersion") {
                  result.success("Android ${android.os.Build.VERSION.RELEASE}")
              } else if (call.method == "niimbot_scan") {
                  val title = call.argument<String>("title")
                  val content = call.argument<String>("content")
                  val intent = Intent(activityPluginBinding.activity,ScanActivity::class.java)
                  intent.putExtra(TAG_TITLE,title)
                  intent.putExtra(TAG_CONTENT,content)
                  activityPluginBinding.activity.startActivityForResult(intent, ACTIVITY_REQUEST_CODE)
              } else {
                  result.notImplemented()
              }
          }
      Copy the code

      Here you can see is according to the specific name of the function identifier, which function to judge the flutter end want to call, call the name of the identifier and flutter end MethodChannel. The invokeMethod method is consistent with the name of the function identifier. Knowing the specific method to call, you can implement it on the Android side. The Result is returned via the Result object. Result. success is called if it is returned normally, and result.onError is called if it is returned abnormally. Result. notImplemented is invoked when the corresponding function name identifier is not found, and the flutter end receives an error indicating that the function is notImplemented.

    7. Make the plugin and aciVITY associated with the callback, that is, ActivityAware callback method

      override fun onDetachedFromActivity() { channel.setMethodCallHandler(null) } override fun onReattachedToActivityForConfigChanges(activityPluginBinding: ActivityPluginBinding) { this.activityPluginBinding = activityPluginBinding } override fun onAttachedToActivity(activityPluginBinding: ActivityPluginBinding) { this.activityPluginBinding = activityPluginBinding this.activityPluginBinding.addActivityResultListener{requestCode,resultCode,data-> if(requestCode== ACTIVITY_REQUEST_CODE){ if(resultCode== Activity.RESULT_OK){ val reply = HashMap<String,Any>() val code = data.getStringExtra(BaseScanActivity.RESULT_QRCODE_STRING) reply["code"] = code result.success(reply) } else { var error  = "cancel" data? .let { error = it.getStringExtra("error") } val reply = HashMap<String,Any>() reply["error"] = error result.error("error",null,reply) } } return@addActivityResultListener true } }Copy the code

      The onAttachedToActivity callback returns an ActivityPluginBinding object that contains an instance of a running Activity that can be used to perform operations that can only be performed in the Activity component. Such as requesting permissions, starting an Activity, taking photos, and so on. It can be said that at this point, you can do all the things that the Android side can do, and that the Flutter side cannot do, in this way you can teach Android code to do.

      Take a look at the ActivityPluginBinding class

      public interface ActivityPluginBinding { @NonNull Activity getActivity(); @NonNull Object getLifecycle(); void addRequestPermissionsResultListener(@NonNull RequestPermissionsResultListener var1); void removeRequestPermissionsResultListener(@NonNull RequestPermissionsResultListener var1); void addActivityResultListener(@NonNull ActivityResultListener var1); void removeActivityResultListener(@NonNull ActivityResultListener var1); void addOnNewIntentListener(@NonNull NewIntentListener var1); void removeOnNewIntentListener(@NonNull NewIntentListener var1); void addOnUserLeaveHintListener(@NonNull UserLeaveHintListener var1); void removeOnUserLeaveHintListener(@NonNull UserLeaveHintListener var1); void addOnSaveStateListener(@NonNull ActivityPluginBinding.OnSaveInstanceStateListener var1); void removeOnSaveStateListener(@NonNull ActivityPluginBinding.OnSaveInstanceStateListener var1); public interface OnSaveInstanceStateListener { void onSaveInstanceState(@NonNull Bundle var1); void onRestoreInstanceState(@Nullable Bundle var1); }}Copy the code

      It defines some common Activity lifecycle callbacks. The name tells you exactly what it does.

      Please note that the objects obtained by getLifecycle cannot be used directly. In pubspec.yaml you need to introduce flutter_plugin_Android_lifecycle. Call FlutterLifecycleAdapter. GetActivityLifecycle (activityPluginBinding), you can get the available Lifecycle.

  • Key class analysis

    1. BinaryMessenger

      Plays an important role in native and flutter communication. It is equivalent to native code and the message channel on the flutter side, responsible for sending and receiving messages. The implementation on Android is DartExecutor.

      Intercept part of the code:

      public class DartExecutor implements BinaryMessenger { private static final String TAG = "DartExecutor"; @NonNull private final FlutterJNI flutterJNI; @NonNull private final AssetManager assetManager; @NonNull private final DartMessenger dartMessenger; @NonNull private final BinaryMessenger binaryMessenger; private boolean isApplicationRunning = false; @Nullable private String isolateServiceId; @Nullable private IsolateServiceIdListener isolateServiceIdListener; private final BinaryMessenger.BinaryMessageHandler isolateChannelMessageHandler = new BinaryMessenger.BinaryMessageHandler() { @Override public void onMessage(ByteBuffer message, final BinaryReply callback) { isolateServiceId = StringCodec.INSTANCE.decodeMessage(message); if (isolateServiceIdListener ! = null) { isolateServiceIdListener.onIsolateServiceIdAvailable(isolateServiceId); }}}; public DartExecutor(@NonNull FlutterJNI flutterJNI, @NonNull AssetManager assetManager) { this.flutterJNI = flutterJNI; this.assetManager = assetManager; this.dartMessenger = new DartMessenger(flutterJNI); dartMessenger.setMessageHandler("flutter/isolate", isolateChannelMessageHandler); this.binaryMessenger = new DefaultBinaryMessenger(dartMessenger); } private static class DefaultBinaryMessenger implements BinaryMessenger { private final DartMessenger messenger; private DefaultBinaryMessenger(@NonNull DartMessenger messenger) { this.messenger = messenger; } /** * Sends the given {@code message} from Android to Dart over the given {@code channel}. * * @param channel the name  of the logical channel used for the message. * @param message the message payload, a direct-allocated {@link ByteBuffer} with the message * bytes */ @Override @UiThread public void send(@NonNull String channel, @Nullable ByteBuffer message) { messenger.send(channel, message, null); } /** * Sends the given {@code messages} from Android to Dart over the given {@code channel} and then * has the provided  {@code callback} invoked when the Dart side responds. * * @param channel the name of the logical channel used for the message. * @param message the message payload, a direct-allocated {@link ByteBuffer} with the message * bytes between position zero and current position, or null. * @param callback a callback invoked when the Dart application responds to the message */ @Override @UiThread public void send( @NonNull String channel, @Nullable ByteBuffer message, @Nullable BinaryMessenger.BinaryReply callback) { messenger.send(channel, message, callback); } /** * Sets the given {@link io.flutter.plugin.common.BinaryMessenger.BinaryMessageHandler} as the * singular handler for all incoming messages received from the Dart side of this Dart execution * context. * * @param channel the name of the channel. * @param handler a {@link BinaryMessageHandler} to be invoked on incoming messages, or null. */ @Override @UiThread public void setMessageHandler( @NonNull String channel, @Nullable BinaryMessenger.BinaryMessageHandler handler) { messenger.setMessageHandler(channel, handler); }}}Copy the code

      You can see that a DefaultBinaryMessenger object is held internally, which in turn wraps a DartMessenger object. The implementation logic is in this class.

      class DartMessenger implements BinaryMessenger, PlatformMessageHandler { private static final String TAG = "DartMessenger"; @NonNull private final FlutterJNI flutterJNI; @NonNull private final Map<String, BinaryMessenger.BinaryMessageHandler> messageHandlers; @NonNull private final Map<Integer, BinaryMessenger.BinaryReply> pendingReplies; private int nextReplyId = 1; DartMessenger(@NonNull FlutterJNI flutterJNI) { this.flutterJNI = flutterJNI; this.messageHandlers = new HashMap<>(); this.pendingReplies = new HashMap<>(); } @Override public void setMessageHandler( @NonNull String channel, @Nullable BinaryMessenger.BinaryMessageHandler handler) { if (handler == null) { Log.v(TAG, "Removing handler for channel '" + channel + "'"); messageHandlers.remove(channel); } else { Log.v(TAG, "Setting handler for channel '" + channel + "'"); messageHandlers.put(channel, handler); } } @Override @UiThread public void send(@NonNull String channel, @NonNull ByteBuffer message) { Log.v(TAG, "Sending message over channel '" + channel + "'"); send(channel, message, null); } @Override public void send( @NonNull String channel, @Nullable ByteBuffer message, @Nullable BinaryMessenger.BinaryReply callback) { Log.v(TAG, "Sending message with callback over channel '" + channel + "'"); int replyId = 0; if (callback ! = null) { replyId = nextReplyId++; pendingReplies.put(replyId, callback); } if (message == null) { flutterJNI.dispatchEmptyPlatformMessage(channel, replyId); } else { flutterJNI.dispatchPlatformMessage(channel, message, message.position(), replyId); } } @Override public void handleMessageFromDart( @NonNull final String channel, @Nullable byte[] message, final int replyId) { Log.v(TAG, "Received message from Dart over channel '" + channel + "'"); BinaryMessenger.BinaryMessageHandler handler = messageHandlers.get(channel); if (handler ! = null) { try { Log.v(TAG, "Deferring to registered handler to process message."); final ByteBuffer buffer = (message == null ? null : ByteBuffer.wrap(message)); handler.onMessage(buffer, new Reply(flutterJNI, replyId)); } catch (Exception ex) { Log.e(TAG, "Uncaught exception in binary message listener", ex); flutterJNI.invokePlatformMessageEmptyResponseCallback(replyId); } } else { Log.v(TAG, "No registered handler for message. Responding to Dart with empty reply message."); flutterJNI.invokePlatformMessageEmptyResponseCallback(replyId); } } @Override public void handlePlatformMessageResponse(int replyId, @Nullable byte[] reply) { Log.v(TAG, "Received message reply from Dart."); BinaryMessenger.BinaryReply callback = pendingReplies.remove(replyId); if (callback ! = null) { try { Log.v(TAG, "Invoking registered callback for reply from Dart."); callback.reply(reply == null ? null : ByteBuffer.wrap(reply)); } catch (Exception ex) { Log.e(TAG, "Uncaught exception in binary message reply handler", ex); } } } /** * Returns the number of pending channel callback replies. * * <p>When sending messages to the Flutter application using {@link BinaryMessenger#send(String, * ByteBuffer, io.flutter.plugin.common.BinaryMessenger.BinaryReply)}, developers can optionally * specify a reply callback if they expect a reply from the Flutter application. * * <p>This method tracks all the pending callbacks that are waiting for response, and is supposed * to be called from the main thread (as other methods). Calling from a different thread could * possibly  capture an indeterministic internal state, so don't do it. */ @UiThread public int getPendingChannelResponseCount() { return pendingReplies.size(); } private static class Reply implements BinaryMessenger.BinaryReply { @NonNull private final FlutterJNI flutterJNI; private final int replyId; private final AtomicBoolean done = new AtomicBoolean(false); Reply(@NonNull FlutterJNI flutterJNI, int replyId) { this.flutterJNI = flutterJNI; this.replyId = replyId; } @Override public void reply(@Nullable ByteBuffer reply) { if (done.getAndSet(true)) { throw new IllegalStateException("Reply already submitted"); } if (reply == null) { flutterJNI.invokePlatformMessageEmptyResponseCallback(replyId); } else { flutterJNI.invokePlatformMessageResponseCallback(replyId, reply, reply.position()); }}}}Copy the code

      The handleMessageFromDart method receives messages from a flutter and retrits the corresponding BinaryMessageHandler based on the Channel name. For example our MethodChannel BinaryMessageHandler is wrapped in MethodChannel FlutterScanPlugin IncomingMethodCallHandler this plug-in, the message forwarded to handle it. The onMethodCall callback implemented in FlutterScanPlugin is finally called.

      @Override @UiThread public void onMessage(ByteBuffer message, final BinaryReply reply) { final MethodCall call = codec.decodeMethodCall(message); try { handler.onMethodCall( call, new Result() { @Override public void success(Object result) { reply.reply(codec.encodeSuccessEnvelope(result)); } @Override public void error(String errorCode, String errorMessage, Object errorDetails) { reply.reply(codec.encodeErrorEnvelope(errorCode, errorMessage, errorDetails)); } @Override public void notImplemented() { reply.reply(null); }}); } catch (RuntimeException e) { Log.e(TAG + name, "Failed to handle method call", e); reply.reply(codec.encodeErrorEnvelope("error", e.getMessage(), null)); }}Copy the code

      In FlutterJNI, this class involves the invocation of JNI native methods, and finally calls the implementation code of the Flutter engine. FlutterJNI partial code interception:

      @Keep
      public class FlutterJNI {
        private static final String TAG = "FlutterJNI";
      
        @Nullable private static AsyncWaitForVsyncDelegate asyncWaitForVsyncDelegate;
        // This should also be updated by FlutterView when it is attached to a Display.
        // The initial value of 0.0 indicates unknown refresh rate.
        private static float refreshRateFPS = 0.0f;
      
        // This is set from native code via JNI.
        @Nullable private static String observatoryUri;
      
        // TODO(mattcarroll): add javadocs
        public static native void nativeInit(
            @NonNull Context context,
            @NonNull String[] args,
            @Nullable String bundlePath,
            @NonNull String appStoragePath,
            @NonNull String engineCachesPath,
            long initTimeMillis);
      
        // --------- Start Platform Message Support ------
        /**
         * Sets the handler for all platform messages that come from the attached platform view to Java.
         *
         * <p>Communication between a specific Flutter context (Dart) and the host platform (Java) is
         * accomplished by passing messages. Messages can be sent from Java to Dart with the corresponding
         * {@code FlutterJNI} methods:
         *
         * <ul>
         *   <li>{@link #dispatchPlatformMessage(String, ByteBuffer, int, int)}
         *   <li>{@link #dispatchEmptyPlatformMessage(String, int)}
         * </ul>
         *
         * <p>{@code FlutterJNI} is also the recipient of all platform messages sent from its attached
         * Flutter context. {@code FlutterJNI} does not know what to do with these messages, so a handler
         * is exposed to allow these messages to be processed in whatever manner is desired:
         *
         * <p>{@code setPlatformMessageHandler(PlatformMessageHandler)}
         *
         * <p>If a message is received but no {@link PlatformMessageHandler} is registered, that message
         * will be dropped (ignored). Therefore, when using {@code FlutterJNI} to integrate a Flutter
         * context in an app, a {@link PlatformMessageHandler} must be registered for 2-way Java/Dart
         * communication to operate correctly. Moreover, the handler must be implemented such that
         * fundamental platform messages are handled as expected. See {@link FlutterNativeView} for an
         * example implementation.
         */
        @UiThread
        public void setPlatformMessageHandler(@Nullable PlatformMessageHandler platformMessageHandler) {
          ensureRunningOnMainThread();
          this.platformMessageHandler = platformMessageHandler;
        }
      
        // Called by native.
        // TODO(mattcarroll): determine if message is nonull or nullable
        @SuppressWarnings("unused")
        @VisibleForTesting
        public void handlePlatformMessage(
            @NonNull final String channel, byte[] message, final int replyId) {
          if (platformMessageHandler != null) {
            platformMessageHandler.handleMessageFromDart(channel, message, replyId);
          }
          // TODO(mattcarroll): log dropped messages when in debug mode
          // (https://github.com/flutter/flutter/issues/25391)
        }
      
        // Called by native to respond to a platform message that we sent.
        // TODO(mattcarroll): determine if reply is nonull or nullable
        @SuppressWarnings("unused")
        private void handlePlatformMessageResponse(int replyId, byte[] reply) {
          if (platformMessageHandler != null) {
            platformMessageHandler.handlePlatformMessageResponse(replyId, reply);
          }
          // TODO(mattcarroll): log dropped messages when in debug mode
          // (https://github.com/flutter/flutter/issues/25391)
        }
      
        /**
         * Sends an empty reply (identified by {@code responseId}) from Android to Flutter over the given
         * {@code channel}.
         */
        @UiThread
        public void dispatchEmptyPlatformMessage(@NonNull String channel, int responseId) {
          ensureRunningOnMainThread();
          if (isAttached()) {
            nativeDispatchEmptyPlatformMessage(nativePlatformViewId, channel, responseId);
          } else {
            Log.w(
                TAG,
                "Tried to send a platform message to Flutter, but FlutterJNI was detached from native C++. Could not send. Channel: "
                    + channel
                    + ". Response ID: "
                    + responseId);
          }
        }
      
        // Send an empty platform message to Dart.
        private native void nativeDispatchEmptyPlatformMessage(
            long nativePlatformViewId, @NonNull String channel, int responseId);
      
        /** Sends a reply {@code message} from Android to Flutter over the given {@code channel}. */
        @UiThread
        public void dispatchPlatformMessage(
            @NonNull String channel, @Nullable ByteBuffer message, int position, int responseId) {
          ensureRunningOnMainThread();
          if (isAttached()) {
            nativeDispatchPlatformMessage(nativePlatformViewId, channel, message, position, responseId);
          } else {
            Log.w(
                TAG,
                "Tried to send a platform message to Flutter, but FlutterJNI was detached from native C++. Could not send. Channel: "
                    + channel
                    + ". Response ID: "
                    + responseId);
          }
        }
      
        // Send a data-carrying platform message to Dart.
        private native void nativeDispatchPlatformMessage(
            long nativePlatformViewId,
            @NonNull String channel,
            @Nullable ByteBuffer message,
            int position,
            int responseId);
      
        // TODO(mattcarroll): differentiate between channel responses and platform responses.
        @UiThread
        public void invokePlatformMessageEmptyResponseCallback(int responseId) {
          ensureRunningOnMainThread();
          if (isAttached()) {
            nativeInvokePlatformMessageEmptyResponseCallback(nativePlatformViewId, responseId);
          } else {
            Log.w(
                TAG,
                "Tried to send a platform message response, but FlutterJNI was detached from native C++. Could not send. Response ID: "
                    + responseId);
          }
        }
      
        // Send an empty response to a platform message received from Dart.
        private native void nativeInvokePlatformMessageEmptyResponseCallback(
            long nativePlatformViewId, int responseId);
      
        // TODO(mattcarroll): differentiate between channel responses and platform responses.
        @UiThread
        public void invokePlatformMessageResponseCallback(
            int responseId, @Nullable ByteBuffer message, int position) {
          ensureRunningOnMainThread();
          if (isAttached()) {
            nativeInvokePlatformMessageResponseCallback(
                nativePlatformViewId, responseId, message, position);
          } else {
            Log.w(
                TAG,
                "Tried to send a platform message response, but FlutterJNI was detached from native C++. Could not send. Response ID: "
                    + responseId);
          }
        }
      
        // Send a data-carrying response to a platform message received from Dart.
        private native void nativeInvokePlatformMessageResponseCallback(
            long nativePlatformViewId, int responseId, @Nullable ByteBuffer message, int position);
      }
      Copy the code

      The JNI native code calls the handlePlatformMessage method to send messages from the Flutter side to the Android side. InvokePlatformMessageResponseCallback will invoke the JNI native method, will the android side message sent to flutter.

      On the flutter MethodChannel invokeMethod calls to

      @optionalTypeArgs Future<T> _invokeMethod<T>(String method, { 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; }Copy the code

      You can see that messages are also sent through BinaryMessenger. If the MethodChannel does not specify BinaryMessenger, the default implementation is used:

      class _DefaultBinaryMessenger extends BinaryMessenger { const _DefaultBinaryMessenger._(); // Handlers for incoming messages from platform plugins. // This is static so that this class can have a const constructor. static final Map<String, MessageHandler> _handlers = <String, MessageHandler>{}; // Mock handlers that intercept and respond to outgoing messages. // This is static so that this class can have a const constructor. static final Map<String, MessageHandler> _mockHandlers = <String, MessageHandler>{}; Future<ByteData> _sendPlatformMessage(String channel, ByteData message) { final Completer<ByteData> completer = Completer<ByteData>(); // ui.window is accessed directly instead of using ServicesBinding.instance.window // because this method might be invoked before any binding is initialized. // This issue was reported in #27541. It is not ideal to statically access //  ui.window because the Window may be dependency injected elsewhere with // a different instance. However, static access at this location seems to be // the least bad option. ui.window.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; }}Copy the code

      Follow up the UI. Window. SendPlatformMessage method:

      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 'Window_sendPlatformMessage';Copy the code

      Dart this section is defined in the window. Dart class that belongs to the Flutter engine. You can see that at this point you turn to the native layer of the Flutter Engine to forward messages to the Android terminal.

      The code for the Flutter Engine implementation is not expanded here.

    2. MethodCodec

      This encodes and decodes data in FLUTTER and Android communication. Why is this necessary? Because the data that a flutter and its native host transfer is binary data, it is impossible to get the data directly, so it needs to encode and decode the data according to certain rules so that the flutter and native host can convert the binary data into the data type supported by the language. The flutter side uses StandardMethodCodec by default, and the codec implementation in this class uses StandardMessageCodec. Intercept part of the code:

      class StandardMessageCodec implements MessageCodec<dynamic> { /// Creates a [MessageCodec] using the Flutter standard binary encoding. const StandardMessageCodec(); 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; void writeValue(WriteBuffer buffer, dynamic value) { if (value == null) { buffer.putUint8(_valueNull); } else if (value is bool) { buffer.putUint8(value ? _valueTrue : _valueFalse); } else if (value is double) { // Double precedes int because in JS everything is a double. // Therefore in JS, both `is int` and `is double` always // return `true`. If we check int first, we'll end up treating // all numbers as ints and attempt the int32/int64 conversion, // which is wrong. This precedence rule is irrelevant when // decoding because we use tags to detect the type of value. 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 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 dynamic item in value) { writeValue(buffer, item); } } else if (value is Map) { buffer.putUint8(_valueMap); writeSize(buffer, value.length); value.forEach((dynamic key, dynamic value) { writeValue(buffer, key); writeValue(buffer, value); }); } else { throw ArgumentError.value(value); } } /// Reads a value from [buffer] as written by [writeValue]. /// /// This method is intended for use by subclasses overriding /// [readValueOfType]. dynamic readValue(ReadBuffer buffer) { if (! buffer.hasRemaining) throw const FormatException('Message corrupted'); final int type = buffer.getUint8(); return readValueOfType(type, buffer); } /// Reads a value of the indicated [type] from [buffer]. /// /// The codec can be extended by overriding this method, calling super for /// types that the extension does not handle. See the discussion at /// [writeValue]. dynamic 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 _valueFloat64List: final int length = readSize(buffer); return buffer.getFloat64List(length); case _valueList: final int length = readSize(buffer); final dynamic result = List<dynamic>(length); for (int i = 0; i < length; i++) result[i] = readValue(buffer); return result; case _valueMap: final int length = readSize(buffer); final dynamic result = <dynamic, dynamic>{}; for (int i = 0; i < length; i++) result[readValue(buffer)] = readValue(buffer); return result; default: throw const FormatException('Message corrupted'); }}}Copy the code

      In this case, a message is sent to the Android terminal by specifying the type of data according to the specified number. For each parameter, data type (represented by the specified int type), size (list, map data), and secondary data are written. If android messages are received, the binary data is parsed using the same rules to restore the data type of flutter. As can be seen from the data type definition, the supported data types are null, bool, int, 8-byte int, String, Double, Uint8List, Int32List, Float64List, List and Map. Here’s a comparison table by platform type:

      Dart Java Kotlin OC Swift
      null null null nil (NSNull when nested) nil
      bool java.lang.Boolean Boolean NSNumber numberWithBool: NSNumber(value: Bool)
      int java.lang.Integer Int NSNumber numberWithInt: NSNumber(value: Int32)
      int, if 32 bits not enough java.lang.Long Long NSNumber numberWithLong: NSNumber(value: Int)
      double java.lang.Double Double NSNumber numberWithDouble: NSNumber(value: Double)
      String java.lang.String String NSString String
      Uint8List byte[] ByteArray FlutterStandardTypedData typedDataWithBytes: FlutterStandardTypedData(bytes: Data)
      Int32List int[] IntArray FlutterStandardTypedData typedDataWithInt32: FlutterStandardTypedData(int32: Data)
      Int64List long[] LongArray FlutterStandardTypedData typedDataWithInt64: FlutterStandardTypedData(int64: Data)
      Float64List double[] DoubleArray FlutterStandardTypedData typedDataWithFloat64: FlutterStandardTypedData(float64: Data)
      List java.util.ArrayList List NSArray Array
      Map java.util.HashMap HashMap NSDictionary Dictionary

      Here’s another look at the StandardMethodCodec class on Android:

      public final class StandardMethodCodec implements MethodCodec { public static final StandardMethodCodec INSTANCE; private final StandardMessageCodec messageCodec; public StandardMethodCodec(StandardMessageCodec messageCodec) { this.messageCodec = messageCodec; } public ByteBuffer encodeMethodCall(MethodCall methodCall) { ExposedByteArrayOutputStream stream = new ExposedByteArrayOutputStream(); this.messageCodec.writeValue(stream, methodCall.method); this.messageCodec.writeValue(stream, methodCall.arguments); ByteBuffer buffer = ByteBuffer.allocateDirect(stream.size()); buffer.put(stream.buffer(), 0, stream.size()); return buffer; } public MethodCall decodeMethodCall(ByteBuffer methodCall) { methodCall.order(ByteOrder.nativeOrder()); Object method = this.messageCodec.readValue(methodCall); Object arguments = this.messageCodec.readValue(methodCall); if (method instanceof String && ! methodCall.hasRemaining()) { return new MethodCall((String)method, arguments); } else { throw new IllegalArgumentException("Method call corrupted"); } } public ByteBuffer encodeSuccessEnvelope(Object result) { ExposedByteArrayOutputStream stream = new ExposedByteArrayOutputStream(); stream.write(0); this.messageCodec.writeValue(stream, result); ByteBuffer buffer = ByteBuffer.allocateDirect(stream.size()); buffer.put(stream.buffer(), 0, stream.size()); return buffer; } public ByteBuffer encodeErrorEnvelope(String errorCode, String errorMessage, Object errorDetails) { ExposedByteArrayOutputStream stream = new ExposedByteArrayOutputStream(); stream.write(1); this.messageCodec.writeValue(stream, errorCode); this.messageCodec.writeValue(stream, errorMessage); this.messageCodec.writeValue(stream, errorDetails); ByteBuffer buffer = ByteBuffer.allocateDirect(stream.size()); buffer.put(stream.buffer(), 0, stream.size()); return buffer; } public Object decodeEnvelope(ByteBuffer envelope) { envelope.order(ByteOrder.nativeOrder()); byte flag = envelope.get(); Object code; switch(flag) { case 0: code = this.messageCodec.readValue(envelope); if (! envelope.hasRemaining()) { return code; } case 1: code = this.messageCodec.readValue(envelope); Object message = this.messageCodec.readValue(envelope); Object details = this.messageCodec.readValue(envelope); if (code instanceof String && (message == null || message instanceof String) && ! envelope.hasRemaining()) { throw new FlutterException((String)code, (String)message, details); } default: throw new IllegalArgumentException("Envelope corrupted"); } } static { INSTANCE = new StandardMethodCodec(StandardMessageCodec.INSTANCE); }}Copy the code

      As you can see, StandardMessageCodec is also used to implement the specific codec of data:

      public class StandardMessageCodec implements MessageCodec<Object> { private static final String TAG = "StandardMessageCodec#"; public static final StandardMessageCodec INSTANCE = new StandardMessageCodec(); private static final boolean LITTLE_ENDIAN; private static final Charset UTF8; private static final byte NULL = 0; private static final byte TRUE = 1; private static final byte FALSE = 2; private static final byte INT = 3; private static final byte LONG = 4; private static final byte BIGINT = 5; private static final byte DOUBLE = 6; private static final byte STRING = 7; private static final byte BYTE_ARRAY = 8; private static final byte INT_ARRAY = 9; private static final byte LONG_ARRAY = 10; private static final byte DOUBLE_ARRAY = 11; private static final byte LIST = 12; private static final byte MAP = 13; protected void writeValue(ByteArrayOutputStream stream, Object value) { if (value ! = null && ! value.equals((Object)null)) { if (value == Boolean.TRUE) { stream.write(1); } else if (value == Boolean.FALSE) { stream.write(2); } else if (value instanceof Number) { if (! (value instanceof Integer) && ! (value instanceof Short) && ! (value instanceof Byte)) { if (value instanceof Long) { stream.write(4); writeLong(stream, (Long)value); } else if (! (value instanceof Float) && ! (value instanceof Double)) { if (! (value instanceof BigInteger)) { throw new IllegalArgumentException("Unsupported Number type: " + value.getClass()); } stream.write(5); writeBytes(stream, ((BigInteger)value).toString(16).getBytes(UTF8)); } else { stream.write(6); writeAlignment(stream, 8); writeDouble(stream, ((Number)value).doubleValue()); } } else { stream.write(3); writeInt(stream, ((Number)value).intValue()); } } else if (value instanceof String) { stream.write(7); writeBytes(stream, ((String)value).getBytes(UTF8)); } else if (value instanceof byte[]) { stream.write(8); writeBytes(stream, (byte[])((byte[])value)); } else { int var5; int var6; if (value instanceof int[]) { stream.write(9); int[] array = (int[])((int[])value); writeSize(stream, array.length); writeAlignment(stream, 4); int[] var4 = array; var5 = array.length; for(var6 = 0; var6 < var5; ++var6) { int n = var4[var6]; writeInt(stream, n); } } else if (value instanceof long[]) { stream.write(10); long[] array = (long[])((long[])value); writeSize(stream, array.length); writeAlignment(stream, 8); long[] var12 = array; var5 = array.length; for(var6 = 0; var6 < var5; ++var6) { long n = var12[var6]; writeLong(stream, n); } } else if (value instanceof double[]) { stream.write(11); double[] array = (double[])((double[])value); writeSize(stream, array.length); writeAlignment(stream, 8); double[] var14 = array; var5 = array.length; for(var6 = 0; var6 < var5; ++var6) { double d = var14[var6]; writeDouble(stream, d); } } else { Iterator var15; if (value instanceof List) { stream.write(12); List<? > list = (List)value; writeSize(stream, list.size()); var15 = list.iterator(); while(var15.hasNext()) { Object o = var15.next(); this.writeValue(stream, o); } } else { if (! (value instanceof Map)) { throw new IllegalArgumentException("Unsupported value: " + value); } stream.write(13); Map<? ,? > map = (Map)value; writeSize(stream, map.size()); var15 = map.entrySet().iterator(); while(var15.hasNext()) { Entry<? ,? > entry = (Entry)var15.next(); this.writeValue(stream, entry.getKey()); this.writeValue(stream, entry.getValue()); } } } } } else { stream.write(0); }}}Copy the code

      Codec the same way as the Flutter end.

conclusion

Flutter and native interaction essentially communicate with each other through a secondary stream of messages. MethodChannel, as a communication channel, encapsulates the sending, receiving and codec of messages such as method calls. BinaryMessenger acts as a message channel for sending and receiving messages. MethodCodec acts as a codec that serializes and deserializes data; The Flutter engine acts as the bridge between flutter and native, and is responsible for message forwarding.