Flutter platform channels for Linux

Introduction

This article is for those who use platform channels for Linux with Flutter. I will write how to integrate Dart with the Linux platform. I will not write the procedure for building the flutter development environment, the procedure for publishing as a license or package, etc.

The Linux platform in Flutter is currently for Desktop, Flutter for Desktop is treated as an alpha version at the time of writing this article (2020/11/3). Please note that the content described in this article, especially below linux /, may change significantly in the future.

Because of such circumstances, I intended to write with an awareness of "how to find out how to make". If you notice any mistakes or outdated parts, we would appreciate it if you could point them out.

We have confirmed in the following environment.

$ uname -a
Linux chama 5.5.8 #1 SMP Sat Mar 7 22:29:22 JST 2020 x86_64 x86_64 x86_64 GNU/Linux
$ lsb_release -d
Description:    Ubuntu 18.04.5 LTS
$ flutter --version
Flutter 1.24.0-7.0.pre.42 • channel master • https://github.com/flutter/flutter
Framework • revision c0ef94780c (2 days ago) • 2020-10-31 03:12:27 -0700
Engine • revision 3460519398
Tools • Dart 2.12.0 (build 2.12.0-3.0.dev)

Platform Channels Overview

When calling platform-specific APIs in Flutter, we use a mechanism called Platform Channels.

Platform Channels allows you to flexibly work with various platforms It is a simple asynchronous message passing mechanism.

If it is only asynchronous, the code on the side that uses the API seems to be difficult to understand, but Dart has async / await, which makes it flexible and simple to implement without compromising readability.

Platform Channels provides the following three APIs according to the purpose. This article is easy to understand about how to use these properly and how they are used.

--[MethodChannel] to implement function call (https://api.flutter.dev/flutter/services/MethodChannel-class.html) --[EventChannel] to realize event distribution (https://api.flutter.dev/flutter/services/EventChannel-class.html) --[BasicMessageChannel] for more general-purpose message passing (https://api.flutter.dev/flutter/services/BasicMessageChannel-class.html)

First, make a template

Create a flutter create command to create a project. At this time, you can create a project from the plugin development template by specifying --template plugin. For --org, specify the organization Reverse domain name notation. Create a plugin for linux platform with the name "platform_proxy" with the following command.

flutter create --platforms linux --template plugin --org xyz.takeoverjp.example platform_proxy

It's not the main point, but it is recommended to do a git commit at this point so that you can see what you have tampered with.

Try running the sample app

In the generated platform_proxy, there is also a sample application called ʻexample. When you run the sample app, the following screen will be displayed. If you have touched Linux, it may come from a string, but it is a sample that outputs the result of ʻuname -v.

Screenshot from 2020-11-03 02-21-48.png

Read the generated code

You can understand the call flow by paying attention to the following three of the generated code.

Also, when developing the Plugin API, we will also modify the unit tests, so we will introduce them here as well.

Plugin API caller

platform_proxy/example/lib/main.dart


class _MyAppState extends State<MyApp> {
  String _platformVersion = 'Unknown';

  @override
  void initState() {
    super.initState();
    initPlatformState();
  }

  // Platform messages are asynchronous, so we initialize in an async method.
  Future<void> initPlatformState() async {
    String platformVersion;
    // Platform messages may fail, so we use a try/catch PlatformException.
    try {
      platformVersion = await PlatformProxy.platformVersion;
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
    }

    // If the widget was removed from the tree while the asynchronous platform
    // message was in flight, we want to discard the reply rather than calling
    // setState to update our non-existent appearance.
    if (!mounted) return;

    setState(() {
      _platformVersion = platformVersion;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: Center(
          child: Text('Running on: $_platformVersion\n'),
        ),
      ),
    );
  }
}

PlatformProxy.platformVersion is the API added by template. As you can see in the comments, PlatformChannel is an asynchronous message. Since the MethodChannel used this time is also an asynchronous call, async / await is used. For Dart's async / await, this article is easy to understand. Simply put, it's the syntax for receiving a response to an asynchronous call in the next event loop.

Plugin API implementation (Dart part)

platform_proxy/lib/platform_proxy.dart


class PlatformProxy {
  static const MethodChannel _channel =
      const MethodChannel('platform_proxy');

  static Future<String> get platformVersion async {
    final String version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }
}

An implementation of PlatformProxy # platformVersion. It creates a MethodChannel for the platform_proxy and calls the MethodChannel # invokeMethod.

Here, If the channel names overlap, the handler will be overwritten and communication will not be possible correctly. I couldn't find it at home, but on this page, (domain name) / (plugin name) / (channel name) Is recommended. In template, as mentioned above, MethodChannel is generated with direct channel name, Don't forget to change it, especially for plugins, as there is a high risk of name collisions. For example, in Flutter official file_chooser plugin, as flutter / filechooser is.

MethodChannel # invokeMethod Pass the method name and arguments you want to call and call them asynchronously. Also, since the response is Future, you can wait for the result with await.

By the way, the Method name passed to ʻinvokeMethod` is just a string. Even if the caller typo the Method name, it will not result in a compile error but a runtime exception. Even if the type or property name is incorrect, it cannot be detected at compile time. It is the responsibility of this layer to make the plugin API more readable while preventing such misuse.

Plugin API implementation (Native part)

platform_proxy/linux/platform_proxy_plugin.cc


static void platform_proxy_plugin_handle_method_call(
    PlatformProxyPlugin* self,
    FlMethodCall* method_call) {
  g_autoptr(FlMethodResponse) response = nullptr;

  const gchar* method = fl_method_call_get_name(method_call);

  if (strcmp(method, "getPlatformVersion") == 0) {
    struct utsname uname_data = {};
    uname(&uname_data);
    g_autofree gchar *version = g_strdup_printf("Linux %s", uname_data.version);
    g_autoptr(FlValue) result = fl_value_new_string(version);
    response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));
  } else {
    response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
  }

  fl_method_call_respond(method_call, response, nullptr);
}
...
static void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call,
                           gpointer user_data) {
  PlatformProxyPlugin* plugin = PLATFORM_PROXY_PLUGIN(user_data);
  platform_proxy_plugin_handle_method_call(plugin, method_call);
}

void platform_proxy_plugin_register_with_registrar(FlPluginRegistrar* registrar) {
  PlatformProxyPlugin* plugin = PLATFORM_PROXY_PLUGIN(
      g_object_new(platform_proxy_plugin_get_type(), nullptr));

  g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
  g_autoptr(FlMethodChannel) channel =
      fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar),
                            "platform_proxy",
                            FL_METHOD_CODEC(codec));
  fl_method_channel_set_method_call_handler(channel, method_call_cb,
                                            g_object_ref(plugin),
                                            g_object_unref);

  g_object_unref(plugin);
}

Flutter for Desktop Linux implements the event loop using GLib. I'm not used to GLib, so I was taken aback at first, GLib functions (g_ ***) are mostly code for object management, so don't worry about it. It's easy to understand if you pay attention to the flutter function (fl_ ***).

To the FlMethodChannel generated by fl_method_channel_new when registering the plugin The callback function is registered with fl_method_channel_set_method_call_handler.

When the target API is called on the Dart side, the Flutter Engine calls method_call_cb and Call platform_proxy_plugin_handle_method_call with the arguments cast appropriately.

Then, get the method name with fl_method_call_get_name and perform the corresponding processing. In this getPlatformVersion, ʻuname (2)is called. Set the result as the return value withFL_METHOD_RESPONSE (fl_method_success_response_new (result)) It is returning a response to the Flutter Engine by callingfl_method_call_respond`.

In addition, communication of platform channels is performed by the main thread is required. If you calculated the result in another thread, move the context to the main thread using g_main_context_invoke etc. and then return the response.

Unit test of Plugin API

You can run a unit test of the Plugin API by running flutter test. MethodChannel has a method for DI called setMockMethodCallHandler. Unit testing is possible with the Native part separated. On the contrary, it is not a test of the implementation of the Native part, so it must be done separately.

test/plugin_method_test.dart


void main() {
  const MethodChannel channel = MethodChannel('platform_proxy');

  TestWidgetsFlutterBinding.ensureInitialized();

  setUp(() {
    channel.setMockMethodCallHandler((MethodCall methodCall) async {
      return '42';
    });
  });

  tearDown(() {
    channel.setMockMethodCallHandler(null);
  });

  test('getPlatformVersion', () async {
    expect(await PlatformProxy.platformVersion, '42');
  });
}

Add API

When calling platform functions from Dart in BasicMessageChannel You can add API by adding N to the above 3 places of code.

If you want to return another type or take an argument sample and [header comment](https://github.com/flutter/engine/tree/master/shell/platform/linux/public/ flutter_linux) will be helpful.

Also, please refer to the other variations implemented in the sample project below. takeoverjp/flutter-linux-plugin-example: A plugin example for platform channels on Linux desktop.

--Call Dart functions from platform on MethodChannel --Event notification from platform on Event Channel --Call platform functions from Dart in BasicMessageChannel

[Reference] About the automatically generated package Pigeon

By the way, in the above method, the method name is treated as a simple character string, The type for the argument and return value for each method is a gentlemen's agreement. If this deviates between the Dart part and the Native part, it will be an exception or the worst undefined behavior. If you always access it via a plugin and test it properly, you can reduce the possibility of actual problems, but you still have anxiety.

As a countermeasure, a package for automatic generation called pigeon is prepared. From a Pigeon file written in a subset of Dart, we will generate the code to communicate between the platform and Dart as described above.

However, desktop is not yet supported at 0.1.14 at the time of writing the article (2020/11/3). It is still available as a tool to generate an implementation of the Dart part. Matching the symbol with the Native part is an important element, so it will be a little disappointing usage, but I will leave the method for reference.

The flow of automatic generation is as follows. API definition file and Generated Dart code If you also look at /takeoverjp/flutter-linux-plugin-example/blob/master/lib/pigeon_platform_proxy.dart), it may be easier to grasp the image.

  1. Add pigeon to dev_dependencies in pubspec.yaml
  2. Prepare a dart file that defines the API prototype declaration and required types.
  3. Generate the Dart part with the following command
flutter pub run pigeon --input pigeons/platform_proxy_message.dart --dart_out lib/pigeon_platform_proxy.dart

After that, the Native part will be implemented according to the generated dart file. Since the generated dart file uses BasicMessageChannel, it is necessary to match the Native part as well.

As a constraint of API created by Pigeon (0.1.14), the argument and return value must be the class defined in one or less API definition files. That is, it does not support taking an int as an argument, taking multiple values as an argument, or returning an int. As a result, it always goes through the class, so both the passer and the receiver are a little redundant. Also, it didn't seem to support event notifications and method calls from the platform side yet.

in conclusion

I tried to see how to use platform channels on Linux Desktop based on the code generated as a plugin template. The platform channels themselves are simple, but it is quite a big impression because it is necessary to implement both the Dart part and the Native part.

If you are dealing with large data, it is disadvantageous to use platform channels that serialize, so I think it is better to use ffi.

On the other hand, if what you want to do is IPC and the communication partner supports unix domain socket / dbus / grpc, etc. In the first place, you may be able to achieve your goal by using dart: io / dbus.dart / grpc-dart directly from Dart.

With that in mind, for Android / iOS, platform channels are required to use the java / kotlin / Obj-C / swift platfrom API, If you think only about Linux, it may not come into play very much.

It was around this time that I had to know each method properly so that I could choose the method that suits my situation. .. ..

reference

Recommended Posts

Flutter platform channels for Linux
pyenv for linux
[For memo] Linux Part 2
What is Linux for?
Linux command for self-collection
Linux Kernel Build for DE10nano
Platform Channel VS FFI (Foreign Function Interface) on Flutter on Linux
Linux distribution recommended for beginners
[Linux] [Initial Settings] [Flutter] Summary
Linux Command Dictionary (for myself)
Linux command memorandum [for beginners]
Convenient Linux shortcuts (for beginners)
Create Scratch Offline Editor for Linux
Convenient shortcut keys for Linux commands! !! !!
Frequently used Linux commands (for beginners)
[Must-see for beginners] Basics of Linux
Teamviewer for Linux installation procedure (CentOS)
pykintone on Windows Subsystem for Linux
Linux Basic Education for Front-end Engineer
Use Azure AD for Linux authentication