Extender PhoneGap/Cordova creando un plugin

Después de jugar un rato con Cordova (antiguo PhoneGap), una de las cosas que más me ha gustado es lo fácil que es saltar del mundo HTML al mundo nativo utilizando los plugins que incluye Cordova o incluso creando tus propios plugins.

Arquitectura de PhoneGap/Cordova

OJO: Todo lo que voy a contar está basado en la versión para Android de Cordova. Supongo que, en líneas generales, será similar en el resto de versiones (iOS, BlackBerry, etc.).

La arquitectura global de Cordova es algo así:

Arquitectura de PhoneGap/Cordova

Arquitectura de PhoneGap/Cordova

Tenemos por una parte el mundo HTML y por otra el mundo nativo, cada uno con sus componentes. La única vía de comunicación entre ambos mundos es a través de Cordova. Cordova tiene 2 partes, una Javascript que reside en el mundo HTML y otra en Java (en el caso de Android) que reside en el mundo nativo.

Toda la funcionalidad existente en Cordova (GPS, acelerómetro, lista de contactos, …) está implementada como un plugin, con su parte Javascript y su parte nativa.

Podemos usar el mismo sistema que los plugins de Cordova para añadir nuevas funcionalidades. Para eso necesitaremos:

  • Crear la parte Javascript del plugin, que será el interface disponible para nuestra aplicación web.
  • Crear la parte nativa del plugin, que será invocada desde el código Javascript y contendrá la implementación de la funcionalidad.

Vamos a ver un ejemplo sencillo, creando un plugin que nos permita iniciar desde la página web la navegación a un destino usando Google Navigation o la aplicación que tenga el usuario registrado para ello.

Creando el interface en Javascript

El código que se necesita para crear el plugin en Javascript es el siguiente:

var GoTo = function() {
};

GoTo.prototype.location = function(address, successCallback, failureCallback) {
    return PhoneGap.exec(
            successCallback,
            failureCallback,
            'GoTo',      // Nombre con el que está registrado el plugin Java
            'location',  // Acción invocada
            [address]);  
};

cordova.addConstructor(function() {
    cordova.addPlugin("goTo", new GoTo());
});

Sólo hay que crear un objeto con los métodos que queremos exponer, en este caso tenemos un objeto GoTo que expone un único método location que podremos invocar indicando la dirección a la queremos navegar y dos callbacks que serán invocadas de forma asíncrona cuando se complete la invocación, una para el caso de que todo funcione, successCallback, y otra por si hay algún problema, failureCallback.

La implementación del método usa PhoneGap para lanzar, a través de Cordova, la invocación de la parte nativa del plugin. Para pasar parámetros hacia la parte nativa usaremos un array que será serializado como JSON.

Por último, debemos añadir a cordova el plugin, lo que hacemos con el último fragmento de código:

cordova.addConstructor(function() {
    cordova.addPlugin("goTo", new GoTo());
});

Con esto se añade una propiedad goTo al objeto plugins y podremos usarlo fácilmente desde Javascript:

plugins.goTo.location("Plaza Mayor, Madrid, España");

Implementando la parte nativa

Para implementar la parte nativa habrá que usar el lenguaje nativo del dispositivo. En el caso de Android, esto implica escribir una clase Java, que en nuestro ejemplo tendrá el siguiente aspecto:

public class GoToPlugin extends Plugin {
	@Override
	public PluginResult execute(String action, JSONArray data, String callbackId) {
		
		try {
			Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("google.navigation:q=" + data.getString(0))); 
			this.ctx.startActivity(i);		
			return new PluginResult(Status.OK);
		}
		catch (Exception e) {
			System.err.println(e.getStackTrace());
			return new PluginResult(Status.ERROR);
		}
	}
}

Olvidemos por un momento la pésima gestión de errores del ejemplo y centrémonos en el plugin. La parte nativa de un plugin es una clase que extiende Plugin en cuyo método execute debemos implementar la funcionalidad del plugin.

Los parámetros de execute incluyen:

  • La acción a realizar. En este caso sólo tenemos una, location, pero podríamos tener varias y en el execute realizar distintas acciones en función del valor del parámetro.
  • Los parámetros de la acción, que recibimos encapsulados en un array JSON. En nuestro caso el array tiene un único elemento que es un String con la dirección a la que queremos dirigirnos.
  • El id de las callback asociadas a la invocación. En este ejemplo no lo utilizamos.

La implementación de la funcionalidad no tiene mucha historia si has programado algo para Android. Únicamente estamos definiendo un Intent para indicar que queremos navegar a una ubicación y dejamos al sistema que inicie ese Intent, con lo que buscará la aplicación asociada y la activará.

Para registrar el plugin en Cordova es necesario modificar el fichero /res/xml/plugins.xml del proyecto de Eclipse y añadir nuestro plugin:

<?xml version="1.0" encoding="utf-8"?>
<plugins>
    ...
    <plugin name="GoTo" value="koalite.cordova.GoToPlugin"/>
</plugins>

Es importante recordar que el nombre asignado al plugin en el fichero plugins.xml es el nombre que debemos usar luego desde Javascript al usar el método PhoneGap.exec(...).

Conclusiones

Cuando se desarrollan aplicaciones sobre plataformas no nativas es fundamental tener algún mecanismo para poder ejecutar código nativo y acceder a funcionalidades que no estén cubiertas por el framework que estás utilizando. Creedme, he padecido mucho con .NETCF y sé de lo que hablo. Llega un momento en que Platform Invoke se convierte en uno de tus mejores amigos.

En Cordova la creación de plugins para salir del mundo HTML+JS+CSS y llegar hasta el dispositivo físico es francamente sencillo y está muy bien resuelto, lo que da mucha tranquilidad. No hay nada peor que saber que la plataforma para la que estás desarrollando soporta algo pero tu lenguaje no, y con el sistema de plugins de Cordova es sencillo remediar esa situación cuando se presenta.

20 comentarios en “Extender PhoneGap/Cordova creando un plugin

  1. Pingback: weinre: el Firebug para móviles « Koalite's blog

  2. Muy buen tutorial!, soy newbie en esto y lo + dificil es la clase en Java. Serias tan amable de indicar cuales son los «import» de la clase? o bien dar un ejemplo completo? ya que en la clase Java da muchos errores
    Muchas gracias

  3. Hola Susana,

    Te he dejado el código completo de la clase en este gist: https://gist.github.com/2970895

    De todas formas, los imports que necesitas son estos:

    import org.apache.cordova.api.Plugin;
    import org.apache.cordova.api.PluginResult;
    import org.apache.cordova.api.PluginResult.Status;
    import org.json.JSONArray;
    import android.content.Intent;
    import android.net.Uri;

    Un saludo,

    Juanma.

  4. Me parece francamente bueno el tutorial.
    Estoy empezando con phonegap, y estaba intentando hacer un pequeño juego, para aprender mas que nada, ya se que es mas optimo utilizar lenguaje nativo para los juegos, pero me apetece experimentar con html5,
    el problema que encuentro es que no puedo pulsar dos elementos a la vez, por ejemplo mover hacia la izquierda y disparar, es decir no soporta multitouch, tu sabrías, ¿ podrías orientarme si es posible hacer un plugin para dar soporte a multitouch desde phonegap?
    Igual estoy pidiendo demasiado, muchas gracias de antemano Juanma, y felicidades por este tutorial tan bueno.

  5. Hola Juanma, perdona por tardar tando en contestar, no me llego al correo el aviso de que me habias contestado.
    Solo agradecerte tu aportación.
    Sigue asi, que se agradecen mucho estos tutoriales.
    Un saludo

  6. Hola. Fantástico artículo. Me queda la duda de saber en qué archivos contretos y de qué forma se ubica el código que indicas. Si me pudieras pasar el proyecto en Eclipse me seria de gran utilidad o indicarme en qué archivo colocas cada sección. No logro realizar, mediante un botón por ejemplo, la ejecución del plugin. Gracias. Saludos!

  7. Hola. Este artículo me ha sido de mucha utilidad. Gracias a el he realizado un plugin que inicia una clase ‘service’ el cual pone en marcha dos servidores sockets, una TCP y otro UDP. Relacionado con este artículo tengo un problema. Cuando los servidores reciben un mensaje necesito enviar un mensaje al código escrito en JavaScript para para que modifique ‘algo’ en el interface gráfico. He buscado por internet y no he encontrado la solución. Lo único que se me ha ocurrido es (para salir del paso) crear una función en JavaScript que se ejecute periódicamente y reciba los mensajes del CallbackContext. Pero esto puede ser algo pesado para la aplicación. ¿Existe alguna manera de enviar un mensaje desde el código nativo a JavaScritp?

  8. Hola Skatu,

    Hacer polling desde JS es una opción, pero más que un problema de carga para la aplicación, lo veo un problema para el consumo de batería, sobre todo si quieres que el UI responda en tiempo «casi real» al mensaje que recibe el servicio porque te obliga a estar consultando continuamente.

    Podrías basarte en lo que hacen plugins oficiales, como el BatteryListener, cuyo código puedes ver aquí: https://github.com/apache/cordova-android/blob/master/framework/src/org/apache/cordova/BatteryListener.java

    Básicamente, la idea es devolver al principio un PluginResult.Status.NO_RESULT e indicar que queremos mantener las callbacks vivas:

    PluginResult pluginResult = new PluginResult(PluginResult.Status.NO_RESULT);
    pluginResult.setKeepCallback(true);
    callbackContext.sendPluginResult(pluginResult);
    return true;
    

    Así, cuando queramos notificar de algo podemos seguir usando el callbackcontext para enviar nuevos resultados:

    PluginResult result = new PluginResult(PluginResult.Status.OK, info);
    result.setKeepCallback(keepCallback);
    callbackContext.sendPluginResult(result);
    

    Espero que te sirva.

    Un saludo,

    Juanma.

  9. Perdona lo he leído pero no me termina de quedar claro, en mi caso quiero implementar una fundación para silenciar el móvil y otra para saber el modo en el que está, en nativo sería con el audio mánager pero me falta la parte de conectarlo como plugin

  10. PhoneGap incluye un plugin parecido, que controla la reproducción de ficheros de audio. Puedes echarle un vistazo y ver cómo está hecho: AudioHandler.

  11. Muy bueno el tuto, sin embargo soy nuevo en esto de java, uso phonegap con jquery mobile, necesito realizar una función que mande a imprimir un ticket por bluetooth. podrías indicarme lo que debo estudiar para poder hacerlo, es decir, por donde empezar y que utilizar para poder hacer un plugin. Gracias de antemano, saludos.

  12. Supongo que necesitarás un plugin que te permita enviar datos a través de una conexión Bluetooth, posiblemente emulando un puerto serie.

    A través de esa conexión podrías enviarle a la impresora lo que tiene que imprimir. El formato de impresión depende del modelo de impresora, pero las de tickets suelen ser compatibles con Esc/Pos.

    Yo empezaría por buscar algo ya hecho sobre phonegap para acceder a un dispositivo Bluetooth, y a partir de ahí empezaría a jugar.

  13. que tal, tengo una duda en cuestion de agregar en el plugin de cordova el siguiente codigo
    cordova.addConstructor(function() {
    cordova.addPlugin(«goTo», new GoTo());
    });

  14. hola, necesito usar bluetooth para mi aplicacion simplemente lectura de un scanner lector de barras, no se por donde empezar lo quiero hacer con phonegap o cordova lo que se mas proximo para hacer, esto ya lo hice en java ahora quiero hacerlo en phonegap o cordova, alguna idea sera bienvenida, gracias de antemano

  15. En caso de que tengas uno, o varios archivos layout.xml que componga la interfaz gráfica de tu activity, toolbars, actionbars, estilos, drawables etc como registras ese tipo de archivos y dependencias para que los reconozca el plugin?

  16. Hola Abraham,

    Nunca me he encontrado con ese caso (un plugin que muestre UI) pero debería ser igual que en cualquier otro proyecto de android. A fin de cuentas, con Cordova es lo que tienes, un proyecto estándar de android que, por defecto, arranca un activity con un webview.

    Saludos

Comentarios cerrados.