ここのところ、Cordovaベースのハイブリッドアプリ開発フレームワークであるIonic Frameworkでアプリを作って公開しました。
マネタイズのために広告を入れる必要があったのですが、とある事情でAdMobを入れることができなかったので、Nend広告を導入することに決めました。
AdMobは既存の素晴らしいCordovaプラグインがあって、バナー広告やインタースティシャル広告をJavaScriptからネイティブコード経由で呼び出すことができます。しかし、Nendにはそのようなプラグインは公開されていなかったので、自分で作ることにしました。
作ったプラグイン : https://github.com/TakayukiSakai/cordova-plugin-nend
本記事では、このプラグインのver0.1.0を開発・公開するまでの過程を細かく説明していきたいと思います。特に、 SDKという第三者がネイティブプラットフォーム用に書いたコードをJavaScriptから呼び出せるようにするという点で、他の記事にはない情報を提供できると思います。
Cordovaプラグイン開発の日本語記事はまだまだ少ないので、それに貢献できればいいな...。
本記事の対象とする人
- Cordova/PhoneGapで少しはアプリを作ったことがある
- Cordovaのプラグインの仕組みを知りたい
- Cordovaのプラグインを作ってみたい
- iOS/Android用SDK開発に携わっていて、SDKをCordova/PhoneGapにも対応させろと
クソ上司から迫られている
実際にプラグインを作るためには、対応させたいプラットフォームのネイティブコードを書く必要があります。
iOSに対応したプラグインを作るためにはObjective-Cを、AndroidのためにはJavaを多少は書かなくてはなりません。
プラグイン作成ステップ
プラグインを公開するまでのステップはだいたい以下のようになります。
- プラットフォームごとにテストアプリを作って、ネイティブコードからSDKの関数を叩いてみる(動作確認・依存パッケージの確認)
- plugmanコマンドで雛形作成
- jsから呼び出す関数を設計・実装
- ネイティブコード(Objective-C/Java)を書いて、それぞれのプラットフォーム(iOS/Androidへ対応)
- plugmanコマンドでプラグインを公開
プラットフォームごとにテストアプリを作って、ネイティブコードからSDKの関数を叩いてみる
さて、プラグインを作る以上、自分自身がネイティブコードでSDKを触ってみないことには始まりません。
また、SDKが依存しているパッケージなどを洗い出す上でも、プラットフォームごとにテストアプリを作って動作確認をするのはマストです。
当たり前のことですが、 SDKをラップしたプラグインを開発するのは、そのSDKを使って普通のネイティブアプリを作るより難易度が高いです。
そこで、このテストアプリ作成時点でつまづいた点は、プラグイン開発時にもつまづく可能性が高いので、ちゃんと覚えておきましょう。
無事にiOS/AndroidそれぞれのテストアプリでSDKが利用できたら、プラグイン開発の始まりです。
plugmanコマンドで雛形作成
まず、Cordovaのプラグインを操作するコマンドであるplugman
コマンドをインストールします。
$ npm install -g plugman@0.23.0
ここで、わざわざ少し古いバージョンのplugman
をインストールしているのは、このページに書かれているように、新しいバージョンだとバグがあって、プラグイン公開時にエラーになるからです。
次に、plugman create
コマンドでプラグインの雛形を作ります。
plugman create --name cordova-plugin-nend --plugin_id com.effers.kaky.nend --plugin_version 0.1.0
ここで指定した名前やidなどは plugin.xml
の中に書かれており、後からいくらでも変更できるので、今は深く悩む必要はありません。
jsから呼び出す関数を設計・実装
設計
まず、js側から呼び出す関数を設計します。ここで設計した関数が将来このプラグインを利用するユーザから呼ばれることになるので、なるべく使いやすさを考慮します。
また、今回のプラグインの場合はあくまでSDKのネイティブコードをjsから呼べるようにするためのものでしかないので、提供できる機能はSDKの機能に依存します。
そのため、この設計時点でSDKの機能はほぼ把握しておく必要があります。
僕が実装したNend SDKのドキュメントを読む感じ、インタースティシャル広告を表示するまでに
- 広告IDの指定
- 広告のプリロード(起動直後に呼ぶ用)
- 広告の表示(任意のタイミング)
の3ステップを踏めばよいようでした。
広告のリフレッシュは、前の広告が非表示になった後にSDK側で自動でやってくれるようなので、上記の3つの機能を実現する関数だけ用意すればよさそうです。
ということで、シンプルに以下のような設計にしました。
Nend.setOptions({interstitialApiKey:"YOUR_API_KEY", interstitialSpotId:"YOUR_SPOT_ID"});
Nend.createInterstitial();
Nend.showInterstitial();
広告のロードに失敗したとき用のコールバックなどがありませんが、とりあえず最低限の実装ということで割愛しました。
実装
次に、先ほど設計した関数を実装していきます。
www/
フォルダ以下にJavaScriptファイルの実装ファイルを置くのが通例です。
plugman createで雛形作成したときに cordova-plugin-nend.js
とかいう長ったらしい名前のファイルが作成されていると思いますが、ファイル名はなんでもいいので nend.js
に変更します。
このとき、plugin.xml
内の以下の部分も変更します。
<js-module name="cordova-plugin-nend" src="www/nend.js"> <!-- src属性の中を変更 -->
<clobbers target="cordova.plugins.cordova-plugin-nend" />
</js-module>
この nend.js
ファイルの中で、module.exports
に定義した関数が、ユーザから使われることになります。
まずは、一番簡単な setOptions()
から実装します。
この関数はユーザが設定した値を保持するだけなので、ネイティブのコードを呼び出す必要はなく、以下のようにしました。
var options = {};
module.exports.setOptions = function(args) {
options = args;
};
次に、広告をロードする createInterstitail()
を実装します。
module.exports.createInterstitial = function() {
cordova.exec(
function() {}, // 成功時のコールバック
function() {}, // 失敗時のコールバック
'Nend', // サービス名
'createInterstitial', // アクション名
[options] // オプション
);
};
ここで 超重要関数 cordova.exec()
が登場しました。この関数がネイティブのコードを呼び出してくれます。
公式のドキュメントに書いてあることをざっくり説明すると、cordova.exec()
はネイティブコードの中の「指定したサービス(クラス)の中に定義してあるアクション(メソッド)を呼ぶよ」という感じですかね。
これらの「サービス」や「アクション」のネイティブコードは、これから自分で実装してやる必要があります。
showInterstitial()
は createInterstitial()
とほとんど同じなので割愛します。
ネイティブコードの実装
さぁ、ついに山場のネイティブコードの実装です。
ネイティブコードの実装は、その中でもさらに以下の2ステップに分けられます。
- plugin.xmlにネイティブコードの実装ファイルや今回ラップするSDKの場所、その他依存パッケージを記述
- ネイティブコードを書く
iOS/Androidそれぞれ、上記2ステップについて順に説明していきます。
iOS
plugin.xmlへの記述
iOS用の実装ファイルは、src/ios/
以下におきます。
cordova-plugin-nendプラグインの場合は、src/ios
以下は、下記のようにファイルを配置しています。
.
+-- CDVNend.h // ヘッダファイル
+-- CDVNend.m // 実装ファイル
+-- _NendAd // Nendから提供されているiOS用SDK
| +-- NADView.h
| +-- NADInterstitial.h
| +-- libNendAd.a
そして、このディレクトリ構造に対応するように plugin.xml
に記述していきます。
<platform name="ios">
<config-file target="config.xml" parent="/*">
<feature name="Nend"> <!-- サービス名(cordova.exec()のサービス名と同じものを使う) -->
<param name="ios-package" value="CDVNend"/> <!-- 実装ファイルのクラス名 -->
<param name="onload" value="true"/> <!-- アプリ起動時にプラグインも初期化するオプション -->
</feature>
</config-file>
<!-- 自分で実装したファイル -->
<header-file src="src/ios/CDVNend.h" />
<source-file src="src/ios/CDVNend.m" />
<!-- Nend SDK -->
<header-file src="src/ios/NendAd/NADView.h" />
<header-file src="src/ios/NendAd/NADInterstitial.h" />
<source-file src="src/ios/NendAd/libNendAd.a" framework="true" /> <!-- .aファイルはframeworkとして登録する必要あり -->
<!-- Nend SDKが依存しているフレームワーク -->
<framework src="AdSupport.framework"/>
<framework src="Security.framework"/>
<framework src="ImageIO.framework"/>
</platform>
ここでは、以下の点に注意してください。
-
<feature name="Nend">
の部分のname属性をcordova.exec()
から呼ぶときのサービス名と同じにする - SDKなどで配布されている
.a
拡張子のファイルは、framework="true"
属性をつけて、フレームワークとして登録しないといけない
Objective-Cの実装
ヘッダファイルには以下のように、cordova側から呼ばれる関数を記述します。
# import <Cordova/CDV.h>
# import "NADInterstitial.h" // Nend SDKのインポート
@interface CDVNend : CDVPlugin <NADInterstitialDelegate>
- (void)createInterstitial:(CDVInvokedUrlCommand *)command;
- (void)showInterstitial:(CDVInvokedUrlCommand *)command;
@end
なにやら引数として CDVInvokedUrlCommand
なるオブジェクトが渡されています。
このオブジェクトには、jsコードから渡された引数やコールバックを呼ぶために必要な情報が含まれています。
実装ファイルは以下のようになります。(showInterstitial()
は割愛。)
# import "CDVNend.h"
@implementation CDVNend
- (void)createInterstitial:(CDVInvokedUrlCommand *)command {
CDVPluginResult *pluginResult;
NSString *callbackId = command.callbackId;
// jsコードから渡された引数を取得
NSDictionary* options = [command argumentAtIndex:0 withDefault:[NSNull null]];
// Nend SDKの関数を実行
[[NADInterstitial sharedInstance] loadAdWithApiKey:[options objectForKey:@"interstitialApiKey"]
spotId:[options objectForKey:@"interstitialSpotId"]];
// 実行成功フラグを立ててリターン
// js側で成功時のコールバックなどを指定してあればそれが実行される
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId];
}
@end
大したことしてない割に長くなってしまいました。
正直今回の場合、以下の2行以外はおまじないと思っても構いません。
NSDictionary* options = [command argumentAtIndex:0 withDefault:[NSNull null]];
[[NADInterstitial sharedInstance] loadAdWithApiKey:[options objectForKey:@"interstitialApiKey"]
spotId:[options objectForKey:@"interstitialSpotId"]];
デバッグ方法
プラグインをテストするためのcordovaプロジェクトにcordova-plugin-consoleを入れると、コンソールログをXcodeで見れるので、地道にプリントデバッグしてました。
Android
plugin.xmlへの記述
Android用の実装ファイルは、src/android/
以下におきます。
cordova-plugin-nendプラグインの場合は、src/android
以下は、下記のようにファイルを配置しています。
.
+-- Nend.java // 実装ファイル
+-- _NendAd // Nendから提供されているAndroid用SDK
| +-- nendSDK-2.6.1.jar
そして、このディレクトリ構造に対応するように plugin.xml
に記述していきます。
<platform name="android">
<config-file target="res/xml/config.xml" parent="/*">
<feature name="Nend"> <!-- サービス名(cordova.exec()のサービス名と同じものを使う) -->
<param name="android-package" value="com.effers.kaky.nend.Nend"/> <!-- 実装ファイルのクラス(フルパス) -->
</feature>
</config-file>
<!-- Nend SDKが必要としているパーミッション -->
<config-file target="AndroidManifest.xml" parent="/*">
<uses-permission android:name="android.permission.INTERNET"/>
</config-file>
<!-- 自分で実装したファイル(クラス名に対応したディレクトリに配置) -->
<source-file src="src/android/Nend.java" target-dir="src/com/effers/kaky/nend" />
<!-- Nend SDK(.jarファイルはlibsフォルダに配置) -->
<source-file src="src/android/NendAd/nendSDK-2.6.1.jar" target-dir="libs" />
<!-- Nend SDKが依存しているフレームワーク -->
<framework src="com.google.android.gms:play-services-ads:+" />
</platform>
iOSの場合と違って、source-file
タグに、 ソースファイルをどこに配置するかを手動で指定する必要があります。
Javaの実装
Javaの場合はヘッダファイルはないので、実装ファイルのみです。以下のようになりました。
package com.effers.kaky.nend;
import org.apache.cordova.*;
import net.nend.android.NendAdInterstitial;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
public class Nend extends CordovaPlugin {
@Override
public boolean execute(String action, JSONArray inputs, final CallbackContext callbackContext) throws JSONException {
PluginResult result = null;
// WebViewのアクティビティを取得
final Activity activity = cordova.getActivity();
// if文でactionを判定して処理を分岐
if (action.equals("createInterstitial")) {
// jsからの引数を取得
JSONObject options = inputs.optJSONObject(0);
// Nendの関数を実行
NendAdInterstitial.loadAd(
activity.getApplicationContext(),
options.getString("interstitialApiKey"),
options.getInt("interstitialSpotId")
);
callbackContext.success();
} else if (action.equals("showInterstitial")) {
// UIを操作する処理は、メインスレッドで
activity.runOnUiThread(new Runnable() {
public void run() {
// Nendの関数を実行
NendAdInterstitial.showAd(activity);
callbackContext.success();
}
});
}
if(result != null) callbackContext.sendPluginResult( result );
return true;
}
}
Androidの場合、iOSとは違って jsのcordova.exec()
でアクションに何を指定しても、Java側では CordovaPlugin#execute
が呼ばれます。
その引数として action
変数が文字列で渡されるので、if文で分岐して処理する感じになります。
1行目のパッケージ名などは、plugin.xml
に記述したものと対応しているかちゃんと確認しましょう。
デバッグ方法
adb logcat
で地道にプリントデバッグ...。
プラグインを公開
この公式ページの一番下にある通りに、
$ plugman adduser
$ plugman publish /path/to/your/plugin
とすれば公開されます。ただし、最初の方も書きましたが2015/07/20現在、最新のplugmanコマンドだと plugman publish
時にエラーになるので、少し古いバージョン(0.23.0)のものをいれてください。
config.xml
の plugin
タグのid
属性に指定したものが、このプラグインのidとして使われるので、公開する前にはちゃんとしたidをつけましょう。
まとめ
- 使いたいSDK用のプラグインがなくても諦めちゃダメ!
- プラグイン開発には、対応したいプラットフォームのネイティブコードを書ける必要あり
- SDKの依存関係周りを
plugin.xml
に記述するのが意外と大変 - まだまだプラグイン開発用のデバッグ方法が確立されてるとは言えない(?)
最後に
せっかくなのでNendの中の人と話してみたいです。