前回の投稿では、Cordovaアプリの基本的な開発手順についてまとめました。
今回は、プラグインの開発手順についてまとめようと思います。
Plugin Development Guide
(2022/12/30)
プラグインからのコールバックを追記しました。
プラグイン名を決める
まずは、プラグイン名を決めましょう。
今回は適当に「sampleplugin」とでもしておきましょうか。
フォルダ名は、慣例があり、「cordova-plugin-sampleplugin」となります。
> mkdir cordova-plugin-sampleplugin
> cd cordova-plugin-sampleplugin
plugin.xmlを作成
<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
id="cordova-plugin-sampleplugin" version="0.0.1">
<name>SamplePlugin</name>
<js-module src="www/plugin_sampleplugin.js" name="sampleplugin">
<clobbers target="sampleplugin" />
</js-module>
<platform name="android">
// 後述
</platform>
<platform name="ios">
// 後述
</platform>
</plugin>
大事なところを補足します。
<js-module src="www/plugin_sampleplugin.js" name="sampleplugin">
<clobbers target="sampleplugin" />
</js-module>
srcは、これから作成するネイティブプラグインとJavascript呼び出しを仲介するコードを実装するJavascriptファイルです。wwwフォルダの下に作成して置く予定です。
targetで指定した文字列を使って、他のJavascriptファイルからこのJavascriptで実装したクラスを参照できます。
この「js-module」で指定したJavascriptファイルは、自動的にWebコンテンツに追加されますので、ほかにも追加したいJavascriptファイルがある場合は、そのファイルの数だけ「js-module」を指定します。
plugin_sampleplugin.jsは、例えば以下のように作成します。
参考に、func1という関数と、func2という関数と、func3というコールバック登録関数を作成していきます。
class SamplePlugin{
constructor(){
}
func1(param1, param2, param3){
return new Promise(function(resolve, reject){
cordova.exec(
function(result){
resolve(result);
},
function(err){
reject(err);
},
"SamplePlugin", "func1",
[param1, param2, param3]);
});
}
func2(return_type){
return new Promise(function(resolve, reject){
cordova.exec(
function(result){
resolve(result);
},
function(err){
reject(err);
},
"SamplePlugin", "func2",
[return_type]);
});
}
func3(enable, callback){
cordova.exec(
function(result){
callback(result);
},
function(err){
console.error("func3 call failed");
},
"SamplePlugin", "func3",
[enable]);
}
}
module.exports = new SamplePlugin();
cordova.execが、ネイティブプラグインを呼び出しているところで、第3引数を後述する定義の名前と合わせる必要があります。
第4引数の文字列は、ネイティブプラグイン実装の中で、呼び出された関数を識別するための文字列になります。
package.jsonの作成
こんなファイルを作っておきます。
{
"name": "cordova-plugin-sampleplugin",
"version": "0.0.1",
"cordova": {
"id": "cordova-plugin-sampleplugin",
"platforms": [
"android",
"ios"
]
}
}
今回は、AndroidとiOSのネイティブプラグインを作成します。
Androidの場合
これからは、スマホのOSごとに異なってきますので、OSに分けて説明します。
plugin.xml(Android)
plugin.xmlのAndroid部分は以下のようになります。
<platform name="android">
<config-file target="res/xml/config.xml" parent="/*">
<feature name="SamplePlugin" >
<param name="android-package" value="jp.or.sample.SamplePlugin.Main"/>
<param name="onload" value="true" />
</feature>
</config-file>
<config-file target="AndroidManifest.xml" parent="/*">
<uses-permission android:name="android.permission.NFC" />
</config-file>
<source-file src="src/android/jp/or/sample/SamplePlugin/Main.java" target-dir="src/jp/or/sample/SamplePlugin" />
</platform>
featureに指定したnameを、cordova.execの第3引数に指定する文字列と合わせます。
<param name="onload" value="true" />
は、アプリ起動と同時に、ネイティブプラグインを有効にしたい場合に指定します。
<config-file target="AndroidManifest.xml" parent="/*">
の部分は、何かAndroidManifest.xmlに定義を追加したい場合に記載します。一例ですので、指定は任意です。
source-fileに指定したsrcがこれから実装するネイティブ実装のソースコードです。通常はsrcフォルダの配下に作成します。AndroidだけでなくiOS用にも作成する予定なので、OSの区別も持たせています。また、Androidではパッケージ名に沿ったフォルダ構成にする必要があります。今回は、jp.or.sample.SamplePluginというパッケージ名にしています。
target-dirは、Androidのプロジェクトに取り込むときの取り込み先の場所を指定します。
ネイティブ実装(Android)
以下のような関数を実装します。
・func1は、3つの引数(Int、String、[Int])を取って、3つのオブジェクトをそのまま返します。func3でコールバック関数を登録していた場合、コールバック関数も呼び出します。
・func2は、1つの引数("bool" or "int" or "string" or "array")を取って、指定した型の戻り値を返します。
・func3は、コールバック関数を受け取ります。いっしょにコールバック有効・無効も指定します。
package jp.or.sample.SamplePlugin;
import android.app.Activity;
import android.content.Intent;
import android.util.Log;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class Main extends CordovaPlugin {
public static String TAG = "SamplePlugin.Main";
private Activity activity;
private CallbackContext callback;
@Override
public void initialize(CordovaInterface cordova, CordovaWebView webView)
{
Log.d(TAG, "[Plugin] initialize called");
super.initialize(cordova, webView);
activity = cordova.getActivity();
}
@Override
public void onResume(boolean multitasking)
{
Log.d(TAG, "[Plugin] onResume called");
super.onResume(multitasking);
}
@Override
public void onPause(boolean multitasking)
{
Log.d(TAG, "[Plugin] onPause called");
super.onPause(multitasking);
}
@Override
public void onNewIntent(Intent intent)
{
Log.d(TAG, "[Plugin] onNewIntent called");
super.onNewIntent(intent);
}
private void sendMessageToJs(JSONObject message, CallbackContext callback) {
final PluginResult result = new PluginResult(PluginResult.Status.OK, message);
result.setKeepCallback(true);
if( callback != null )
callback.sendPluginResult(result);
}
@Override
public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException
{
Log.d(TAG, "[Plugin] execute called");
if( action.equals("func1") ){
int arg0 = args.getInt(0);
String arg1 = args.getString(1);
JSONArray input_array = args.getJSONArray(2);
int[] arg2 = new int[input_array.length()];
for( int i = 0 ; i < arg2.length ; i++ )
arg2[i] = input_array.getInt(i);
JSONArray output_array = new JSONArray();
for( int i = 0 ; i < arg2.length ; i++ )
output_array.put(arg2[i]);
JSONObject result = new JSONObject();
result.put("arg0", arg0);
result.put("arg1", arg1);
result.put("arg2", output_array);
callbackContext.success(result);
sendMessageToJs(result, callback);
}else
if( action.equals("func2") ){
String arg0 = args.getString(0);
if( arg0.equals("int") ){
callbackContext.success(1234);
}else
if( arg0.equals("string") ){
callbackContext.success("Hello World");
}else
if( arg0.equals("array") ){
JSONArray output_array = new JSONArray();
for( int i = 0 ; i < 5 ; i++ )
output_array.put(i);
callbackContext.success(output_array);
}else{
callbackContext.error("Unknown arg0");
return false;
}
}else
if( action.equals("func3") ){
boolean arg0 = args.getBoolean(0);
if( arg0 ){
callback = callbackContext;
}else{
callback = null;
callbackContext.success("OK");
}
}else {
String message = "Unknown action : (" + action + ") " + args.getString(0);
Log.d(TAG, message);
callbackContext.error(message);
return false;
}
return true;
}
}
コールバック関数の実装方法について補足します。
呼び出し側から呼ばれてから、普通にcallbackContext.successを呼び出すと、その呼び出しはそれで終了してしまい、コールバック関数を呼び出す前に終わってしまうので、とりあえずそれは呼び出さないでおきます。その時、次回コールバック関数呼び出せるように、callbackContextを覚えておきます。
そして、コールバックしたい時に、覚えておいたcallbackContextを使ってcallbackContext.successを呼び出すことができます。ですが、そこでそのまま呼び出すと終了してしまうので、result.setKeepCallback(true)を呼び出しておくと、また次にコールバック関数を呼べるようになります。
iOSの場合
plugin.xml
plugin.xmlのiOS部分は以下のようになります。
<platform name="ios">
<dependency id="cordova-plugin-add-swift-support" version="2.0.2"/>
<config-file target="config.xml" parent="/*">
<feature name="SamplePlugin" >
<param name="ios-package" value="SamplePlugin"/>
<param name="onload" value="true" />
</feature>
</config-file>
<config-file target="*-Info.plist" parent="NFCReaderUsageDescription">
<string>NFC Scanning</string>
</config-file>
<source-file src="src/ios/SamplePlugin.swift" target-dir="src/ios" />
</platform>
もともとCordovaのプラグインは、Object-Cで作成するのが通常でしたが、最近はSwiftでも作成できるようになっています。そのための設定が<dependency id="cordova-plugin-add-swift-support" version="2.0.2"/>
です。
featureに指定したnameを、cordova.execの第3引数に指定する文字列と合わせます。
<param name="onload" value="true" />
は、アプリ起動と同時に、ネイティブプラグインを有効にしたい場合に指定します。ここらへんは、Androidと同じです。
<config-file target="*-Info.plist" parent="NFCReaderUsageDescription">
の部分は、何かplistファイルに定義を追加したい場合に記載します。一例ですので、指定は任意です。
source-fileに指定したsrcがこれから実装するネイティブ実装のソースコードです。通常はsrcフォルダの配下に作成します。target-dirは、iOSのプロジェクトに取り込むときの取り込み先の場所を指定します。
ネイティブ実装(iOS)
Androidの時と同様に、以下のような関数を実装します。
・func1は、3つの引数(Int、String、[Int])を取って、3つのオブジェクトをそのまま返します。func3でコールバック関数を登録していた場合、コールバック関数も呼び出します。
・func2は、1つの引数("bool" or "int" or "string" or "array")を取って、指定した型の戻り値を返します。
・func3は、コールバック関数を受け取ります。いっしょにコールバック有効・無効も指定します。
import Foundation
@objc(SamplePlugin)
class SamplePlugin : CDVPlugin
{
var callbackId: String?
override
func pluginInitialize() {
}
@objc(func1:)
func func1(command: CDVInvokedUrlCommand)
{
NSLog("func1 called")
guard let arg0 = command.arguments[0] as? Int, let arg1 = command.arguments[1] as? String, let arg2 = command.arguments[2] as? [Int] else {
NSLog("Parameter invalid")
let pluginResult:CDVPluginResult = CDVPluginResult(status:CDVCommandStatus_ERROR, messageAs: "Parameter Invalid")
self.commandDelegate.send(pluginResult, callbackId:command.callbackId)
return
}
let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: ["arg0": arg0, "arg1": arg1, "arg2": arg2] )
commandDelegate.send(pluginResult, callbackId: command.callbackId)
let pluginResult2 = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: ["arg0": arg0, "arg1": arg1, "arg2": arg2] )
pluginResult2?.keepCallback = true
if let callbackId = self.callbackId {
commandDelegate.send(pluginResult2, callbackId: callbackId)
}
}
@objc(func2:)
func func2(command: CDVInvokedUrlCommand)
{
NSLog("func2 called")
guard let arg0 = command.arguments[0] as? String else {
NSLog("Parameter invalid")
let pluginResult:CDVPluginResult = CDVPluginResult(status:CDVCommandStatus_ERROR, messageAs: "Parameter Invalid")
self.commandDelegate.send(pluginResult, callbackId:command.callbackId)
return
}
if arg0 == "int" {
let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: 1234)
commandDelegate.send(pluginResult, callbackId: command.callbackId)
}else if arg0 == "string" {
let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: "Hello World")
commandDelegate.send(pluginResult, callbackId: command.callbackId)
}else if arg0 == "array" {
var output_array:[Int] = []
for i in 1 ..< 5 {
output_array.append(i)
}
let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: output_array)
commandDelegate.send(pluginResult, callbackId: command.callbackId)
}else{
let pluginResult = CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: "Invalid arg0")
commandDelegate.send(pluginResult, callbackId: command.callbackId)
return
}
return
}
@objc(func3:)
func func3(command: CDVInvokedUrlCommand)
{
NSLog("func3 called")
guard let arg0 = command.arguments[0] as? Bool else {
NSLog("Parameter invalid")
let pluginResult:CDVPluginResult = CDVPluginResult(status:CDVCommandStatus_ERROR, messageAs: "Parameter Invalid")
self.commandDelegate.send(pluginResult, callbackId:command.callbackId)
return
}
if arg0 {
self.callbackId = command.callbackId
}else{
self.callbackId = nil
let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: "OK" )
commandDelegate.send(pluginResult, callbackId: command.callbackId)
}
}
}
コールバック関数の実装方法について補足します。
呼び出し側から呼ばれてから、普通にcommandDelegate.sendを呼び出すと、その呼び出しはそれで終了してしまい、コールバック関数を呼び出す前に終わってしまうので、とりあえずそれは呼び出さないでおきます。その時、次回コールバック関数呼び出せるように、callbackIdを覚えておきます。
そして、コールバックしたい時に、覚えておいたcallbackIdを使ってcommandDelegate.sendを呼び出すことができます。ですが、そこでそのまま呼び出すと終了してしまうので、pluginResult2?.keepCallback = true を呼び出しておくと、また次にコールバック関数を呼べるようになります。
Cordovaプロジェクトにプラグインを追加
以下を実行します。2通りの方法があります。
> cordova plugin add ..\cordova-plugin-sampleplugin
または
> cordova plugin add ..\cordova-plugin-sampleplugin --link
前者は、元のプラグインのコピーがプロジェクトフォルダに作成されます。後者は、単にリンクが作成されます。
ですので、プラグインのデバッグ時は後者で作成して、ネイティブコードが完成した後は前者を使うようにします。
実行
[Androidの場合]
> cordova build android
> cordova run android
[iOSの場合]
> cordova build ios
> cordova run ios
プラグインのデバッグ
プラグインのネイティブ実装をデバッグしたい場合は、開発環境(Android StudioまたはXCode)で開いてデバッグします。
以下のフォルダを開きます。
[Androidの場合]
platforms\android
[iOSの場合]
platforms\ios
それぞれの開発環境において、ネイティブソースコードのところにブレークポイントが晴れるので、より詳しくデバッグができます。
iOSの場合は、XCodeで、以下の部分を設定します。
- Signing & CapabilitiesのTeam
- Build SettingsのiOS Deployment TargetのiOSバージョン
終わりに
以下も参考にしてください。
以下のページを参考にさせていただきました(ありがとうございました)
https://tech-blog.rakus.co.jp/entry/20220207/cordva
作成したソースコードをGitHubに上げておきました。
以上