Photo by Pau Sayrol on Unsplash

Hello again! Today I will continue my Flutter Linux journey and we will touch Linux integration again. Last time we managed to setup Visual Studio Code for plugin development, today we will take a closer look at Method Channels.

If you want to create a plugin with Method Channel support it’s quite easy, just generate a Flutter project from template using flutter create -t plugin --platforms=linux <name of your project>. However, what if you don’t want to create a separate plugin and just add some custom code to your Flutter Linux application? I’ve found out it’s not straightforward so I thought that I will write this article so you won’t have to figure it out by yourself.

To not prolong it any longer, let’s just get started.

Create method channel

First we need to create codec, binary messenger and channel. Next we assign a method call back to our custom method which we will create in the next step.

To do so let’s open my_application.cc inside linux folder and navigate to my_application_activate function. Next we implement objects described above after plugins initialization.

In the example below, name_of_our_channel is the method which we are calling from Dart code.

static void my_application_activate(GApplication *application)
{

  ...
  // Other standard code for our application

  fl_register_plugins(FL_PLUGIN_REGISTRY(view));

  // START OF OUR CUSTOM  BLOCK

  // Get engine from view
  FlEngine *engine = fl_view_get_engine(view);

  g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
  g_autoptr(FlBinaryMessenger) messenger = fl_engine_get_binary_messenger(engine);
  g_autoptr(FlMethodChannel) channel =
      fl_method_channel_new(messenger,
                            "name_of_our_channel",  // this is our channel name
                            FL_METHOD_CODEC(codec));
  fl_method_channel_set_method_call_handler(channel, 
  // Method which will be called when we call invokeMethod() from dart
                                            method_call_cb,  
                                            g_object_ref(view),
                                            g_object_unref);

  fl_method_channel_set_method_call_handler(channel, 
                            method_call_cb, g_object_ref(view), g_object_unref);

  // END OF OUR CUSTOM BLOCK

  gtk_widget_grab_focus(GTK_WIDGET(view));
}

Callback function

Now let’s create callback function:

static void method_call_cb(FlMethodChannel *channel,
                           FlMethodCall *method_call,
                           gpointer user_data) {}

Pretty straightforward, we pass a channel, methodcall and some user data.

To check for the channel’s method name we need to use the fl_method_call_get_name function on method_call object. And compare it with strcmp like so:

static void method_call_cb(FlMethodChannel *channel,
                           FlMethodCall *method_call,
                           gpointer user_data)
{
  const gchar *method = fl_method_call_get_name(method_call);
  if (strcmp(method, "my_method") == 0)
  {
      // Code for "my_method"
  }
}

Method not implemented response

If method passed to channel does not exist, we need to return not implemented result to do this we need to call fl_method_call_respond

static void method_call_cb(FlMethodChannel *channel,
                           FlMethodCall *method_call,
                           gpointer user_data)
{
  const gchar *method = fl_method_call_get_name(method_call);
  if (strcmp(method, "my_method") == 0) {
      // Code for "my_method"
  } else {
    // Create response
    g_autoptr(FlMethodResponse) response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());

    // Create error, in this case null
    g_autoptr(GError) error = nullptr;

    // Send response back to dart
    fl_method_call_respond(method_call, response, &error);

  }
}

Error handling

Before we jump into arguments and custom result, let’s quickly look at error handling.

Fortunately for us, it’s very similar to not implemented method:

static void method_call_cb(FlMethodChannel *channel,
                           FlMethodCall *method_call,
                           gpointer user_data)
{
  const gchar *method = fl_method_call_get_name(method_call);
  if (strcmp(method, "my_method") == 0) {

    // An error has occurred
    g_autoptr(FlMethodResponse) response = FL_METHOD_RESPONSE(fl_method_error_response_new(
           "error_id", "Custom message", nullptr));
    
    // Create error, in this case null
    g_autoptr(GError) error = nullptr;

    // Send response back to dart
    fl_method_call_respond(method_call, response, &error);

  } else {
      // Previous not implemented method result
  }
}

We can see it’s almost identical, we just create the result with fl_method_error_response_new instead of fl_method_not_implemented_response_new.

Fetching Dart arguments

Alright, now it’s time to write what we wanted to write from the beginning. Let’s assume that we want to send data from Dart to C++, to do that we just send a map from Dart side, but how to fetch it?

To do so, we need to call fl_method_call_get_args(FlMethodCall) which returns a pointer to FlValue.

Next we check if returned value is the proper type:

static void method_call_cb(FlMethodChannel *channel,
                           FlMethodCall *method_call,
                           gpointer user_data)
{
  const gchar *method = fl_method_call_get_name(method_call);
  if (strcmp(method, "my_method") == 0)
  {
    // Get Dart arguments
    FlValue* args = fl_method_call_get_args(method_call);
    // Fetch string value named "name"
    FlValue *text_value = fl_value_lookup_string(args, "name");

    // Check if returned value is either null or string
    if (text_value == nullptr ||
        fl_value_get_type(text_value) != FL_VALUE_TYPE_STRING)
    {

      // Return error 
      g_autoptr(FlMethodResponse) response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
    
      // Create error, in this case null
      g_autoptr(GError) error = nullptr;

      // Send response back to dart
      fl_method_call_respond(method_call, response, &error);
    } else {
        // Custom logic
    }

  }
}

In example above we lookup for string, but there are other ones like int, float, bool, map. For complete list checkout Flutter Engine Documentation

Same goes for type check, to check whole list of enum go to Documentation

Returning some value

We’re almost done, we managed to handle not implemented method, errors, and getting arguments from method. What’s left is returning values back to Dart.

Fortunately for us it’s very similar to what we’ve already done, we just need to create FlMethodResponse and put FlValue inside. Here is an example:

static void method_call_cb(FlMethodChannel *channel,
                           FlMethodCall *method_call,
                           gpointer user_data)
{
  const gchar *method = fl_method_call_get_name(method_call);
  if (strcmp(method, "my_method") == 0)
  {

    // Fetch string value named "name"
    FlValue *text_value = fl_value_lookup_string(args, "name");

    // Check if returned value is either null or string
    if (text_value == nullptr ||
        fl_value_get_type(text_value) != FL_VALUE_TYPE_STRING)
    {
        // Previous error handling
    } else {


        // Create response
        FlValue *res = fl_value_new_string("Response from Linux");

        // Send it back to Dart
        g_autoptr(FlMethodResponse) response = FL_METHOD_RESPONSE(fl_method_success_response_new(res));
    }

  }
}

Like before here is link to documentation for more value creation function

VSCode code completion + debugging

I think I should give you some reward for coming this far, so I decided to write setup for code completion and debugging in Visual Studio Code.

Before you start, C++ and Cmake plugins must be installed.

First let’s setup code completion. To do that create file named c_cpp_properties.json inside .vscode folder in the root of your project and put this config inside:

{
  "configurations": [
    {
      "name": "Linux",
      "includePath": [
        "${workspaceFolder}/**"
      ],
      "defines": [],
      "compilerPath": "/usr/bin/clang",
      "cStandard": "c17",
      "cppStandard": "c++14",
      "intelliSenseMode": "linux-clang-x64",
      "compileCommands": "${workspaceFolder}/build/compile_commands.json",
      "configurationProvider": "ms-vscode.cmake-tools"
    }
  ],
  "version": 4
}

Check your compiler path (Flutter uses Clang) and adjust C/C++ standards if need.

To setup debugging we need to create launch configuration inside launch.json in the same .vscode folder. Let’s take a look at the config:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Debug native",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/build/linux/x64/debug/bundle/hello_world",   // ADJUST YOUR BINARY NAME HERE
            "cwd": "${workspaceFolder}"
        }
    ]
}

Pretty straightforward but you need to change your binary name. Also, be aware that to make it work you need to build your flutter project with flutter run.

Closing

Thank you for reading, hopefully you will find it useful.

Happy Coding!

Complete example can be found here.