概要

Flutterアプリを作成している際、ネイティブコードを使いたい時があります。
ここではiOSでの実装方法を2パターンご紹介します。
外部パッケージの導入は保守や脆弱性対応の対象も増えます。
実装量もそれほど多くないため、選択肢として把握しておくと便利です。

パターン1 Flutterからネイティブコードを呼ぶ

iOS側

import Flutter

@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {

  func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
    GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
    ...
    let methodChannel = FlutterMethodChannel(name: "jp.co.jwork.sample.app/sample", binaryMessenger: engineBridge.applicationRegistrar.messenger())
    methodChannel.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
        let method = call.method
        if (method == "input") {
            let args = call.arguments as! [Any]
            // ...iOSでやりたい処理 ...
            result("ok")
        } else {
            result(FlutterMethodNotImplemented)
        }
    })
  }
}

Flutter側

import 'dart:async';

import "package:flutter/services.dart";

class Caller {

    static const MethodChannel _methodChannel = const MethodChannel('jp.co.jwork.sample.app/sample');

    Future run(String param) async {
        try {
            return await _methodChannel.invokeMethod("input", [ param ]);
        } on PlatformException {
            // エラーハンドリング
        }
    }
}

パターン2 iOSからイベントをFlutter側に流す

iOSからFlutterへ渡す場合、EventChannelを使用できます。
Flutter側で入り口(EventChannel)を名前付きで用意しておき、名前を元にiOS側で呼ぶイメージです。
なお、1回きりで良い場合は、同じ流れでパターン1のMethodChannelを使うことでも実現可能です。

iOS側

import Flutter

@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {

  static var eventSink: FlutterEventSink?
  func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
    GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
    ...
    FlutterEventChannel(name: "jp.co.jwork.sample.app/sampleEvent", binaryMessenger: engineBridge.applicationRegistrar.messenger())
            .setStreamHandler(self)
  }

  // 何らかのiOS側処理の値
  private func onInput(_ value: String) {
    AppDelegate.eventSink?(value)
  }
}


extension AppDelegate: FlutterStreamHandler {

  func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
    AppDelegate.eventSink = events
    return nil
  }

  func onCancel(withArguments arguments: Any?) -> FlutterError? {
    AppDelegate.eventSink = nil
    return nil
  }
}

Flutter側

import 'dart:async';

import "package:flutter/services.dart";

class Receiver {

    static const EventChannel _eventChannel = const EventChannel("jp.co.jwork.sample.app/sampleEvent");
    
    StreamSubscription? _listenEventStreamSubs;
    
    // 引数で関数を受けていますが、単純にStreamSubscriptionを返す作りでも良いと思います。
    Future<void> listen(void Function(String value) onListen) async {
        await _listenEventStreamSubs?.cancel();
        _listenEventStreamSubs = null;
        _listenEventStreamSubs = _eventChannel.receiveBroadcastStream().listen((event) {
            if (event is String) {
                onListen(event);
            }
        }, onError: (error) {
            // エラーハンドリング
        }, cancelOnError: true);
    }

    // 不要になったら閉じます。
    Future<void> cancel() async {
        await _listenEventStreamSubs?.cancel();
        _listenEventStreamSubs = null;
    }
}

まとめ

MethodChannel、EventChannelで簡単にiOSコードとやりとりが出来ました。
汎用的なものならpluginとして切り出し、使いまわせるようにするのも良いと思います。