3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Cordovaアプリ開発の備忘録(プラグイン編)

Last updated at Posted at 2022-10-10

前回の投稿では、Cordovaアプリの基本的な開発手順についてまとめました。
今回は、プラグインの開発手順についてまとめようと思います。

Plugin Development Guide

(2022/12/30)
プラグインからのコールバックを追記しました。

プラグイン名を決める

まずは、プラグイン名を決めましょう。
今回は適当に「sampleplugin」とでもしておきましょうか。

フォルダ名は、慣例があり、「cordova-plugin-sampleplugin」となります。

> mkdir cordova-plugin-sampleplugin
> cd cordova-plugin-sampleplugin

plugin.xmlを作成

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>

大事なところを補足します。

cordova-plugin-sampleplugin/plugin.xml
    <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というコールバック登録関数を作成していきます。

cordova-plugin-sampleplugin\www\plugin_sampleplugin.js
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の作成

こんなファイルを作っておきます。

cordova-plugin-sampleplugin\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部分は以下のようになります。

cordova-plugin-sampleplugin\plugin.xml
    <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は、コールバック関数を受け取ります。いっしょにコールバック有効・無効も指定します。

cordova-plugin-sampleplugin\src\android\jp\or\sample\SamplePlugin\Main.java
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部分は以下のようになります。

cordova-plugin-sampleplugin\plugin.xml
    <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は、コールバック関数を受け取ります。いっしょにコールバック有効・無効も指定します。

cordova-plugin-sampleplugin\src\ios\SamplePlugin.swift
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バージョン

終わりに

以下も参考にしてください。

 Cordovaアプリ開発の備忘録

以下のページを参考にさせていただきました(ありがとうございました)

 https://tech-blog.rakus.co.jp/entry/20220207/cordva

作成したソースコードをGitHubに上げておきました。

以上

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?