Capacitor、Flutter、React Nativeのプラグインコード比較
定期的に、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では、 ReactContextBaseJavaModule
の getName
を override
して、プラグイン名を登録します。メソッド名に関しては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
コマンドで行っていた部分ですね。
+ 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