🔮

【Flutter】MethodChannelでネイティブコードを呼び出す📣

2024/02/05に公開

背景

Flutterでネイティブアプリ向けSDKを利用してAPI実装が必要だったので、調査した内容をまとめました。

ゴール

ネイティブコードの呼び出しができること。

  • iOS:Swift(+α Objective-C)
  • Android:Kotlin(+α Java)

SDKを利用できること

  • iOS:framework, xcframework
  • Android:AAR

検討した方法

MethodChannelを使用する

実装例

ネイティブコードの呼び出し

使用する場面が多そうなSwiftとKotlinの例を用意しました。
Objective-CとJavaについてもメモ書きしたので参考になれば🙏

Objective-C, Java

Objective-CまたはJavaを直接呼び出す

実装の参考は以下


Swift(またはKotlin)からObjective-C(またはJava)を呼び出し、それをFlutterから呼び出す

  1. Objective-Cの実装

XCodeのFile>New>File..からObjective-CFile(EmptyFile)を作成

ios/Runner/hoge.m
#import <Foundation/Foundation.h>
+#import "hoge.h"
+
+@implementation Hoge
+
+ // 処理
+-(void) testMethod {
+    NSLog(@"Hello from Objective-C!");
+}
+
+@end

XCodeのFile>New>File..からHeaderFileを作成

ios/Runner/hoge.h
#ifndef hoge_h
#define hoge_h

+#import <Foundation/Foundation.h>

#endif /* hoge_h */

+// クラス定義
+@interface Hoge : NSObject
+// メソッド定義
+-(void) testMethod;
+
+@end

ブリッジファイルにimportを追加

ios/Runner/Runner-Bridging-Header.h
#import "GeneratedPluginRegistrant.h"
+#import "hoge.h"
  1. Swiftの実装
    Objective-Cのメソッドを呼び出し
ios/Runner/AppDelegate.swift
import UIKit
import Flutter

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

+        let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
+        let methodChannel = FlutterMethodChannel(name: "Channel",binaryMessenger: controller as! FlutterBinaryMessenger)
+        methodChannel.setMethodCallHandler({
+            (call:FlutterMethodCall, result:FlutterResult) -> Void in
+            switch call.method {
+            case "getHello" :
+                Hoge.init().testMethod()
+                result("Hello from Objective-C!")
+            default :
+                result(nil)
+            }
+        })
        
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}
  1. Flutterの実装
main.dart
+ import 'package:flutter/services.dart';
//
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  
  Widget build(BuildContext context) {
+    // ネイティブの処理を呼び出す
+    Future<void> getHello() async {
+      const channel = MethodChannel('Channel');
+      final resultText = await channel.invokeMethod('getHello');
+      debugPrint(resultText);
+    }

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextButton(
+              onPressed: () async => getHello(),
              child: const Text('getHello'),
            )
          ],
        ),
      ),
    );
  }
}
  1. テキストボタンを押下するとデバックコンソールにHello from Objective-C!と表示される。
  1. 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 {
        GeneratedPluginRegistrant.register(with: self)
        
+        let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
+        let methodChannel = FlutterMethodChannel(name: "Channel",binaryMessenger: controller as! FlutterBinaryMessenger)
+        methodChannel.setMethodCallHandler({
+            (call:FlutterMethodCall, result:FlutterResult) -> Void in
+            switch call.method {
+            case "getHello" :
+                result("Hello from Swift!")
+            default :
+                result(nil)
+            }
+        })
        
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}
  1. Kotlinの実装
android/app/src/main/kotlin/com/example/flutter_native_sample/MainActivity.kt
package com.example.flutter_native_sample

import io.flutter.embedding.android.FlutterActivity

class MainActivity : FlutterActivity() {
+    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
+        super.configureFlutterEngine(flutterEngine)
+        MethodChannel(
+            flutterEngine.dartExecutor.binaryMessenger,
+            "Channel"
+        ).setMethodCallHandler { call, result ->
+            when (call.method) {
+                "getHello" ->
+                    result.success("Hello from Kotlin!")
+
+                else ->
+                    result.success(null)
+            }
+        }
+
+    }
}
  1. Flutterの実装
main.dart
+ import 'package:flutter/services.dart';
//
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  
  Widget build(BuildContext context) {
+    // ネイティブの処理を呼び出す
+    Future<void> getHello() async {
+      const channel = MethodChannel('Channel');
+      final resultText = await channel.invokeMethod('getHello');
+      debugPrint(resultText);
+    }

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextButton(
+              onPressed: () async => getHello(),
              child: const Text('getHello'),
            )
          ],
        ),
      ),
    );
  }
}
  1. テキストボタンを押下するとデバックコンソールにHello from Swift!またはHello from Kotlin!と表示される。

SDKの利用

  1. iOSの実装

framework(xcframework)の取り込み
iosフォルダをXcodeで開き、「Target > Runner > General > Frameworks,Libraries...」にframeworkをドラックアンドドロップ
※xcframeworkの場合でも同様

ブリッジファイルにimportを追加
Objective-Cライブラリの場合のみ必要な手順で、Swiftライブラリの場合は不要かも

ios/Runner/Runner-Bridging-Header.h
#ifndef Generated_Plugin_Registrant_h
#define Generated_Plugin_Registrant_h
#import "GeneratedPluginRegistrant.h"
+// .modulemapの`framework module {モジュール名}`を参照し定義
+#import <{モジュール名}/{モジュール名}.h>
#endif

frameworkの呼び出し

ios/Runner/AppDelegate.swift
import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        GeneratedPluginRegistrant.register(with: self)
        
+        let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
+        let methodChannel = FlutterMethodChannel(name: "Channel",binaryMessenger: controller as! FlutterBinaryMessenger)
+        methodChannel.setMethodCallHandler({
+            (call:FlutterMethodCall, result:FlutterResult) -> Void in
+            switch call.method {
+            case "hoge" :
+                self.hoge()
+                result("hoge")
+            default :
+                result(nil)
+            }
+        })
        
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

+    private func hoge() -> Void {
+        // 追加したライブラリのメソッドを呼び出し
+        Hoge.fuga()
+    }
}
  1. Androidの実装

AARの取り込み
android/app配下にlibsフォルダを作成
.aarを格納し依存関係を記述

android/app/build.gradle
dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+    implementation files('libs/{ファイル名}.aar')
}
android/build.gradle
allprojects {
    repositories {
	 ~
+        flatDir {
+            dirs 'libs'
+        }
    }
}

AARの呼び出し

android/app/src/main/kotlin/~/MainActivity.kt
+import {必要なパッケージのインポート(「more actions...」で入れれる)}
~~~
class MainActivity : FlutterActivity() {
+    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
+        super.configureFlutterEngine(flutterEngine)
+        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "Channel").setMethodCallHandler { call, result ->
+            when (call.method) {
+                "hoge" -> {
+                    Hoge().huga()
+                    result.success("hoge")
+                }
+
+                else ->
+                    result.success(null)
+            }
+        }
+
+    }
}
  1. Flutterの実装
main.dart
+ import 'package:flutter/services.dart';
//
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  
  Widget build(BuildContext context) {
+    // ネイティブ(ライブラリ)の処理を呼び出す
+    Future<void> hoge() async {
+      const channel = MethodChannel('Channel');
+      final resultText = await channel.invokeMethod('hoge');
+      debugPrint(resultText);
+    }

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextButton(
+              onPressed: () async => hoge(),
              child: const Text('hoge'),
            )
          ],
        ),
      ),
    );
  }
}
  1. 正常にビルド、ライブラリのメソッド呼び出しができればOK!

まとめ

MethodChannelを使うことでネイティブの呼び出しができるのは便利ですが、
要件によっては複雑さが増して対応が難しいケースもあると思うのでその都度ネイティブ知識のキャッチアップが欠かせなそうです。

一例)ネイティブのメソッドの引数にUIViewController型が必要なケース
AppDelegate.swiftでUIViewControllerを継承したクラスを渡しても処理が実行されず。。
対象のメソッドを呼び出すためにSwiftで画面を作成しないといけないかもでハマった。
結論、let controller: FlutterViewController = window?.rootViewController as! FlutterViewControllerのcontrollerを渡すことで正常に作動。

参考

https://rightcode.co.jp/blog/information-technology/flutter-native-code-syain
https://qiita.com/ko2ic/items/ed87dbdb515b03f4dbd3
https://note.com/mizutory/n/ne69dacbe34de

Discussion