【Flutter】様々なプラットフォームのMethodChannel実装方法

# はじめに

Flutterでプラットフォームとやりとりする方法の1つとして`MethodChannel`を使う方法があります。  
ここでは、その`MethodChannel`を各プラットフォームごとにどのように実装するかをまとめてみました。  

■本記事でわかることは...
+ Flutterとプラットフォーム間で複数データをやりとりする方法
+ 各プラットフォームへの実装方法

■本記事で分からないことは...  
+ APIの詳細

■今回対象とするのは以下5つのプラットフォーム。

|プラネットフォーム|開発言語|
|---|---|
|Android|Kotolin|
|iOS|Swift|
|macOS|Swift|
|Windows|C++|
|Linux|C++|

# 事前説明

## 環境

VSCodeでFlutterプロジェクトを作成して出来たテンプレートをもとに紹介していきます。  
(Flutter開発用の拡張機能ある前提 : Flutter、Dart)　　  

手順を踏めばこのような構造のプロジェクトが作成されているはずです。  
【手順】「VSCodeのコマンドパレットを開く」>「Flutter:NewProject」>「Application」  

<img width="360" alt="Project" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2309969/815c7570-1bfd-c45f-841a-275f0aeafc35.png">


__バージョン情報__

+ Flutter : 3.0.3
+ VSCode : 1.74.3
+ Android SDK : 30.0.3
+ Android API26(Androiudエミュレータ)
+ MacOS : 12.6.2
+ Xcode : 14.2
+ Windows10
+ Ubuntu22.04

## 作るものについて

全プラットフォームで同じことができることを示すために作るものは共通のものとなります。  

Flutter公式の[サンプル(バッテリー残量取得)](https://docs.flutter.dev/development/platform-integration/platform-channels)を改造したものを今回は紹介させていただきます。

アプリ動作
1. Flutter側 : 画面のバッテリーボタンを押下
1. Flutter側 : プラットフォームへデータを渡して関数呼び出し
1. プラットフォーム側 : Flutterから渡されたデータを解析して文字列作成
1. プラットフォーム側 : 各プラットフォームにてバッテリー残量を取得
1. プラットフォーム側 : プラットフォームからデータを結果に入れてFlutterへ返す
1. Flutter側 : 画面に結果を表示

<img width="460" alt="demo" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2309969/a51d92b0-4414-200c-2506-5bf93a5f6bac.png">




## 前提条件

■MethodChannelでやりとりするための共通設定

+ チャンネル : `platform_method/battery`
+ メソッド : `getBatteryInfo`

■Flutterから送るデータ

+ `text`(文字列)
+ `num`(数値)

■プラットフォームから返ってくるデータ

+ `device`(文字列) : プラットフォーム名
+ `level`(数値) : バッテリー残量%
+ `message`(文字列) : 表示文字列(textの前後に特定文字をnum回分追加した文字列)

# Flutter側

## ソースコード

```dart:lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        textTheme: const TextTheme(bodyText2: TextStyle(fontSize: 32)),
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  /* @POINT1 : メソッドチャンネルを作成 */
  static const batteryChannel = MethodChannel('platform_method/battery');
  // バッテリーの残量
  String batteryLevel = 'Waiting...';
  String message = "";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
          child: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[Text(batteryLevel), Text(message)])),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          await getBatteryInfo(); // バッテリー情報を取得
        },
        tooltip: 'Get Battery Level',
        child: const Icon(Icons.battery_full),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

  Future getBatteryInfo() async {
    String devName = "Not...";
    int level = 0;
    String devMessage = "ERROR";

    try {
      /* @POINT2 : パラメータを作成して、プラットフォームの関数呼び出し */
      final arguments = {
        'text': "Flutter",
        'num': 5,
      };
      final res =
          await batteryChannel.invokeMethod('getBatteryInfo', arguments);

      /* @POINT3 : プラットフォームからの結果を解析取得 */
      devName = res["device"];
      level = res["level"];
      devMessage = res["message"];
    } on PlatformException catch (e) {
      batteryLevel = "Failed to get battery level: '${e.message}'.";
    }

    // 画面を再描画する
    setState(() {
      batteryLevel = '$devName:$level%';
      message = '$devMessage';
    });
  }
}
```


## 詳細

やってること...

+ フロートボタンを押されたら`getBatteryInfo()`を呼び出し
+ `getBatteryInfo()`の中でプラットフォームからデータを取得して画面表示更新

#### チャンネルを作成

```dart
static const batteryChannel = MethodChannel('platform_method/battery');
```

`MethodChannel(チャンネル名)`で使用チャンネルを用意する。    

#### プラットフォーム処理呼び出し

```dart
final arguments = {
    'text': "Flutter",
    'num': 5,
};
final res =
      await batteryChannel.invokeMethod('getBatteryInfo', arguments);
```

Flutterから複数データを渡すときはMap型を使う。 　　
(本コードでは`<String, dynamic>`型)

`MethodChannel.invokeMethod(メソッド名、パラメータ)`で、プラットフォーム処理を指定し、パラメータには先ほど用意したMapを渡す。  


#### プラットフォーム結果を取得

```dart
final res =
    await batteryChannel.invokeMethod('getBatteryInfo', arguments);

devName = res["device"];
level = res["level"];
devMessage = res["message"];
```

プラットフォームでの実行結果は`invokeMethod`の返り値に入る。

今回は複数データをやりとりするしているため、プラットフォーム側からの結果をMapとして受け取りデータを取り出している。  
(補足)`res`のデータ側を調べてみると`_InternalLinkedHashMap<Object?, Object?>`となっている模様。

# プラットフォーム側

## Android

### ソースコードと動作結果

```kotlin:android/app/src/main/kotlin/com/example/all_sdk/MainActivity.kt
package com.example.platform_method

// MethodChannelのため
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodCall

// バッテリー取得のため
import android.content.Context
import android.os.BatteryManager

class MainActivity: FlutterActivity() {

    /*===== ここから =====*/
    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        /* @POINT1 チャンネルとハンドラコールバック登録 */
        val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "platform_method/battery")
        channel.setMethodCallHandler { methodCall: MethodCall, result: MethodChannel.Result ->

            if (methodCall.method == "getBatteryInfo") {

                /* @POINT2 : Flutterから渡されたデータを解析取得 */
                val text = methodCall.argument<String>("text") ?: "Not Message..."
                val num = methodCall.argument<Int>("num") ?: 0
                val message = makeMessage(text, num)

                // バッテリー残量を取得
                val level = getBatteryLevel()

                /* @POINT3 : Flutterへ返す情報を作成 */
                val res =  mapOf(
                    "device" to "Android",
                    "level" to level,
                    "message" to message,
                )
                result.success(res)

            }
            else {
                result.notImplemented()
            }
        }
    }
    /*===== ここまで =====*/
    
    /* メッセージ作成 */
    // Androidからはtextの前後に「-」をnum回数分付与
    private fun makeMessage(text:String, num:Int) : String {
        val mark = "-"   
        var message = ""
        for (i in 1..num) {
            message += mark
        } 
        message += text
        for (i in 1..num) {
            message += mark
        }
        return message
    }

    /* バッテリー残量を取得 */
    private fun getBatteryLevel() :Int {
        val manager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
        val batteryLevel : Int = manager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
        return batteryLevel
    }
}
```

Androidでの動作結果
<img width="360" alt="Android" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2309969/f38550bd-485c-e942-e060-6e225d5b301b.png>

### 詳細

#### MethodChannelの実装場所

VSCodeで自動作成した場合は未定義なので、以下を`MainActivity`に追加。  

```kotlin
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine){}
```

そして、Flutterからの受信を行うMethodChannelは以下の場所に実装していく。
```kotlin
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)

    /****** この中に実装していきます ******/
}
```

#### MethodChannelの準備

```kotlin
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "platform_method/battery")
channel.setMethodCallHandler { methodCall: MethodCall, result: MethodChannel.Result ->
    // Flutterから指定チャネルが呼ばれたらここが呼ばれる
    if (methodCall.method == "getBatteryInfo") {
        // メソッド名「getBatteryInfo」が指定されていればここの処理が実行できる
    }
}
```

`MethodChannel`を作成して、`setMethodCallHandler`の中に呼ばれた時の制御を書いていく。  
Flutterから指定されるメソッドは`MethodCall`の`method`で取得。

#### Flutterから渡されたデータを取得

```kotlin
val text = methodCall.argument<String>("text") ?: "Not Message..."
val num = methodCall.argument<Int>("num") ?: 0
```

Flutterからのデータは`MethodCall`の`argument`に入っており、キー名とデータ型を指定して取得。  
このとき取得データ型はNullable(?がつくデータ型)になる。

#### Flutterへ返すデータを作成して結果通知

```kotlin
val res =  mapOf(
    "device" to "Android",
    "level" to level,
    "message" to message,
)
result.success(res)
```

Androidではコレクションで複数データを返すことができ、今回はMapを使う。  
結果は`result.success(通知する結果)`で返せる。

## iOS

### ソースコードと動作結果

```swift:ios/Runner/AppDelegate.swift
import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {

    /*===== ここから =====*/

    /* @POINT1 チャンネルとハンドラコールバック登録 */

    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let batteryChannel = FlutterMethodChannel(name:  "platform_method/battery", binaryMessenger: controller as! FlutterBinaryMessenger)
    batteryChannel.setMethodCallHandler({
      (methodCall: FlutterMethodCall, result: @escaping FlutterResult) -> Void in

        if (methodCall.method == "getBatteryInfo") {

          /* @POINT2 : Flutterから渡されたデータを解析取得 */
          var message = "Not Message...";
          if let args = methodCall.arguments as? Dictionary<String, Any> {
            if let text = args["text"] as? String,
              let num = args["num"] as? Int {
                message = self.makeMessage(text:text,num:num)
            }
          }

          // バッテリー残量を取得
          let level = self.getBatteryLevel()

          /* @POINT3 : Flutterへ返す情報を作成 */
          let res = [
              "device": "iOS",
              "level": level,
              "message": message,
          ]
          result(res)

        } else {
            result(FlutterMethodNotImplemented)
        }
    })

    /*===== ここまで =====*/
      
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
  
  /* メッセージ作成 */
  private func makeMessage(text:String, num:Int) -> String {
    let mark = "#"
    var message = ""
    for _ in 1...num {
      message += mark
    } 
    message += text
    for _ in 1...num {
      message += mark
    }
    return message
  }

  /* バッテリー残量を取得 */
  private func getBatteryLevel() -> Int {
    let device = UIDevice.current
    device.isBatteryMonitoringEnabled = true
    
    if (device.batteryState == UIDevice.BatteryState.unknown) {
      return -1
    } else {
      return Int(device.batteryLevel * 100)
    }
  }
}
```

iOSでの動作結果  
※iOSシミュレータだとバッテリー情報は取れないのでバッテリー残量が-1になってます

<img width="360" alt="iOS" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2309969/f51daea3-5a71-f7a7-3973-df35a4601343.png>

### 詳細

#### MethodChannelの実装場所

VSCodeで自動作成した場合は既に以下関数が存在しています。
```swift
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {}
```
そして、Flutterからの受信を行うMethodChannelは以下の場所に実装していきます。
```swift
override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {

        /****** この中に実装していきます ******/
    
    }
  
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
```

#### MethodChannelの準備

```swift
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let batteryChannel = FlutterMethodChannel(name:  "platform_method/battery", binaryMessenger: controller as! FlutterBinaryMessenger)
batteryChannel.setMethodCallHandler({
      (methodCall: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
    // Flutterから指定チャネルが呼ばれたらここが呼ばれる
    if (methodCall.method == "getBatteryInfo") {
        // メソッド名「getBatteryInfo」が指定されていればここの処理が実行できる
    }
})
```

`FlutterMethodChannel`を作成して、`setMethodCallHandler`の中に呼ばれた時の制御を書いていく。  
Flutterから指定されるメソッドはFlutterMethodCallの`method`で取得。


#### Flutterから渡されたデータを取得

```swift
/* @POINT2 : Flutterから渡されたデータを解析取得 */
var message = "Not Message...";
if let args = methodCall.arguments as? Dictionary<String, Any> {
  if let text = args["text"] as? String,
     let num = args["num"] as? Int {
      message = self.makeMessage(text:text,num:num)
  }
}
```

FlutterからのデータはMethodCallの`arguments`に入っており、`Dictionary<String, Any>`として取り出せる。  
後はDictionaryにキー名を指定することでバリューを取得。

#### Flutterへ返すデータを作成して結果通知

```swift
/* @POINT3 : Flutterへ返す情報を作成 */
let res = [
  "device": "iOS",
  "level": level,
  "message": message,
]
result(res)
```

iOSではmapを使うことで複数データを用意することが可能。  
結果は`result(通知する結果)`で返せる。  

## macOS

### ソースコードと動作結果

```swift:macos/Runner/MainFlutterWindow.swift
import Cocoa
import FlutterMacOS
import IOKit.ps   // バッテリー残量取得のため

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

    /*===== ここから =====*/

    /* @POINT1 チャンネルとハンドラコールバック登録 */
    let batteryChannel = FlutterMethodChannel(name: "platform_method/battery", binaryMessenger: flutterViewController.engine as! FlutterBinaryMessenger)
    batteryChannel.setMethodCallHandler({
      (methodCall: FlutterMethodCall, result: @escaping FlutterResult) -> Void in

        if (methodCall.method == "getBatteryInfo") {

          /* @POINT2 : Flutterから渡されたデータを解析取得 */
          var message = "Not Message...";
          if let args = methodCall.arguments as? Dictionary<String, Any> {
            if let text = args["text"] as? String,
              let num = args["num"] as? Int {
                message = self.makeMessage(text:text,num:num)
            }
          }

          // バッテリー残量を取得
          let level = self.getBatteryLevel()

          /* @POINT3 : Flutterへ返す情報を作成 */
          let res = [
              "device": "macOS",
              "level": level,
              "message": message,
          ]
          result(res)

        } else {
            result(FlutterMethodNotImplemented)
        }
    })

    /*===== ここまで =====*/

    RegisterGeneratedPlugins(registry: flutterViewController)

    super.awakeFromNib()
  }

  /* メッセージ作成 */
  private func makeMessage(text:String, num:Int) -> String {
    let mark = "@"
    var message = ""
    for _ in 1...num {
      message += mark
    } 
    message += text
    for _ in 1...num {
      message += mark
    }
    return message
  }

  /* バッテリー残量を取得 */
  private func getBatteryLevel() -> Int {

    guard let snapshot = IOPSCopyPowerSourcesInfo()?.takeRetainedValue() 
    else { 
      return -1
    }

    guard let sources: NSArray = IOPSCopyPowerSourcesList(snapshot)?.takeRetainedValue()
    else { 
      return -1
    }

    for ps in sources {
      guard let info: NSDictionary = IOPSGetPowerSourceDescription(snapshot, ps as CFTypeRef)?.takeUnretainedValue()
      else { 
        return -1
      }
      if let capacity = info[kIOPSCurrentCapacityKey] as? Int {
        return( capacity )
      }
    }
    
    return -1
  }
}
```

macOSでの動作結果

<img width="480" alt="macOS" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2309969/30daba55-4ecc-2762-d951-9f6274834465.png>

### 詳細

macOSの場合はMethodChannelなどは同じswiftでもあり同じ方法で組み込むことが可能。  
しかし、実装する対象のファイル(該当関数のある場所)が異なるので注意が必要。

#### MethodChannelの実装場所

VSCodeで自動作成した場合は既に以下関数が存在している。(`NSWindow`クラス)
```swift
override func awakeFromNib() {}
```

そして、Flutterからの受信を行うMethodChannelは以下の場所に実装していく。

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

        /****** この中に実装していきます ******/

  RegisterGeneratedPlugins(registry: flutterViewController)

  super.awakeFromNib()
}
```

#### MethodChannelの準備

```swift
let batteryChannel = FlutterMethodChannel(name: "platform_method/battery", binaryMessenger: flutterViewController.engine as! FlutterBinaryMessenger)
batteryChannel.setMethodCallHandler({
      (methodCall: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
    // Flutterから指定チャネルが呼ばれたらここが呼ばれる
    if (methodCall.method == "getBatteryInfo") {
        // メソッド名「getBatteryInfo」が指定されていればここの処理が実行できる
    }
})
```

※iOSと同様なので省略

#### Flutterから渡されたデータを取得

```swift
var message = "Not Message...";
if let args = methodCall.arguments as? Dictionary<String, Any> {
  if let text = args["text"] as? String,
     let num = args["num"] as? Int {
       message = self.makeMessage(text:text,num:num)
  }
}
```

※iOSと同様なので省略

#### Flutterへ返すデータを作成して結果通知

```swift
let res = [
  "device": "macOS",
  "level": level,
  "message": message,
]
result(res)
```

※iOSと同様なので省略


## Windows

### ソースコードと動作結果

```c++:windows/runner/flutter_window.cpp
#include "flutter_window.h"

#include <optional>

#include "flutter/generated_plugin_registrant.h"

// MethodChannelのため
#include <flutter/method_channel.h>
#include <flutter/standard_method_codec.h>

/* メッセージ作成 */
static std::string MakeMessage(std::string text, int num) {
  std::string mark = "w";
  std::string message = "";
  for ( int i=0; i<num; i++ ) {
    message += mark;
  }
  message += text;
  for ( int i=0; i<num; i++ ) {
    message += mark;
  }
  return message;
}

/* バッテリー残量取得 */
static int GetBatteryLevel() {
  SYSTEM_POWER_STATUS status;
  if (GetSystemPowerStatus(&status) == 0 || status.BatteryLifePercent == 255) {
    return -1;
  }
  return status.BatteryLifePercent;
}

FlutterWindow::FlutterWindow(const flutter::DartProject& project)
    : project_(project) {}

FlutterWindow::~FlutterWindow() {}

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

  RECT frame = GetClientArea();

  flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
      frame.right - frame.left, frame.bottom - frame.top, project_);
  if (!flutter_controller_->engine() || !flutter_controller_->view()) {
    return false;
  }
  RegisterPlugins(flutter_controller_->engine());

  /* ===== ここから ===== */
  flutter::MethodChannel<> channel(
    flutter_controller_->engine()->messenger(), 
    "platform_method/battery",
    &flutter::StandardMethodCodec::GetInstance()
  );
  channel.SetMethodCallHandler(
    [](const flutter::MethodCall<>& call,
    std::unique_ptr<flutter::MethodResult<>> result) {
      
      if ( call.method_name() == "getBatteryInfo" ) {
        
        std::string message = "Not Message...";
        if ( call.arguments() ) {
          const auto *arguments = std::get_if<flutter::EncodableMap>(call.arguments());
          if ( arguments ) {
            auto text_it = arguments->find(flutter::EncodableValue("text"));
            auto num_it = arguments->find(flutter::EncodableValue("num"));
            if ( (text_it != arguments->end()) && (num_it != arguments->end()) ) {
              std::string text = std::get<std::string>(text_it->second);
              int num = std::get<int>(num_it->second);
              message = MakeMessage(text,num);
            }
          }
        }
        
        int batteryLevel = GetBatteryLevel();

        flutter::EncodableMap res = {
          {"device", "Windows"},
          {"level", batteryLevel },
          {"message", message },
        };

        result->Success(res);

      } else {
        result->Error("UNAVAILABLE","ERROR");
      }
  });

  /*===== ここまで =====*/

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


/* 
    以下関数は省略
        OnDestroy
        MessageHandlerは省略
*/
```

Windowsでの動作結果

<img width="480" alt="Windows" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2309969/e104de4f-c9f8-ddcb-fdf1-7717cdddd0f8.png>

### 詳細

#### MethodChannelの実装場所

VSCodeで自動作成した場合は既に以下関数が存在している。

```c++
bool FlutterWindow::OnCreate() {}
```

そして、Flutterからの受信を行うMethodChannelは以下の場所に実装していく。

```c++
bool FlutterWindow::OnCreate() {

    // コード省略
  
  RegisterPlugins(flutter_controller_->engine());  // ここが実装位置の基準

        /****** この中に実装していきます ******/

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

#### MethodChannelの準備

```c++
flutter::MethodChannel<> channel(
    flutter_controller_->engine()->messenger(), 
    "platform_method/battery",
    &flutter::StandardMethodCodec::GetInstance()
);
channel.SetMethodCallHandler(
    [](const flutter::MethodCall<>& call,
    std::unique_ptr<flutter::MethodResult<>> result) {
      // Flutterから指定チャネルが呼ばれたらここが呼ばれる
      if ( call.method_name() == "getBatteryInfo" ) {
        // メソッド名「getBatteryInfo」が指定されていればここの処理が実行できる
      }
    }
);
```

`flutter::MrthodChannel`のチャンネルを作成して、`SetMethodCallHandler`の中に呼ばれたときの制御を書いていく。  
Flutterから指定されるメソッドは`flutter::MethodCall<>& call`の`method_name()`にて取得。

#### Flutterから渡されたデータを取得

```c++
if ( call.arguments() ) {
  // EncodableMapポインタ
  const auto *arguments = std::get_if<flutter::EncodableMap>(call.arguments());
  if ( arguments ) {
    auto text_it = arguments->find(flutter::EncodableValue("text"));
    auto num_it = arguments->find(flutter::EncodableValue("num"));
    if ( (text_it != arguments->end()) && (num_it != arguments->end()) ) {
      std::string text = std::get<std::string>(text_it->second);
      int num = std::get<int>(num_it->second);
      message = MakeMessage(text,num);
    }
  }
}
```

FlutterからのデータはMethodCallの`argumens`に入って  `flutter::EncodableMap`ポインタとして取得ことができる。  
取得したMapポインタから`flutter::EncodableValue(キー名)`で検索をかけて取得する。   


#### Flutterへ返すデータを作成して結果通知

```c++
// map用意
flutter::EncodableMap res = {
  {"device", "Windows"},
  {"level", batteryLevel },
  {"message", message },
};

result->Success(res);
```

複数データを返す方法もデータを取り出すときと同様に`flutter::EncodableMap`に詰めることで返すことが可能。  
結果は`result->Success(通知する結果)`で返せる。

## Linux

### ソースコードと動作結果

```c++:linux/my_application.cc
#include "my_application.h"

#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif

#include "flutter/generated_plugin_registrant.h"

// メッセージ作成のため
#include <string>
#include <stdlib.h>

/* メッセージ作成 */
static std::string makeMessage(std::string text, int num) {
  std::string mark = "&";
  std::string message = "";
  for ( int i = 0; i < num; i++ ) {
    message += mark;
  }
  message += text;
  for ( int i = 0; i < num; i++ ) {
    message += mark;
  }
  return message;
}

/* バッテリー残量を取得 */
static int getBatteryLevel() {
  int level = -1;
  FILE* fp = nullptr;
  char buf[30];

  // バッテリー残量をコマンド実行で取得
  const char* cmd = "cat /sys/class/power_supply/BAT0/capacity";
  if ( (fp=popen(cmd,"r")) == nullptr ) {

  }
  // バッファにコマンド結果を格納
  if ( fgets(buf, 30, fp) != nullptr ) {
    level = atoi(buf);
  }
  (void)pclose(fp);

  return level;
}

/* コールバック関数 */
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, "getBatteryInfo") == 0) {

    /* @POINT2 : Flutterから渡されたデータを解析取得 */

    std::string message = "Not Message...";

    // データ取得
    FlValue* args = fl_method_call_get_args(method_call);
    FlValue* text_value = fl_value_lookup_string(args, "text");
    FlValue* num_value = fl_value_lookup_string(args, "num");

    bool isText = (text_value == nullptr || fl_value_get_type(text_value) == FL_VALUE_TYPE_STRING ) ? true : false;
    bool isNum  = (num_value  == nullptr || fl_value_get_type(num_value)  == FL_VALUE_TYPE_INT ) ? true : false;

    if ( isText && isNum ) {
      // 取得データを適したデータ型に変換
      const char* text = fl_value_get_string(text_value);
      int num = fl_value_get_int(num_value);
      message = makeMessage(text,num);
    }

    // バッテリー残量取得
    int level = getBatteryLevel();

    /* @POINT3 : Flutterへ返す情報を作成 */

    g_autoptr(FlValue) res = fl_value_new_map();
    fl_value_set(res, 
      fl_value_new_string("device"),               
      fl_value_new_string("Linux")
    );
    fl_value_set(res, 
      fl_value_new_string("level"),               
      fl_value_new_int(level)
    );
    fl_value_set(res, 
      fl_value_new_string("message"),               
      fl_value_new_string(message.c_str())
    );

    g_autoptr(FlMethodResponse) response = FL_METHOD_RESPONSE(fl_method_success_response_new(res));
    g_autoptr(GError) error = nullptr;
    fl_method_call_respond(method_call, response, &error);

  } else {
    g_autoptr(FlMethodResponse) response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
    g_autoptr(GError) error = nullptr;
    fl_method_call_respond(method_call, response, &error);
  }

}

struct _MyApplication {
  GtkApplication parent_instance;
  char** dart_entrypoint_arguments;
};

G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)

static void my_application_activate(GApplication* application) {

  MyApplication* self = MY_APPLICATION(application);
  GtkWindow* window =
      GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));

  gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
  GdkScreen* screen = gtk_window_get_screen(window);
  if (GDK_IS_X11_SCREEN(screen)) {
    const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
    if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
      use_header_bar = FALSE;
    }
  }
#endif
  if (use_header_bar) {
    GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
    gtk_widget_show(GTK_WIDGET(header_bar));
    gtk_header_bar_set_title(header_bar, "platform_method");
    gtk_header_bar_set_show_close_button(header_bar, TRUE);
    gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
  } else {
    gtk_window_set_title(window, "platform_method");
  }

  gtk_window_set_default_size(window, 1280, 720);
  gtk_widget_show(GTK_WIDGET(window));

  g_autoptr(FlDartProject) project = fl_dart_project_new();
  fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);

  FlView* view = fl_view_new(project);
  gtk_widget_show(GTK_WIDGET(view));
  gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));

  fl_register_plugins(FL_PLUGIN_REGISTRY(view));

  gtk_widget_grab_focus(GTK_WIDGET(view));

  /*===== ここから =====*/

  /* @POINT1 チャンネルとハンドラコールバック登録 */
  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,
                            "platform_method/battery", 
                            FL_METHOD_CODEC(codec));
  fl_method_channel_set_method_call_handler(channel, 
                                            method_call_cb,  
                                            g_object_ref(view),
                                            g_object_unref);

  /*===== ここまで =====*/

}

/*
    以下関数は省略(自動生成されたテンプレート参照)
        my_application_local_command_line
        my_application_dispose
        my_application_class_init
        my_application_init
        my_application_new
*/

```

Linuxでの動作結果
<img width="480" alt="Linux" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2309969/393a4d20-6403-5055-46c1-48069c4eda68.png>

### 詳細

#### MethodChannelの実装場所

VSCodeで自動作成した場合は既に以下関数が存在している。
```c++
static void my_application_activate(GApplication* application) {}
```

そして、Flutterからの受信を行うMethodChannelは以下の場所に実装していく。
```c++
static void my_application_activate(GApplication* application) {
  // 他実装は長いので省略
  fl_register_plugins(FL_PLUGIN_REGISTRY(view));

  gtk_widget_grab_focus(GTK_WIDGET(view)); // ここが実装位置の基準

        /****** この中に実装していきます ******/

}
```
#### MethodChannelの準備

```c++
// チャンネル用意
g_autoptr(FlMethodChannel) channel =
      fl_method_channel_new(messenger,
                            "platform_method/battery", 
                            FL_METHOD_CODEC(codec));

// コールバック関数を登録
fl_method_channel_set_method_call_handler(channel,    // 用意したチャンネル
                                        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);
```

Linuxの場合はMethodChannelはコールバック関数として登録してあげる必要があるらしい。  

`FlMethodChannel`のチャンネルを作成して、
`fl_method_channel_set_method_call_handler`に作成チャンネルとコールバック関数を登録。

今回登録するコールバック関数は以下で、この中にメソッドが指定されてた時の制御を書いていく。
```c++
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, "getBatteryInfo") == 0) {
    // メソッド名「getBatteryInfo」が指定されていればここの処理が実行できる
  }
}
```

Flutterから指定されるメソッドは引数の`FlMethodCall *method_call`を用いて`fl_method_call_get_name(method_call)`で取得。

#### Flutterから渡されたデータを取得

```c++
// データ取得
FlValue* args = fl_method_call_get_args(method_call);       // データ一覧を取得
FlValue* text_value = fl_value_lookup_string(args, "text"); // キー名を指定して各バリューを取り出す
FlValue* num_value = fl_value_lookup_string(args, "num"); 

// 取得できているかチェック
bool isText = (text_value == nullptr || fl_value_get_type(text_value) == FL_VALUE_TYPE_STRING ) ? true : false;
bool isNum  = (num_value  == nullptr || fl_value_get_type(num_value)  == FL_VALUE_TYPE_INT ) ? true : false;

if ( isText && isNum ) {
  // 取得データを適したデータ型に変換
  const char* text = fl_value_get_string(text_value);
  int num = fl_value_get_int(num_value);
  message = makeMessage(text,num);
}
```
Flutterからのデータは引数の`FlMethodCall *method_call`より、  
`fl_method_call_get_args(method_call)`で`FLValue`ポインタとして取得。  

バリューを取り出すときは`fl_value_lookup_string(method_callから取り出したポインタ,"キー名")`で取得。(この時も結果は`FlValue`ポインタ)    
取り出したバリューは`fl_value_get_XXX`でXXXのデータ型に変換。


#### Flutterへ返すデータを作成して結果通知

```c++
// map用意
g_autoptr(FlValue) res = fl_value_new_map();

// キーとバリューをmapに登録
fl_value_set(res, 
  fl_value_new_string("device"),               
  fl_value_new_string("Linux")
);
fl_value_set(res, 
  fl_value_new_string("level"),               
  fl_value_new_int(level)
);
fl_value_set(res, 
  fl_value_new_string("message"),               
  fl_value_new_string(message.c_str())
);

// レスポンス作成
g_autoptr(FlMethodResponse) response = FL_METHOD_RESPONSE(fl_method_success_response_new(res));
g_autoptr(GError) error = nullptr;

// 結果登録
fl_method_call_respond(method_call, response, &error);
```

複数データを返すときは`fl_value_new_map()`でmapを用意して使う。  
mapにデータを登録するときは`fl_value_set`でキーとバリューを設定。  

結果を返すときは`FL_METHOD_RESPONSE(fl_method_success_response_new(通知する結果))`にてレスポンスを作成し、  
`fl_method_call_respond(method_call, response, &error)`に作成したレスポンスを入れることで返せる。


# 最後に

MethodChannelについてAndroidやiOSは結構記事が見つかるものの、少し前（2022/02)に正式対応となったWindowsやmacOS向けのやり方がなかなか見つからなかったので、本記事を作成してみました。  

今回はMethodChannelを使ったもので、やり取りするデータについて型安全とは言えないと思います。  
`pigeon`という型安全なメッセージでやりとりできる仕組みもあるらしいので、気が向いたら置き換えとかしてみたいなぁと思ったりしてます。


# 参考

Dart、Android、iOS  
https://docs.flutter.dev/

Windows、Linusx
https://www.dynamsoft.com/codepool/flutter-document-scanning-plugin-windows-linux.html