Method Channel Implementation on Mobile and Desktop

Method Channel Implementation on Mobile and Desktop

Android + IOS + MacOS + Windows + Linux

·

6 min read

Method channel allows flutter to communicate with the native platform. The channel message is enveloped in binary and sent with FIFO. Many packages has used this method to implement the features and it is not rare project required the native integration. Out of curiosity, I have searched the flutter.dev, it has provided the Android, IOS and Windows examples, but seems not have the MacOS and Linux example. This article has summarised the implementation of Android (Kotlin), IOS(Swift), MacOS(Swift), Windows(C++) and Linux(C) method channel setup in main project ( NOT the flutter plugin one).

Flutter(Dart)

The first thing is to define the channel name. Later on the native side, the channel name will be used to identify whether it is the same channel. Similarly the method name defined in the _platform.invokeMethod will also used to identify what action it is calling. Since the channel communication is asynchronous, it requires await the response later returned from native platform. Here is a simple state of stateful widget which only has one button to trigger the call.

class _MyHomePageState extends State<MyHomePage> {
  /// TODO:[Flutter][1] Setup the MethodChannel
  final MethodChannel _platform =
      const MethodChannel('cbl.tool.flutter_platform_channel');

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Builder(builder: (context) {
        return Center(
          child: ElevatedButton(
            child: Text('Say hi to ${Platform.operatingSystem}!'),
            onPressed: () async {
              /// TODO:[Flutter][2] Submit call to the platform
              String msg = await _platform.invokeMethod('shakeHand');
              if (mounted) {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(
                    content: Text(msg),
                  ),
                );
              }
            },
          ),
        );
      }),
    );
  }
}

Android(Kotlin)

In android, what we need to do is to override the configureFlutterEngine and use the FlutterEngine to Create a MethodChannel by the binary messenger and channel name. The setMethodCallHandler will call back if there is method called from flutter side. When the method name is shakeHand, the channel will later greeting back the flutter side.

class MainActivity: FlutterActivity() {
    private val channel = "cbl.tool.flutter_platform_channel"
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        // TODO:[Android] [1] override configureFlutterEngine and create MethodChannel
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channel).setMethodCallHandler {
            call, result ->
            if (call.method == "shakeHand") {
                result.success("Hi from Android!");
            }else{
                result.notImplemented();
            }
          }
    }
}

IOS(Swift)

For IOS, binaryMessenger is retrieved from the FlutterViewController which will bind on the rootViewController when app initialisation.

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    override func application(
            _ application: UIApplication,
            didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // TODO:[IOS][1] Get the flutter root view controller for binary messenger
        let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
        // TODO:[IOS][2] Set the flutter channel
        let flutterChannel = FlutterMethodChannel(name: "cbl.tool.flutter_platform_channel",
                binaryMessenger: controller.binaryMessenger)
        // TODO:[IOS][3] Set the channel method call handler
        flutterChannel.setMethodCallHandler({
            (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
            if (call.method == "shakeHand") {
                result(String("Hi from IOS!"))
            } else {
                result(FlutterMethodNotImplemented)
            }
        })

        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}

MacOS(Swift)

Similar to IOS, MacOS binaryMessenger is retrieved from the FlutterViewController. Setup is basically same as the IOS one.

class MainFlutterWindow: NSWindow {
    override func awakeFromNib() {
        let flutterViewController = FlutterViewController.init()
        let windowFrame = self.frame
        self.contentViewController = flutterViewController
        self.setFrame(windowFrame, display: true)

        // TODO:[Mac][1] Set the flutter channel
        let flutterChannel = FlutterMethodChannel(name: "cbl.tool.flutter_platform_channel",
                binaryMessenger: flutterViewController.engine.binaryMessenger)
        // TODO:[Mac][2] Set the channel method call handler
        flutterChannel.setMethodCallHandler({
            (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
            if (call.method == "shakeHand") {
                result(String("Hi from MacOS!"))
            } else {
                result(FlutterMethodNotImplemented)
            }
        })

        RegisterGeneratedPlugins(registry: flutterViewController)
        super.awakeFromNib()
    }
}

Windows(C++)

Basically, similar to the MacOS one, using the flutterViewController to get the binary messenger. Be aware to import the header files.

#include "flutter_window.h"
#include <flutter/event_channel.h>
#include <flutter/event_sink.h>
#include <flutter/event_stream_handler_functions.h>
#include <flutter/method_channel.h>
#include <flutter/standard_method_codec.h>

#include <optional>

#include "flutter/generated_plugin_registrant.h"

bool FlutterWindow::OnCreate() {
  if (!Win32Window::OnCreate()) {
    return false;
  }

  RECT frame = GetClientArea();

  // The size here must match the window dimensions to avoid unnecessary surface
  // creation / destruction in the startup path.
  flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
      frame.right - frame.left, frame.bottom - frame.top, project_);
  // Ensure that basic setup of the controller was successful.
  if (!flutter_controller_->engine() || !flutter_controller_->view()) {
    return false;
  }
  RegisterPlugins(flutter_controller_->engine());

  // TODO:[Windows] [1] use flutter_controller get engin and then messenger
  flutter::MethodChannel<> channel(
      flutter_controller_->engine()->messenger(), "cbl.tool.flutter_platform_channel",
      &flutter::StandardMethodCodec::GetInstance());
  channel.SetMethodCallHandler(
      [](const flutter::MethodCall<>& call,
          std::unique_ptr<flutter::MethodResult<>> result) {
              if (call.method_name() == "shakeHand") {
                  result->Success(flutter::EncodableValue("Hi from Windows!"));
              }
              else {
                  result->NotImplemented();
              }
      });

  SetChildContent(flutter_controller_->view()->GetNativeWindow());
  return true;
}

Linux(C)

For Linux, use flutter engine to get the binary messenger and then create a new method channel. The method_call_cb is to handle the method call call back.

// TODO: [Linux][5] add handling to each method name
static void method_call_cb(FlMethodChannel* channel,
                           FlMethodCall* method_call,
                           gpointer user_data)
{
  g_autoptr(FlMethodResponse) response = nullptr;

  const gchar* method = fl_method_call_get_name(method_call);
  if (strcmp(method, "shakeHand") == 0) {
    response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_string("Hi from Linux!")));
  } else {   
    response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
  }
  fl_method_call_respond(method_call, response, nullptr);
}

// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
  ...

  fl_register_plugins(FL_PLUGIN_REGISTRY(view));

  // TODO: [Linux][1] Get engine from view
  FlEngine *engine = fl_view_get_engine(view);  
  // TODO: [Linux][2] Get binary messenger
  g_autoptr(FlBinaryMessenger) messenger = fl_engine_get_binary_messenger(engine);
  // TODO: [Linux][3] Set channel
  g_autoptr(FlMethodChannel) channel =
      fl_method_channel_new(messenger,
                            "cbl.tool.flutter_platform_channel",  // this is our channel name
                            FL_METHOD_CODEC(fl_standard_method_codec_new()));
  // TODO: [Linux][4] Set channel call handler
  fl_method_channel_set_method_call_handler(channel, 
                            method_call_cb, g_object_ref(view), g_object_unref);

  gtk_widget_grab_focus(GTK_WIDGET(view));
}

Value Mapping

Since the method channel uses the standard message codec which supports serialise and deserialise binary message to different types. This the table for the types mapping:

DartKotlinJavaSwiftObjective-CC++C
nullnullnullnilnil (NSNull when nested)EncodableValue()FlValue()
boolBooleanjava.lang.BooleanNSNumber(value: Bool)NSNumber numberWithBool:EncodableValue(bool)FlValue(bool)
intIntjava.lang.IntegerNSNumber(value: Int32)NSNumber numberWithInt:EncodableValue(int32_t)FlValue(int62_t)
int, if 32 bits not enoughLongjava.lang.LongNSNumber(value: Int)NSNumber numberWithLong:EncodableValue(int64_t)FlValue(double)
doubleDoublejava.lang.DoubleNSNumber(value: Double)NSNumber numberWithDouble:EncodableValue(double)FlValue(gchar*)
StringStringjava.lang.StringStringNSStringEncodableValue(std::string)FlValue(uint8_t*)
Uint8ListByteArraybyte[]FlutterStandardTypedData(bytes: Data)FlutterStandardTypedData typedDataWithBytes:EncodableValue(std::vector)FlValue(int32_t*)
Int32ListIntArrayint[]FlutterStandardTypedData(int32: Data)FlutterStandardTypedData typedDataWithInt32:EncodableValue(std::vector)FlValue(int64_t*)
Int64ListLongArraylong[]FlutterStandardTypedData(int64: Data)FlutterStandardTypedData typedDataWithInt64:EncodableValue(std::vector)FlValue(int64_t*)
Float32ListFloatArrayfloat[]FlutterStandardTypedData(float32: Data)FlutterStandardTypedData typedDataWithFloat32:EncodableValue(std::vector)FlValue(float*)
Float64ListDoubleArraydouble[]FlutterStandardTypedData(float64: Data)FlutterStandardTypedData typedDataWithFloat64:EncodableValue(std::vector)FlValue(double*)
ListListjava.util.ArrayListArrayNSArrayEncodableValue(std::vector)FlValue(FlValue)
MapHashMapjava.util.HashMapDictionaryNSArrayEncodableValue(std::map)FlValue(FlValue, FlValue)

Demo Project

Example Project on GitHub

Reference

flutter.dev: Writing custom platform-specific code

url_launcher repo for different platform implementation reference