💬

Capacitor、Flutter、React Nativeのプラグインコード比較

2024/11/28に公開

定期的に、Capacitor、Flutter、React Nativeをテーマにして、ビューの実装とプラグイン開発を混合してる人を見かけます。

ここでいうプラグインとは、CapacitorやFlutter、React Nativeのビューからプラットフォームの機構を通じてSwiftやKotlinのコードとやりとりすることができる機能です。 ネイティブのカメラやセンター等の機能、また様々なサードパーティSDKを利用して例えばFacebookログインやAdMob広告、決済などを実現することができます。

プラグインはユーザとして使うだけだとブラックボックス化してしまいがちですが、プラグインを知る第一歩として、Kotlinで簡単に echo するだけのプラグインコードの差をみてみましょう。

Capacitorプラグイン

Capacitor Androidプラグインは、 @CapacitorPlugin デコレーターをつけたClass内の @PluginMethod デコレーターをつけたメソッドがプラグインのメソッドとして登録されます。メソッド名は、関数名がそのまま採用されます。

@CapacitorPlugin(name = "MyPlugin")
class MyPlugin : Plugin() {
    @PluginMethod
    fun echo(call: PluginCall) {
        val value = call.getString("value") ?: ""
        call.success(JSObject().put("value", value))
    }
}

プロジェクトへの反映はとても簡単で、インストール後にこのコマンドを実行するだけです。

% npx cap update

そうすると、このようにビュー内で利用することができます。

import { MyPlugin } from 'capacitor-plugin-my-plugin';

const ret = await MyPlugin.echo({ value: 'Hello' });
console.log(ret.value); // Hello

簡単ですね。

React Nativeプラグイン

React Nativeでは、 ReactContextBaseJavaModulegetNameoverride して、プラグイン名を登録します。メソッド名に関してはCapacitorと同じように、 @ReactMethod デコレーターのついている関数名がそのまま採用される形です。

class MyPluginModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
    override fun getName(): String {
        return "MyPlugin"
    }

    @ReactMethod
    fun echo(value: String?, promise: Promise) {
        val result = mapOf("value" to (value ?: ""))
        promise.resolve(result)
    }
}

このプラグインを利用しようとすると、ネイティブプラグインを登録するために、プロジェクトの getPackages に追加する必要があります。これはCapacitorが npx cap update コマンドで行っていた部分ですね。

java
+ import com.example.myplugin.MyPluginPackage; // 追加

  @Override
  protected List<ReactPackage> getPackages() {
      return Arrays.<ReactPackage>asList(
          new MainReactPackage(),
+         new MyPluginPackage()
      );
  }

こうすると、ビューで利用できるようになります。 NativeModules オブジェクトからプラグインを呼び出すことができます。

import { NativeModules } from 'react-native';
const { MyPlugin } = NativeModules;

const result = await MyPlugin.echo("Hello");
console.log(result.value); // Output: Hello

Flutterプラグイン

Flutterプラグインの特徴はプラグインがアタッチ/デタッチされたタイミングでライフサイクルメソッドが走ることでしょうか。アタッチされたタイミングでプラグインを登録するので、このライフサイクルの利用は必須です(デタッチは不要)。

メソッドを呼び出すと、 onMethodCall が呼び出され、 call.method からメソッド名で分岐を行います。

class MyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
    private lateinit var channel: MethodChannel

    override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
        channel = MethodChannel(flutterPluginBinding.binaryMessenger, "my_plugin")
        channel.setMethodCallHandler(this)
    }

    override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
        channel.setMethodCallHandler(null)
    }

    override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) {
        when (call.method) {
            "echo" -> {
                val value = call.argument<String>("value") ?: ""
                val ret = mapOf("value" to value)
                result.success(ret)
            }
            else -> result.notImplemented()
        }
    }
}

このプラグインを使うためにはこんな感じのコードを書きます。 MyPlugin の経由は必須ではないのですが、アタッチ / デタッチのタイミングを制御するために、ビューでの実行時以前にこのようにプラグインを登録することが推奨されています。

import 'package:flutter/services.dart';

class MyPlugin {
  static const MethodChannel _channel = MethodChannel('my_plugin');

  static Future<Map<String, dynamic>> echo(String value) async {
    final result = await _channel.invokeMethod('echo', {'value': value});
    return Map<String, dynamic>.from(result);
  }
}

void main() async {
  final response = await MyPlugin.echo("Hello");
  print(response['value']); // Output: Hello
}

まとめ

ビューの設計ももちろんのことですが、プラグイン機構でもそれぞれの開発思想が垣間見れて面白いのではないでしょうか。

私はCapacitorユーザなのですが、慣習的に使われている call.resolve よりも、React Nativeの promise.resolve の方がビュー側からの操作が直感的でわかりやすく感じたり、反面 npx cap update でプロジェクトへの自動反映はめちゃくちゃ便利だよと思ったりとしてます。

また、Flutterの call.method で呼び出し名が変わるのも面白いですね。プラグイン次第では、メソッド実行前に共通のValidationを行い時(例えば initialize が済んでるかなど)に使えるので、そういう用途でとてもいいなと思いました。

いずれにしろ、ビュー部分とは分離して、ネイティブ部分はネイティブ(SwiftやKotlin)で書くので、これに関してはパフォーマンスであったりメンテコストはほぼどれも変わらないかなとは思います。

ぜひプラグインをブラックボックス化したままにせず、中身にまで踏み込んで楽しい開発ライフを送ってください!

それではまた。

Discussion