Crea un plugin de Cordova multiplataforma para Android, iOS y Electron

18 de abril de 2022
Última actualización: 1 de agosto de 2024
Tabla de contenidos

Introducción

En este tutorial os voy a mostrar como crear un plugin de Cordova, como configurarlo, como usar dependencias nativas, como devolver uno o más valores por cada petición (como un Observable) y como editar los ficheros de configuración nativos como el AndroidManifest.xml. Como más aprendí a crear plugins de Cordova, fue mirando el código fuente de otros plugins que ya estaba usando. Si no encuentras lo que buscas en esta guía, te animo a que hagas lo mismo (o deja un comentario 🙂). Voy a asumir que ya tienes algo de experiencia desarrollando aplicaciones y usando plugins de Cordova.

Vamos a crear cada fichero a mano, pero puedes probar esta herramienta si quieres tener un CLI para generar la estructura base del plugin: plugman. Yo he probado plugman, pero siempre he preferido crearlo todo a mano, usando una plantilla que me cree hace tiempo como referencia. Además plugman te genera el código de iOS en Objective-C en vez de Swift.

Puedes ver el código fuente de este tutorial en GitHub:
https://github.com/adrian-bueno/multiplatform-cordova-plugin-example.

¡Empecemos!

Configuración base

Crea un nuevo directorio para tu nuevo plugin. Yo lo voy a llamar cordova-plugin-example. Dentro de este directorio, crea otros 2 directorios: src y www, y 2 ficheros plugin.xml y package.json.

Opcionalmente, puedes crear los ficheros README.md para describir lo que hace tu plugin, y un .gitignore.

📁 cordova-plugin-example
├── 📁 src
├── 📁 www
├── 📄 .gitignore
├── 📄 package.json
├── 📄 plugin.xml
└── 📄 README.md

Abre el fichero plugin.xml y añade el siguiente contenido:

<?xml version='1.0' encoding='utf-8'?>
<plugin id="cordova-plugin-example" version="1.0.0"
  xmlns="http://apache.org/cordova/ns/plugins/1.0"
  xmlns:android="http://schemas.android.com/apk/res/android">

  <name>CordovaPluginExample</name>
  <description>Cordova plugin example</description>
  <license>MIT-0</license>
  <keywords>cordova, example, multiplatform, android, ios, electron</keywords>

  <!-- www -->
  
  <!-- android -->
  
  <!-- ios -->

  <!-- electron -->
  
</plugin>

Más adelante iremos añadiendo la configuración de cada plataforma.

Abre el package.json y añade lo siguiente:

{
  "name": "cordova-plugin-example",
  "version": "1.0.0",
  "description": "Cordova plugin example",
  "author": "<Your name>",
  "license": "MIT-0",
  "homepage": "<Your git repository>",
  "repository": {
    "type": "git",
    "url": "<Your git repository>"
  },
  "cordova": {
    "id": "cordova-plugin-example",
    "platforms": [
      "android",
      "ios",
      "electron"
    ]
  },
  "keywords": [
    "ecosystem:cordova",
    "cordova-android",
    "cordova-ios",
    "cordova-electron",
    "cordova",
    "android",
    "ios",
    "electron",
    "example"
  ]
}

Adapta el package.json a tus necesidades. Puedes borrar todas las propiedades excepto name, version y cordova.

www

Dentro de esta carpeta vamos a crear un fichero JavaScript cuyo proposito es funcionar como interfaz entre nuestras apps web y el código nativo.

Crea el fichero www/CordovaPluginExample.js. Vamos a definir unos métodos/funciones dentro de él.

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

const PLUGIN_NAME = 'CordovaPluginExample';

// Devuelve el string "Hello {name}!"
exports.greeting = function (successCallback, errorCallback, name) {
  exec(successCallback, errorCallback, PLUGIN_NAME, 'greeting', [name]);
};

// Devuelve un número cada segundo, desde el valor de "seconds" hasta 0.
exports.countdownTimer = function (successCallback, errorCallback, seconds) {
  exec(successCallback, errorCallback, PLUGIN_NAME, 'countdownTimer', [seconds]);
};

// Escribe un fichero en:
// - iOS: El directorio Documents de la app
// - Android:
//   - (Android 9-) En el directorio Documents del móvil
//   - (Android 10+) El usuario puede elegir el directorio
// - Electron: En el directorio Documents del directorio del usuario
exports.writeFile = function (successCallback, errorCallback, fileName, text) {
  exec(successCallback, errorCallback, PLUGIN_NAME, 'writeFile', [fileName, text]);
};

// Devuelve el valor actual del Bitcoin
exports.bitcoinCurrentPrice = function (successCallback, errorCallback) {
  exec(successCallback, errorCallback, PLUGIN_NAME, 'bitcoinCurrentPrice', []);
};

Como has podido observar, los métodos tienen la siguiente interfaz:

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

exports.miMetodo = function (callbackTodoBien, callbackError, arg0, arg1, arg2, argX, ...) {
  exec(callbackTodoBien, callbackError, '<nombre-del-plugin>', '<nombre-del-metodo>', [arg0, arg1, arg2, argX, ...]);
};

Ahora que tenemos este fichero, vamos a declararlo en el plugin.xml, debajo del comentario de www:

<!-- www -->
<js-module name="CordovaPluginExample" src="www/CordovaPluginExample.js">
  <clobbers target="cordova.plugins.CordovaPluginExample" />
</js-module>

El tag clobbers lo que hace es asociar el script www/CordovaPluginExample.js a la variable window.cordova.plugins.CordovaPluginExample. Puedes poner la ruta que quieras, puedes poner simplemente <clobbers target="CordovaPluginExample"/> y en este caso sería accesible desde window.CordovaPluginExample.

Ya estamos listos para empezar a programar el código nativo.

He separado la implementación de cada plataforma en un tutorial diferente para mejorar la legibilidad y la carga de esta página.

Android

iOS

Electron

Estructura final del plugin

📁 cordova-plugin-example
├── 📁 src
│   ├── 📁 android
│   │    ├── 📄 build.gradle
│   │    ├── 📄 CordovaPluginExample.java
│   │    └── 📄 WriteFileHelper.java
│   ├── 📁 ios
│   │    ├── 📄 CallbackHelper.swift
│   │    └── 📄 CordovaPluginExample.swift
│   └── 📁 electron
│        ├── 📄 CordovaPluginExample.js
│        └── 📄 package.json
├── 📁 www
│    └── 📄 CordovaPluginExample.js
├── 📄 .gitignore
├── 📄 package.json
├── 📄 plugin.xml
└── 📄 README.md

Usando el plugin en una aplicación de Cordova

Crea una aplicación de Cordova vacía

Instala el CLI de Cordova:

npm i -g cordova

Crea una app de Cordova:

cordova create <app-name>

Entra en el nuevo directorio de la app:

cd <app-name>

Añade todas las plataformas que necesites:

cordova platform add android
cordova platform add ios
cordova platform add electron@4.0.0
# O si quieres usar mi fork de cordova-electron:
cordova platform add https://github.com/adrian-bueno/cordova-electron#feature/keep-callback

Si no has añadido el plugin que añade soporte de Swift dentro del plugin.xml, instalalo ahora:

cordova plugin add cordova-plugin-add-swift-support

Instala el plugin para probarlo

Instala el plugin:

npx cordova plugin add <ruta-al-plugin>/cordova-plugin-example

O instala el plugin creando un link (mejor para desarrollo, no tienes que estar instalándolo con cada cambio):

npx cordova plugin add <ruta-al-plugin>/cordova-plugin-example --link

Elimina el plugin de la app:

npx cordova plugin remove cordova-plugin-example

Añade botones al HTML

Abre www/index.html y añade los siguientes botones:

<body>
  <div class="app">
    <h1>Apache Cordova</h1>
    <div id="deviceready" class="blink">
      <p class="event listening">Connecting to Device</p>
      <p class="event received">Device is Ready</p>
    </div>

    <!-- copia desde aquí -->
    
    <br>

    <button id="greeting">Greeting "Hello World!"</button>
    <button id="greeting-empty">Greeting "Hello!"</button>

    <hr>

    <button id="bitcoin">Bitcoin current price</button>

    <hr>

    <button id="countdown">Countdown (12)</button>

    <hr>

    <button id="write-file">Write file</button>
    
    <!-- hasta aquí -->

  </div>
  <script src="cordova.js"></script>
  <script src="js/index.js"></script>
</body>

Deberías ver algo así cuando ejecutes la app:

Usa el plugin desde JavaScript

Abre www/js/index.js y añade la funcionalidad de los botones de arriba:

Recuerda que pusiste en el tag clobbers en el fichero plugin.xml. En mi caso puse <clobbers target="cordova.plugins.CordovaPluginExample" /> por lo que ahora el plugin esta asociado a la variable window.cordova.plugins.CordovaPluginExample .

// Para este ejemplo, vamos a definir una función
// para cada método del plugin

// Haremos llamadas al plugin y solamente
// imprimiremos por la consola los resultados

// La función 'greeting' usa funciones
// de callback para imprimir los resultados
function greeting(name) {
  function success(response) {
    console.log(response);
  }
  function error(code) {
    console.error(code);
  }
  cordova.plugins.CordovaPluginExample.greeting(success, error, name);
}

// Podemos devolver una Promesa y usar 'resolve'
// y 'reject' como las funciones de callback
function bitcoinCurrentPrice() {
  return new Promise((resolve, reject) => {
    cordova.plugins.CordovaPluginExample.bitcoinCurrentPrice(resolve, reject);
  });
}

// Lo mismo que 'bitcoinCurrentPrice' pero con parámetros
function writeFile(fileName, text) {
  return new Promise((resolve, reject) => {
    cordova.plugins.CordovaPluginExample.writeFile(resolve, reject, fileName, text);
  });
}

// Lo mismo que 'greeting'
function countdownTimer(seconds) {
  function success(response) {
    console.log(response);
  }
  function error(code) {
    console.error(code);
  }
  cordova.plugins.CordovaPluginExample.countdownTimer(success, error, seconds);
}

// Usaremos esta función para simplificar
// la asociación del evento de click
// de los botones con las funciones de arriba
function bindClick(elementId, callbackFunction) {
  document
    .getElementById(elementId)
    .addEventListener("click", callbackFunction);
}

// Solo podremos usar los plugins de Cordova
// una vez que la plataforma de Cordova 
// este lista. Esta función será llamada cuando
// se reciva el evento global 'deviceready'
function onDeviceReady() {
  // Estas 3 siguientes lineas han sido generadas
  // por el CLI de Cordova:

  // Cordova is now initialized. Have fun!
  console.log("Running cordova-" + cordova.platformId + "@" + cordova.version);
  document.getElementById("deviceready").classList.add("ready");

  // Ahora que la plataform de Cordova está lista,
  // podemos asociar las funciones a los botones
  
  bindClick("greeting", () => greeting("World"));
  bindClick("greeting-empty", () => greeting());

  bindClick("bitcoin", () => {
    bitcoinCurrentPrice()
      .then((res) => console.log(res))
      .catch((error) => console.error(error));
  });

  bindClick("countdown", () => countdownTimer(12));

  bindClick("write-file", () => {
    writeFile("cordova-plugin-example.txt", "Hello there 👋")
      .then((res) => console.log("File written"))
      .catch((error) => console.error(error));
  });
}

document.addEventListener("deviceready", onDeviceReady, false);

Depurar el código nativo

Android

Para depurar el código Java de Android, abre platforms/android con Android Studio y ejecuta la app con este IDE.

Para ver los logs de la capa web, habilita la depuración USB en tu dispositivo Android. Después abre: chrome://inspect/#devices. La app tiene que estar en ejecución para aparecer listada en esta página.

iOS

Para depurar el código Swift de iOS, abre platforms/ios con Xcode y ejecuta la app con este IDE.

Para ver los logs de la capa web, habilita las herramientas de desarrolladores en Safari y selecciona tu dispositivo iOS en el nuevo menú. La app tiene que estar en ejecución para aparecer listada en el menú.

En el caso de iOS, los logs de la capa web también aparecen por la consola de Xcode (cuando se ejecuta la app con Xcode).

Electron

Por el momento desconozco como depurar el código del proceso de Node.js (aparte de usando console.log).

Referencias útiles

@awesome-cordova-plugins (antes @ionic-native)

Puedes usar la capa de @awesome-cordova-plugins (antes llamada @ionic-native) para facilitar el uso de tu plugin usando Promesas y Observables en vez de funciones callback.