Creazione di un plugin per Apache Cordova

Creazione di un plugin per Apache Cordova

Di solito nello scegliere un framework per creare app mobile multipiattaforma ci si basa quasi esclusivamente sulla semplicità d’uso. In realtà la vera killer feature di questa tipologia di framework è un’altra: l’estendibilità attraverso plugin. Il motivo della centralità di questa caratteristica è presto detto: qualsiasi framework decideremo di utilizzare non potrà coprire tutte possibili funzionalità che ci mettono a disposizione gli SDK nativi. In questo post ci occuperemo di come creare un plugin Android e iOS per Apache Cordova: famoso framework per creazione di applicazioni ibride. Questo framework è alla base di Ionic di cui ho parlato in uno degli ultimi post.

Setup

Il primo passo per creare un nuovo plugin è quello di far sapere alla nostra applicazione Cordova dove poter trovare il plugin che andremo a sviluppare. Per fare questo modifichiamo il file fetch.json all’interno della cartella plugins come segue:

{
    //...Altri plugin già presenti nell'applicazione
    "net.extrategy.cordova": {
        "source": {
            "type": "local",
            "path": "plugin-stub"
        }
    }
}

net.extrategy.cordova è l’id del nostro plugin. L’attributo source indica che il plugin è locale al nostro progetto mentre path ci dice che sarà presente nella cartella plugin-stub.

Plugin filesystem

Struttura di un plugin Cordova

Definizione

Tutte le proprietà di un plugin Cordova vengono definite all’interno del file plugin.xml presente all’interno della root del plugin stesso. La configurazione base di un plugin è la seguente:

<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android" id="net.extrategy.cordova" version="0.0.1">
    <name>Stub</name>
    <description>Cordova Plugin Stub</description>
    <license>MIT</license>
    <author>e-xtrategy</author>
    <js-module src="stub.js" name="Stub">
        <clobbers target="Stub" />
    </js-module>
</plugin>

Come potete vedere abbiamo definito un js-module presente nel file stub.js. Questi moduli rappresentano i bridge tra il mondo Cordova e il mondo nativo. L’utilizzo dell’elemento clobbers ci permette di specificare a quale proprietà dell’oggetto window sarà agganciato il nostro modulo. In questo caso infatti, per accedere alle funzionalità del modulo dovremmo utilizzare window.Stub. Vediamo insieme il codice del nostro modulo: lo scopo del plugin sarà quello di effettuare due operazioni sulle stringhe, reverse e replicate.

module.exports = {
    reverse: function (input, successCallback, errorCallback) {
    	cordova.exec(successCallback, errorCallback, "Stub", "reverse", [input]);
    },
    replicate: function (input, successCallback, errorCallback) {
    	cordova.exec(successCallback, errorCallback, "Stub", "replicate", [input]);
    }
};

Come vedete ognuna delle funzioni non è altro che un wrapper della funzione cordova.exec. Questa funzione è il cuore del bridge verso il nativo e accetta i seguenti parametri:

  • Callback di success
  • Callback di error
  • Nome del modulo
  • Azione da invocare
  • Array di parametri

Android

Passiamo ora al primo dei due moduli nativi, quello per Android. Prima di analizzare il codice, vediamo insieme come cambia la configurazione all’interno del file config.xml quando dobbiamo lavorare sulla piattaforma Google.

<platform name="android">
    <config-file target="res/xml/config.xml" parent="/*">
        <feature name="Stub">
            <param name="android-package" value="net.extrategy.cordova.Stub" />
        </feature>
    </config-file>
    <source-file src="src/android/Stub.java" target-dir="src/net/extrategy/cordova" />
</platform>

Come potete vedere dobbiamo definire nome e package della classe Java che comporrà il nostro modulo, di cui potete vedere il codice qui sotto:

package net.extrategy.cordova;

import org.apache.cordova.*;
import org.json.JSONArray;
import org.json.JSONException;

public class Stub extends CordovaPlugin {

    private String reverse(String input){
        return new StringBuilder(input).reverse().toString();
    }

    private String replicate(String input){
        return input+input;
    }

    @Override
    public boolean execute(String action, JSONArray data, CallbackContext callbackContext) throws JSONException {
        
        String input = data.getString(0);
        
        if (action.equals("reverse")) {
            
            callbackContext.success(reverse(input));

            return true;

        } else if (action.equals("replicate")) {
            
            callbackContext.success(replicate(input));

            return true;

        }else {
            return false;
        }
    }
}

Come vedete la parte Android del nostro plugin deve estendere la classe CordovaPlugin e implementare il metodo execute. Al suo interno il modulo mappa la action ricevuta in ingresso con i metodi reverse e replicate che implementano la vera business logic del modulo nativo.

iOS

Vediamo ora l’altra faccia delle medaglia, quella nativa iOS. In maniera analoga a quanto fatto in precedenza, partiamo dal file config.xml:

<platform name="ios">
    <config-file target="config.xml" parent="/widget">
        <feature name="Stub">
            <param name="ios-package" value="Stub" />
        </feature>
    </config-file>
    <header-file src="src/ios/Stub.h" target-dir="StubPlugin" />
    <source-file src="src/ios/Stub.m" target-dir="StubPlugin" />
</platform>

Come potete vedere qui dobbiamo definire qual è il file di header e il suo relativo file di source. Analizziamo insieme il codice di entrambi i file:

Header

#import <Cordova/CDV.h>

@interface Stub : CDVPlugin

- (void) reverse:(CDVInvokedUrlCommand*)command;
- (void) replicate:(CDVInvokedUrlCommand*)command;

@end

Source

#import "Stub.h"

@implementation Stub

- (void)reverse:(CDVInvokedUrlCommand*)command
{

    NSString* callbackId = [command callbackId];
    NSString* input = [[command arguments] objectAtIndex:0];

    NSMutableString *output = [NSMutableString string];
    NSInteger charIndex = [input length];
  	while (charIndex > 0) {
      	charIndex--;
      	NSRange subStrRange = NSMakeRange(charIndex, 1);
      	[output appendString:[input substringWithRange:subStrRange]];
  	}
	
    CDVPluginResult* result = [CDVPluginResult
                               resultWithStatus:CDVCommandStatus_OK
                               messageAsString:output];

    [self success:result callbackId:callbackId];
}

- (void)replicate:(CDVInvokedUrlCommand*)command
{

    NSString* callbackId = [command callbackId];
    NSString* input = [[command arguments] objectAtIndex:0];
    NSString* output = [input stringByAppendingString:input];

    CDVPluginResult* result = [CDVPluginResult
                               resultWithStatus:CDVCommandStatus_OK
                               messageAsString:output];

    [self success:result callbackId:callbackId];
}

@end

La cosa interessante da notare rispetto al modulo Android è la mancanza di boilerplate causata dal metodo execute. Nel caso di iOS basta far sì che i metodi si chiamino esattamente come le action che ci vengono passate da cordova.exec e il tutto funziona automaticamente.

Utilizzo

In questo prossimo esempio vediamo infine come utilizzare il nostro plugin.

<!DOCTYPE html>
<html>
    <head>
        <meta name="format-detection" content="telephone=no">
        <meta name="msapplication-tap-highlight" content="no">
        <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
        <title>Cordova Plugin stub</title>
    </head>
    <body>
        <h1>Cordova plugin stub</h1>
        <input id="textInput" type="text" value="Try Me Bro!"></input>
        <button onclick="reverse();">Reverse</button>
        <button onclick="replicate();">Replicate</button>
        <script type="text/javascript" src="cordova.js"></script>
        <script type="text/javascript">
            var app = {
                reverse:function(value){
                    window.Stub.reverse(value,
                        function(value){
                            alert(value);
                        },
                        function(error){
                            alert(error);
                        }
                    );
                },
                replicate:function(value){
                    window.Stub.replicate(value,
                        function(value){
                            alert(value);
                        },
                        function(error){
                            alert(error);
                        }
                    );
                }
            };

            function reverse () {
                app.reverse(document.getElementById('textInput').value);
            }

            function replicate () {
                app.replicate(document.getElementById('textInput').value);
            }
        </script>

    </body>
</html>

Il risultato del nostro lavoro è il seguente:

Reverse di una stringa tramite un modulo nativo

Reverse di una stringa tramite un modulo nativo

Ora che abbiamo analizzato tutti gli elementi che compongono un plugin di Cordova possiamo desumerne il flusso di utilizzo:

  1. L’applicazione invoca uno dei metodi dell’oggetto window.Stub
  2. Il modulo javascript invoca il metodo cordova.exec
  3. Cordova invoca il modulo nativo Android o iOS
  4. Il risultato torna all’applicazione web tramite le callback

Conclusioni

Purtroppo l’estensione di Cordova tramite plugin è uno di quegli argomenti che non vengono mai trattati nei tutorial getting started e anche nella documentazione ufficiale viene solo accennato. Spero quindi che questa micro-introduzione all’argomento possa essere d’aiuto a tutti gli sviluppatori di applicazioni ibride. Chi vuole approfondire può trovare il codice di questo post su GitHub. Alla prossima.

Francesco Strazzullo

Faccio il Front-end developer per e-xtrategy dove mi occupo di applicazioni AngularJS e mobile. In passato ho lavorato principalmente con applicazioni con stack Spring+Hibernate+JSF 2.X+Primefaces. Sono tra i collaboratori del progetto Primefaces Extensions: suite di componenti aggiuntivi ufficialmente riconosciuta da Primefaces. Sono anche uno dei fondatori del progetto MaterialPrime: una libreria JSF che segue le direttive del Material Design di Google. 

  • Giorgio Mandolini

    Ciao Francesco,
    ora serve lo step 2: integrare lib e framework di terze parti nativi 🙂

    • Hai già qualcosa in mente?

      • Giorgio Mandolini

        Molti framework di advertising sono chiusi e si portano dietro le proprie risorse, sono probabilmente il caso più completo perché devi integrare sia il codice che il modo di fargli reperire i propri assets