ionic3のNative pluginを作る #ionic #cordova

シェアする

この記事はIonic Advent Calendar 2017の12/10のエントリーです。

2016年6月に『最新のテクノロジーを使って、新しい音楽の体験を生み出し、「音楽を演奏する人」「音楽を聴く人」にとって、より幸せな世界をつくる』というミッションを胸に会社を2人で起業しました。

今は、弦楽器の練習をサポートするシステムBowingVisionを自社開発していて、世にリリースするべく開発に励んでいます。

最近、ionicを使ってモバイルアプリを作ることがよくあります。ionicには色々なプラグインが用意されていてスマートホンの様々な機能を使うことができるようになっています。また、Cordovaのプラグインもそのまま使うことができるので、大抵のことは既存のプラグインで実現できます。

しかし、それでも、独自の機能を入れたプラグインがあれば、さらに便利になることもあります。そこで、ionic3でNative pluginを作って見ました。

ionicは進化が激しいので、pluginの作り方も少しずつ変わっています。今回の作成方法も現時点での作り方ですので、将来変更になる可能性はあります。今回使用したバージョンは3.19.0です。

また、開発環境はMac OSX 10.13.2を使っています。

Native pluginとは

Ionicで使えるプラグインには、Ionic native pluginとCordovaプラグインの2つがあります。この2つの主な違いは呼び出し方にあります。

Cordova plugin

Cordovaのプラグインは、グローバルオブジェクトとしてインストールされるので、使用するときにはプラグインのオブジェクトを使います。ただし、TypeScriptでは未定義のオフジェクトを使おうとするとコンパイルエラーになりますので、定義が必要となります。

例えば、ExamplePluginという名前のプラグインを使用する場合には、以下のようにdeclareなどで宣言する必要があります。

import { Component } from '@angular/core';
import { NavController, Platform } from 'ionic-angular';

declare var ExamplePlugin: any;

@Component({
    selector: 'page-home',
    templateUrl: 'home.html'
})
export class HomePage {

    constructor(public navCtrl: NavController, public platform: Platform) {
        this.platform.ready().then(() => {
            ExamplePlugin.func('abc');
        });
    }
}
Ionic native plugin

Ionic native pluginはCordova pluginのラッパーです。プラグインの実体はCordova pluginですが、ブラグイン用のオブジェクトを無理やり?定義することなく、使用できるようにするためのものです。ExamplePluginがNative pluginになっていれば以下のように使うことができます。

import { Component } from '@angular/core';
import { NavController, Platform } from 'ionic-angular';
import { ExamplePlugin } from '@ionic-native/example-plugin';

@Component({
    selector: 'page-home',
    templateUrl: 'home.html'
})
export class HomePage {

    constructor(public navCtrl: NavController, public platform: Platform, private examplePugin: ExamplePlugin) {
        this.platform.ready().then(() => {
            this.examplePlugin.func('abc');
        });
    }
}

Cordova Pluginの作成

Ionic Native PluginはCordova Pluginをラップしたものなので、まず、Cordova Pluginを作成します。

プロジェクトの作成

プラグイン開発のためのプロジェクトを作ります。プロジェクト名を”my-plugin”とします。

$ ionic start my-plugin blank
$ cd my-plugin

Pluginのテンプレートのダウンロード

ionic-teamがプラグインのテンプレートを公開していますので、今回はこれを利用します。プラグイン開発用のディレクトリとしてplugins-devを作成し、その中にmy-cordova-pluginというパッケージ名でテンプレートをクローンします。

$ mkdir plugins-dev
$ cd plugins-dev
$ git clone https://github.com/ionic-team/cordova-plugin-template.git my-cordova-plugin

テンプレートの修正

今日の時点でのテンプレートにはいくつか問題があるので、最低限それらを直す必要があります。

package.jsonの修正

以前のpluginには必要ありませんでしたが、現在はこのファイルがないとプラグインとして追加することができません。package.jsonの内容は以下の通りです。

{
    "name": "cordova-plugin-template",
    "version": "1.0.0",
    "cordova": {
    "id": "cordova-plugin-template",
    "platforms": [
        "android",
        "ios",
    ]
    },
    "description": "A starting point for a Cordova Plugin"
}
 8行目の”ios”の後の”,”を削除します。これを削除しないとプラグインを追加するときにエラーとなります。また、”name”と”id”を今回のパッケージ名”my-cordova-plugin”に変更します。特に、”name”はパッケージ名と一致していないと、後でプラグインを更新できなくなります。修正したファイルは以下になります。
{
    "name": "my-cordova-plugin",
    "version": "1.0.0",
    "cordova": {
        "id": "my-cordova-plugin",
        "platforms": [
            "android",
            "ios"
        ]
    },
    "description": "A starting point for a Cordova Plugin"
}

plugin.xmlの修正

最初のplugin.xmlは以下のようになっています。

<?xml version="1.0" encoding="UTF-8"?>

<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
    id="my-cordova-plugin"
    version="1.0.0">
    <name>Cordova Plugin Template</name>
    <description></description>
    <license>MIT</license>
    <keywords></keywords>
    <repo>https://github.com/driftyco/cordova-plugin-template.git</repo>
    <issue>https://github.com/driftyco/cordova-plugin-template/issues</issue>

    <!-- android -->
    <platform name="android">
        <js-module src="www/plugin.js" name="plugin">
            <runs/>

            <!-- This is the window variable name you want, like window.MyCordovaPlugin -->
            <clobbers target="MyCordovaPlugin" />
        </js-module>
        <config-file target="res/xml/config.xml" parent="/*">
            <feature name="MyCordovaPlugin">
                <param name="android-package" value="com.example.MyCordovaPlugin" />
                <param name="onload" value="true" />
            </feature>
        </config-file>

        <source-file src="src/android/com/example/MyCordovaPlugin.java" target-dir="src/com/example/" />
    </platform>

    <!-- ios -->
    <platform name="ios">
        <js-module src="www/plugin.js" name="plugin">
            <runs/>
            <clobbers target="MyCordovaPlugin" />
        </js-module>
        <config-file target="config.xml" parent="/*">
            <feature name="MyCordovaPlugin">
                <param name="ios-package" value="MyCordovaPlugin" onload="true" />
            </feature>
        </config-file>

        <header-file src="src/ios/MyCordovaPlugin.h" />
        <source-file src="src/ios/MyCordovaPlugin.m" />
    </platform>
</plugin>

今回はテンプレートで使っているパッケージ名”my-cordova-pugin”やプラグイン名”MyCordovaPlugin”と同じにしているので、修正するところはほとんどありません。別の名前を使う場合はそれに合わせてプラグイン名を全部修正します。

今回修正したplugin.xmlは以下の通りです。

<?xml version="1.0" encoding="UTF-8"?>

<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
    id="my-cordova-plugin"
    version="1.0.0">
    <name>My Cordova Plugin</name>
    <description></description>
    <license>MIT</license>
    <keywords></keywords>

    <!-- android -->
    <platform name="android">
        <js-module src="www/plugin.js" name="plugin">
            <runs/>

            <!-- This is the window variable name you want, like window.MyCordovaPlugin -->
            <clobbers target="MyCordovaPlugin" />
        </js-module>
        <config-file target="res/xml/config.xml" parent="/*">
            <feature name="MyCordovaPlugin">
                <param name="android-package" value="com.example.MyCordovaPlugin" />
                <param name="onload" value="true" />
            </feature>
        </config-file>

        <source-file src="src/android/com/example/MyCordovaPlugin.java" target-dir="src/com/example/" />
    </platform>

    <!-- ios -->
    <platform name="ios">
        <js-module src="www/plugin.js" name="plugin">
            <runs/>
            <clobbers target="MyCordovaPlugin" />
        </js-module>
        <config-file target="config.xml" parent="/*">
            <feature name="MyCordovaPlugin">
                <param name="ios-package" value="MyCordovaPlugin" onload="true" />
            </feature>
        </config-file>

        <header-file src="src/ios/MyCordovaPlugin.h" />
        <source-file src="src/ios/MyCordovaPlugin.m" />
    </platform>
</plugin>

plugin.jsの修正

wwwディレクトリの下にあるplugin.jsもプラグイン名に合わせて編集します。今回はプラグイン名を変更しませんでしたので、そのまま使います。plugin.jsは次のようになっています。

var exec = require('cordova/exec');

var PLUGIN_NAME = 'MyCordovaPlugin';

var MyCordovaPlugin = {
  echo: function(phrase, cb) {
    exec(cb, null, PLUGIN_NAME, 'echo', [phrase]);
  },
  getDate: function(cb) {
    exec(cb, null, PLUGIN_NAME, 'getDate', []);
  }
};

module.exports = MyCordovaPlugin;

MyCordovaPluginを使ってみる

ここまでできればテンプレートのプラグインを動かすことができます。

Pluginの追加

最初にプラグインをプロジェクトに追加します。

$ ionic cordova plugin add plugins-dev/my-cordova-plugin

以下のようにしてプラグインが追加されたことを確かめることができます。

$ ionic cordova plugin ls
> cordova plugin ls
my-cordova-plugin 1.0.0 "My Cordova Plugin"

Pluginの更新

プラグインを修正するなどして更新したい場合は、一旦削除して再度インストールします。

$ ionic cordova plugin rm my-cordova-plugin
$ ionic cordova plugin add plugins-dev/my-cordova-plugin

削除するときはプラグインのパッケージ名を指定します。パッケージ名は上記のlsコマンドで確認できます。

もし、万一、プラグインのpackage.jsonのnameを直し忘れ、パッケージ名と事なってしまった場合は、削除した後の追加がエラーになります。その場合は、アプリの方のpackage.jsonを見て、間違ったパッケージ名をnpm コマンドでアンインストールします。

$ npm uninstall --save <間違ったパッケージ名>

そうすれば、再度プラグインを追加できるようになります。

アプリの作成

このプラグインは2つのメソッドを持っています。

// 引数の文字列をログとして出力する
MyCordovaPlugin.echo("Hello!");
// 現在の時間をコールバックで返す
MyCordovaPlugin.getDate(date => {
    console.log(date);
});

確認するアプリを作るためにsrc/pages/homeの中のファイルを修正してきます。まず、home.htmlを以下のように変更します。

<ion-header>
  <ion-navbar>
    <ion-title>
      My Cordova Plugin
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <ion-item>
    Now: {{now}}
  </ion-item>
</ion-content>

次にhome.tsを以下のようにします。今回のプラグインは”MyCordovaPlug”という名前でグローバルオブジェクトとして登録されます。したがって、window.MyCordovaPluginまたはMyCordovaPlugin という名前で呼び出すことができます。以下のコードでは、MyCordovaPluginを宣言して、呼び出せるようにしています。

また、constructorが呼ばれたたいみんぐでプラグインが読み込み終わっているとは限らないので、platform.ready() を使ってプラグインが読み終わったタイミングで呼び出すようにしてます。

import { Component } from '@angular/core';
import { NavController, Platform } from 'ionic-angular';

declare var MyCordovaPlugin: any;

@Component({
    selector: 'page-home',
    templateUrl: 'home.html'
})
export class HomePage {

    public now: string;

    constructor(public navCtrl: NavController, public platform: Platform) {
        this.platform.ready().then(() => { 
            MyCordovaPlugin.echo('Hello MyCordova Plugin'); 
            MyCordovaPlugin.getDate(date => { 
                this.now = date; 
            }); 
        }); 
    }
}

アプリを動かす

それではiPhoneでアプリを動かしてみます。ionicにiosのプラットフォームを追加して、Xcodeを起動します。

$ ionic cordova platform add ios
$ ionic cordova prepare
$ open platforms/ios/MyApp.xcworkspace/

Xcodeが起動したら、singingの設定を行い、”Build and Run”します。問題なく起動すれば、Xcodeのコンソールにechoした文字列が表示されます。

2017-12-12 11:49:01.054526+0900 MyApp[11733:844796] DiskCookieStorage changing policy from 2 to 0, cookie file: file:///Users/tetsuo/Library/Developer/CoreSimulator/Devices/714515F2-3CB2-4A6C-A0A0-F07B468E1E92/data/Containers/Data/Application/077502F7-0DE1-4B89-8748-BB097DD0E802/Library/Cookies/io.ionic.starter.binarycookies
2017-12-12 11:49:01.186879+0900 MyApp[11733:844796] Apache Cordova native platform version 4.4.0 is starting.
2017-12-12 11:49:01.187687+0900 MyApp[11733:844796] Multi-tasking -> Device: YES, App: YES
2017-12-12 11:49:01.243561+0900 MyApp[11733:844796] CDVWKWebViewEngine: trying to inject XHR polyfill
2017-12-12 11:49:01.577821+0900 MyApp[11733:844796] [MC] Lazy loading NSBundle MobileCoreServices.framework
2017-12-12 11:49:01.579284+0900 MyApp[11733:844796] [MC] Loaded MobileCoreServices.framework
2017-12-12 11:49:01.599056+0900 MyApp[11733:844796] CDVWKWebViewEngine will reload WKWebView if required on resume
2017-12-12 11:49:01.599216+0900 MyApp[11733:844796] Using Ionic WKWebView
2017-12-12 11:49:01.599794+0900 MyApp[11733:844796] [CDVTimer][handleopenurl] 0.128984ms
2017-12-12 11:49:01.602356+0900 MyApp[11733:844796] [CDVTimer][intentandnavigationfilter] 2.314031ms
2017-12-12 11:49:01.602999+0900 MyApp[11733:844796] [CDVTimer][gesturehandler] 0.096977ms
2017-12-12 11:49:01.603323+0900 MyApp[11733:844796] [CDVTimer][mycordovaplugin] 0.060022ms
2017-12-12 11:49:01.658638+0900 MyApp[11733:844796] [CDVTimer][splashscreen] 55.198014ms
2017-12-12 11:49:01.660286+0900 MyApp[11733:844796] [CDVTimer][keyboard] 1.402974ms
2017-12-12 11:49:01.660524+0900 MyApp[11733:844796] [CDVTimer][TotalPluginStartup] 60.863972ms
2017-12-12 11:49:06.491890+0900 MyApp[11733:844796] Hello MyCordova Plugin

また、画面上にも現在の時間が表示されます。

プラグインのコード

今回はテンプレートのコードをそのまま利用しましたが、独自の機能を実装するには、以下の”*”印がついたファイルを修正します。

my-cordova-plugin/
├── LICENSE
├── README.md
├── package.json
├── plugin.xml
├── src
│   ├── android
│   │   └── com
│   │       └── example
│   │           └── MyCordovaPlugin.java (*)
│   └── ios
│       ├── MyCordovaPlugin.h (*)
│       └── MyCordovaPlugin.m (*)
└── www
    └── plugin.js (*)

Ionic native pluginの作成

準備

先ほど作成したCordova pluginをIonic native pluginとして使えるようにします。まず、最初にnative pluginに必要なコードをhttps://github.com/ionic-team/ionic-nativeからクローンします。ディレクトリはどこでも良いのすが、plugins-devの下に置くことにします。

$ cd plugin-devs
$ git clone https://github.com/ionic-team/ionic-native.git

ionic-native/src/@ionic-native/pluginsの下には既存のIonic native pluginのソースコードが置いてあります。ビルドするときに邪魔になるので一旦pluginsの下を全て削除します。

$ rm -rf ionic-native/src/@ionic-native/plugins/*

Native Plugin用のファイルの生成

今回のNative Pluginを作るためのファイルを生成します。ionic-nativeのディレクトリの下で以下のコマンドを実行します。

$ cd ionic-native
$ gulp plugin:create -n MyCordovaPlugin

正常に終了すると以下のファイルが生成されます。

src/@ionic-native/plugins/my-cordova-plugin/index.ts

ファイルの編集

上で生成したファイルは以下のようになっています。

/**
 * This is a template for new plugin wrappers
 *
 * TODO:
 * - Add/Change information below
 * - Document usage (importing, executing main functionality)
 * - Remove any imports that you are not using
 * - Add this file to /src/index.ts (follow style of other plugins)
 * - Remove all the comments included in this template, EXCEPT the @Plugin wrapper docs and any other docs you added
 * - Remove this note
 *
 */
import { Injectable } from '@angular/core';
import { Plugin, Cordova, CordovaProperty, CordovaInstance, InstanceProperty, IonicNativePlugin } from '@ionic-native/core';
import { Observable } from 'rxjs/Observable';

/**
 * @name My Cordova Plugin
 * @description
 * This plugin does something
 *
 * @usage
 * ```typescript
 * import { MyCordovaPlugin } from '@ionic-native/my-cordova-plugin';
 *
 *
 * constructor(private myCordovaPlugin: MyCordovaPlugin) { }
 *
 * ...
 *
 *
 * this.myCordovaPlugin.functionName('Hello', 123)
 *   .then((res: any) => console.log(res))
 *   .catch((error: any) => console.error(error));
 *
 * ```
 */
@Plugin({
  pluginName: 'MyCordovaPlugin',
  plugin: '', // npm package name, example: cordova-plugin-camera
  pluginRef: '', // the variable reference to call the plugin, example: navigator.geolocation
  repo: '', // the github repository URL for the plugin
  install: '', // OPTIONAL install command, in case the plugin requires variables
  installVariables: [], // OPTIONAL the plugin requires variables
  platforms: [] // Array of platforms supported, example: ['Android', 'iOS']
})
@Injectable()
export class MyCordovaPlugin extends IonicNativePlugin {

  /**
   * This function does something
   * @param arg1 {string} Some param to configure something
   * @param arg2 {number} Another param to configure something
   * @return {Promise<any>} Returns a promise that resolves when something happens
   */
  @Cordova()
  functionName(arg1: string, arg2: number): Promise<any> {
    return; // We add return; here to avoid any IDE / Compiler errors
  }

}

まず、プラグインの定義部分を以下のように変更します。

@Plugin({
  pluginName: 'MyCordovaPlugin',
  plugin: 'my-cordova-plugin', // npm package name, example: cordova-plugin-camera
  pluginRef: 'MyCordovaPlugin', // the variable reference to call the plugin, example: navigator.geolocation
  repo: '', // the github repository URL for the plugin
  install: '', // OPTIONAL install command, in case the plugin requires variables
  installVariables: [], // OPTIONAL the plugin requires variables
  platforms: ['Android', 'iOS'] // Array of platforms supported, example: ['Android', 'iOS']
})

pluginNameはその下のクラス名と一致していなければなりません。pluginはパッケージ名、pluginRefはプラグイン名にします。最後にサポートするプラットフォームを記述します。

次にクラス内に必要なメソッドを追加します。今回のプラグインは2つのメソッドを持っているのでそれを記述します。

@Injectable()
export class MyCordovaPlugin extends IonicNativePlugin {

  /**
   * This function does something
   * @param arg1 {string} Some param to configure something
   * @param arg2 {number} Another param to configure something
   * @return {Promise<any>} Returns a promise that resolves when something happens
   */
  @Cordova()
  echo(arg1: string): void { }

  @Cordova()
  getDate(): Promise<any> {
    return;
  }
}

最後に使用していないモジュールを削除して、import文は以下のようになります。

import { Injectable } from '@angular/core';
import { Plugin, Cordova, IonicNativePlugin } from '@ionic-native/core';

Native Pluginのビルド

作成したNative Pluginラッパーをビルドして利用できるファイルを生成します。ビルドはNative Pluginのトップディレクトリ(プロジェクト/plugins-dev/ionic-native)で行います。

$ npm run build

ビルドが終了すると以下のディレクトリが作られ、その中に必要なファイルが生成されます。

dist/@ionic-native/my-cordova-plugin

Native Pluginのインストール

作成したNative Pluginをインストールします。単純にプロジェクトのnode_modules/@ionic-nativeの下にコピーしても使えますが、アプリのビルドなどをしていると消えることがあるので、インストールした方が良いようです。インストールはプロジェクトのトップで以下のコマンドを実行します。

$ cd ../..
$ npm install --save plugins-dev/ionic-native/dist/@ionic-native/my-cordova-plugin

これでNative Pluginもインストールされました。

ls -l node_modules/@ionic-native/
total 0
drwxr-xr-x  34 tetsuo  staff  1088 12 12 14:32 core
drwxr-xr-x   8 tetsuo  staff   256 12 12 14:42 http
lrwxr-xr-x   1 tetsuo  staff    67 12 12 17:45 my-cordova-plugin -> ../../plugins-dev/ionic-native/dist/@ionic-native/my-cordova-plugin
drwxr-xr-x  10 tetsuo  staff   320 12 12 14:32 splash-screen
drwxr-xr-x  10 tetsuo  staff   320 12 12 14:32 status-bar

Native Pluginの更新

インストールしたNative Pluginを更新するには一旦削除して、再度インストールします。

$ npm uninstall @ionic-native/my-cordova-plugin
$ npm install --save plugins-dev/ionic-native/dist/@ionic-native/my-cordova-plugin

Native Pluginを使う

次に今作成したNative Pluginを使用します。まず、アプリのファイルsrc/pages/home/home.tsを以下のように修正します。

import { Component } from '@angular/core';
import { NavController, Platform } from 'ionic-angular';
import { MyCordovaPlugin } from '@ionic-native/my-cordova-plugin';

@Component({
    selector: 'page-home',
    templateUrl: 'home.html'
})
export class HomePage {

    public now: string;

    constructor(public navCtrl: NavController, public platform: Platform, public myCordovaPlugin: MyCordovaPlugin) {
        this.platform.ready().then(() => {
            this.myCordovaPlugin.echo('Hello MyCordova Plugin>>>>');
            this.myCordovaPlugin.getDate().then(date => {
                this.now = date;
            });
        });
    }

}

また、src/app/app.modue.tsにもプラグインを使用するための設定を追加します。

import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core';
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';
import { MyCordovaPlugin } from '@ionic-native/my-cordova-plugin';

import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';

@NgModule({
  declarations: [
    MyApp,
    HomePage
  ],
  imports: [
    BrowserModule,
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage
  ],
  providers: [
    StatusBar,
    SplashScreen,
    {provide: ErrorHandler, useClass: IonicErrorHandler},
    MyCordovaPlugin
  ]
})
export class AppModule {}

これで準備はできました。Cordova Pluginの時と同様にビルドすればアプリが動作するが確認できます。

結果は、先ほどと全く同じです。

まとめ

  • Cordovaのプラグインは、手順さえ分かってしまえば、作るのはそれほど難しいものではありません。
  • プラグインの構造が分かれば、既存のプラグインを改造することも簡単にできます。
  • Ionic Native Pluginは、Cordovaのプラグインのラッパーなので、かなり簡単に作成できます。