Apache Cordova y Brython
Crear aplicaciones para Android utilizando el Framework Cordova y Brython
(Python en el WebView)

Cordova-Brython Logo


INTRODUCCIÓN

Una forma de crear aplicaciones para Android con Python es utilizar el Framework Apache Cordova.

Cordova es un framework que permite crear aplicaciones multiplataforma utilizando HTML, CSS y JavaScript. La interfaz gráfica de la aplicación será un webview, y Cordova expone una API para interactuar con el dispositivo (permite instalar plugins).

Gracias a Brython, una implementación del interprete de Python para navegadores web escrita en JavaScript, es posible utilizar el Framework Cordova para crear aplicaciones Android con Python.

Lo mas aburrido es la instalación de todos los componentes necesarios. Sin embargo, una vez configurado el entorno de desarrollo, crear aplicaciones con Cordova y Brython resulta muy sencillo y divertio. A continuación tienes algunos ejemplos de lo que se puede hacer:


Ejemplos de Aplicaciones creadas con Cordova + Brython



En este artículo vamos a ver todo lo necesario para desarrollar aplicaciones con Python aprovechando la mayor ventaja que tiene JavaScript (el ser un lenguaje que se ejecuta en el navegador y por tanto fácil de implementar en cualquier plataforma).

¡¡No te irás de aquí sin crear un apk que ejecute tu código python!!

Sin mas dilación empecemos con la parte mas aburrida: la instalación del entorno de desarrollo.


INSTALACIÓN

Unicamente explicaré cómo realizar la instalación en Ubuntu (también he probado la misma instalación en el subsistema de linux para windows y funciona perfectamente):

  1. Instalar Java Development Kit 8 (JDK8):

    sudo add-apt-repository ppa:openjdk-r/ppa
    sudo apt update
    sudo apt install openjdk-8-jdk
    

    Abre el archivo ~/.bashrc y añade lo siguiente al final:

    export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
    export PATH=$PATH:$JAVA_HOME/bin
    

    Guarda los cambios y ejecuta source ~/.bashrc para aplicar los cambios.

  2. Instalar Android Studio, Gradle y el SDK Platform 28:

    • Android Studio:

      sudo apt install snapd
      sudo snap install android-studio --classic
      

      Abre el archivo ~/.bashrc y añade lo siguiente al final:

      export ANDROID_HOME=$HOME/Android/Sdk
      export ANDROID_SDK_ROOT=$HOME/Android/Sdk
      export PATH=$PATH:$ANDROID_HOME/tools
      export PATH=$PATH:$ANDROID_HOME/platform-tools
      

      Guarda los cambios y ejecuta source ~/.bashrc para aplicar los cambios.

    • Gradle:

      sudo apt install gradle
      
    • SDK Platform 28:

      Abre Android Studio ejecutando android-studio, ve a configuración (esquina inferior derecha) y abre el SDK Manager:

    Android Studio - Main

    Instala el SDK Platform 28, que corresponde a Android 9.0 (Pie):

    Android Studio - SDK Platform 28

  3. Instalar NodeJS y npm:

    • NodeJS:

      sudo apt install python-software-properties
      sudo apt install curl
      curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
      sudo apt install nodejs
      
    • npm:

      sudo apt install npm
      
  4. Instalar Cordova:

    sudo npm install -g cordova
    sudo chown -R $USER:$(id -gn $USER) $HOME/.config
    
  5. Instalar Brython:

    python3 -m pip install brython
    

CREAR UNA APLICACIÓN

Vamos a crear una aplicación sencilla. Lo primero será crear el proyecto Cordova, en el que tenemos que instalar Brython. Borraremos los archivos innecesarios que se crean por defecto y copiaremos una plantilla. Finalmente crearemos nuestro APK que instalaremos en nuestro dispositivo:

  1. cordova create miapp

    Ejecuta este comando en el directorio que quieras. Se creará un subdirectorio llamado, en este caso, "miapp" que contiene el proyecto.

    Entra al directorio del proyecto "miapp" mediante cd miapp para poder ejecutar el siguiente comando:

  2. cordova platform add android

    Hemos añadido la plataforma Android a nuestro proyecto. Recordemos que aunque estamos interesados en crear aplicaciones para Android con Brython, el propósito del framework Apache Cordova es crear aplicaciones multiplataforma.

Dentro de nuestro proyecto, el directorio www es en el que se encuentran los archivos HTML, CSS y JavaScript (o Python) con los que crearemos nuestra aplicación. Entra mediante cd www y elimina los archivos .html, .css y .js que se crearon por defecto: rm index.html css/index.css js/index.js.

Entra al directorio vacío js mediante cd js e instala Brython: python3 -m brython --install

Elimina los archivos innecesarios que se crearon durante la instalación de Brython: rm *.html *.txt

Vuelve al directorio www con cd .. y crea un archivo index.html, copia la siguiente plantilla:

<!DOCTYPE html>

<html>

<head>

<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval'">

<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<meta name="viewport" content="initial-scale=1, width=device-width, viewport-fit=cover">

<title>Cordova-Brython-App</title>

<script src="js/brython.js"></script>
<script src="js/brython_stdlib.js"></script>

<script type="text/javascript">

function startBrython(){
    document.addEventListener('deviceready', brython, false);
}

</script>

</head>

<body onload='startBrython();'>

<script type="text/javascript" src="cordova.js"></script>

<script type="text/python3">

from browser import document, alert
from browser.html import *

document <= H1('Brython Ready! :)')
alert('Brython Ready!')

</script>

</body>

</html>

Lo mas importante a tener en cuenta en nuestro archivo index.html es lo siguiente:

Al dispararse el evento deviceready se ejecutará la función brython. Ahora solo falta que la función startBrython se ejecute una vez se haya cargado body:

<body onload='startBrython();'>

UNA FORMA DE IMPORTAR MÓDULOS

Con el método anterior hemos conseguido ejecutar código fuente mediante la función run_script (también podríamos haber utilizado exec), pero no hemos importado nuestro archivo app.py como un módulo, sino que lo hemos ejecutado como un script independiente.

Personalmente, aunque investigué mucho sobre como importar módulos casi nunca es necesario y basta con utilizar el sencillo y efectivo método anterior. En cualquier caso, importar módulos es posible y vamos a verlo a continuación.

La sentencia import no funciona para brython en cordova, ya que solicita el script con un query-string para asegurar que se obtiene la última versión del módulo, es decir, para evitar obtener el módulo del cache del navegador. Cordova no es un servidor web y para recuperar recursos locales debe hacerse una petición ajax en la que se indica el path del recurso sin ningún argumento adicional. Por el mismo motivo tampoco funcionan los scripts type="text/python" en los que se especifica el atributo src ni la función open.

Aunque la sentencia import no funciona podemos utilizar el módulo types para crear e importar módulos.

A continuación defino la clase py_import que puedes utilizar para importar cualquier script en el directorio www o en sus subdirectorios:

import os, types
from browser import ajax

class py_import:

  def __init__(self, script, location=''):

    name = os.path.splitext(script)[0]
    self.module = types.ModuleType(name)
    ajax.get(os.path.join(location, script), blocking=True, oncomplete=self.__load)
    exec(self.source, self.module.__dict__)

  def __load(self, req):
    self.source = req.text

  def __call__(self):
    return self.module


# Importar www/app.py
app = py_import('app.py')()

# Importar www/mismodulos/modulo1.py
modulo1 = py_import('modulo1.py', location='mismodulos')()

No es necesario definir la clase py_import en todos los scripts en los que quieras importar otros módulos. Puedes definir la clase en el archivo index.html y guardarla como atributo del objeto window:

# En el archivo index.html
# dendro de un <script type='text/python3'></script>

import os, types
from browser import window, ajax

class py_import:

  def __init__(self, script, location=''):

    name = os.path.splitext(script)[0]
    self.module = types.ModuleType(name)
    ajax.get(os.path.join(location, script), blocking=True, oncomplete=self.__load)
    exec(self.source, self.module.__dict__)

  def __load(self, req):
    self.source = req.text

  def __call__(self):
    return self.module


# Exponemos la clase py_import
window.py_import = py_import

# Importamos www/app.py
app = py_import('app.py')()

Ahora puedes utilizar la clase py_import en todos tus scripts para importar módulos desde módulos:

# www/app.py

from browser import window
py_import = window.py_import

# Importamos www/mismodulos/modulo1.py desde www/app.py
modulo1 = py_import('modulo1.py', location='mismodulos')()

COMPILAR LA APLICACIÓN (APK)

Ejecuta los siguientes comandos:

cordova prepare android
cordova compile android

Puedes simplificar la compilación en un único comando:

cordova build android

Tu APK se encuentra en el directorio miapp/platforms/android/app/build/outputs/apk/debug y se llama app-debug.apk.

Puedes instalarlo en tu dispositivo Android y comprobarás que funciona perfectamente

Aunque app-debug.apk funciona perfectamente, se trata de una aplicación compilada en modo depuración (debug).

Una aplicación compilada en modo debug tiene el indicador de depuración habilitado, es decir, puede depurarse. Además, se ha compilado con la clave de firma de depuración predeterminada, de modo que no puede ser publicada en Google Play Store.


### Diferencias entre compilación de depuración (*debug*) y de lanzamiento (*release*) --------------------------------------------------------------------------------------

Las principales diferencias entre una compilación de depuración y una de lanzamiento son la indicador de depuración y las claves de firma:


### Compilación de Lanzamiento: *app-release.apk* -------------------------------------------------

Para realizar una compilación de lanzamiento (release) necesitas:


#### 1. Crear el almacén de claves (keystore):

Para crear el keystore de tu aplicación tienes que utilizar la utilidad keytool: Abre un terminal en el directorio ráiz de tu proyecto cordova miapp y presta atención al siguiente comando:

echo y | keytool -genkeypair -dname "cn=José María, ou=tecnobillo1, o=tecnobillo, c=ES" -alias miapp -keypass claveLlave -keystore miapp.keystore -keyalg RSA -keysize 2048 -storepass claveAlmacen -validity 20000

### 2. Crear el archivo de configuración (build.json):

El archivo de configuración debe contener información correcta en relación al keystore creado previamente. Un ejemplo acorde con miapp.keystore es el siguiente:

{

    	"android": {
    			"release": {
    					"keystore": "miapp.keystore",
    					"storePassword": "claveLlave",
    					"alias": "miapp",
    					"password": "claveAlmacen",
    					"keystoreType": ""
    			}
    	}


}

Ten en cuenta que he indicado miapp.keystore como path del almacén de claves porque he creado este archivo en el directorio raíz del proyecto. Si lo has creado en otro directorio debes indicar el path absoluto del keystore.

El archivo build.json también puede definir la configuración relativa a la compilación debug. Por ejemplo build.json podría lucir así:

{

    "android": {
        "release": {
            "keystore": "miapp.keystore",
            "storePassword": "claveLlave",
            "alias": "miapp",
            "password": "claveAlmacen",
            "keystoreType": ""
        },
        "debug": {
            "keystore": "miapp.keystore",
            "storePassword": "claveLlave",
            "alias": "miapp",
            "password": "claveAlmacen",
            "keystoreType": ""
        }
    }


}

En este caso la configuración es la misma para release y debug, cosa que no tiene por que ser así e incluso no debería ser así, pero es válido.


### 3. Compilar app-release.apk

Ya puedes compilar la versión de lanzamiento de tu aplicación, firmada con el keystore que has creado anteriormente. En el directorio del proyecto cordova miapp abre un terminal y ejecuta:

cordova prepare android
cordova compile android -release

El archivo app-release.apk se ha generado en el directorio miapp/platforms/android/app/build/outputs/apk/release. Puedes simplificar la compilación en un único comando:

cordova build android -release

Si ya instalaste app-debug.apk en tu dispositivo tienes que desinstalarla antes de instalar app-release.apk, ya que como son la misma aplicación con diferente keystore, el sistema android no te permitirá realizar la instalación de ambas aplicaciones.

¡¡ MISIÓN CUMPLIDA !!


DEPURACIÓN EN BRYTHON

En Brython los errores en tiempo de ejecución se imprimen por defecto en la consola del navegador, pero cuando creas el archivo APK con Cordova y lo instalas en un dispositivo, al ejecutar la aplicación no es posible abrir la consola del navegador (webview) y ver lo que está sucediendo, de modo que si tu código tiene errores resulta muy difícil localizarlos y corregirlos.

SALIDA DE ERROR ESTÁNDAR

No obstante, puedes modificar la salida de error estándar en tu script mientras desarrollas y pruebas la aplicación en tu dispositivo (app-debug.apk). Por ejemplo, puedes añadir el siguiente código al inicio de tu script:

import sys
from browser import document, alert

class Err:
    def write(self, err):
    	document <= P(err)
    	alert(err)

sys.stderr = Err()

Así, si se produce un error podrás visualizarlo en el documento y además se mostrará una alerta. Al finalizar tu aplicación (app-release.apk), cuando estés seguro/a de que no hay errores, puedes eliminar este trozo de código.


El archivo config.xml

El archivo config.xml es el archivo de configuración de nuestra aplicación. Se encuentra en el directorio raíz del proyecto (fuera del directorio www). Por defecto se ve así:

<?xml version='1.0' encoding='utf-8'?>
<widget id="io.cordova.hellocordova" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
    <name>HelloCordova</name>
    <description>
        A sample Apache Cordova application that responds to the deviceready event.
    </description>
    <author email="[email protected]" href="http://cordova.io">
        Apache Cordova Team
    </author>
    <content src="index.html" />
    <plugin name="cordova-plugin-whitelist" spec="1" />
    <access origin="*" />
    <allow-intent href="http://*/*" />
    <allow-intent href="https://*/*" />
    <allow-intent href="tel:*" />
    <allow-intent href="sms:*" />
    <allow-intent href="mailto:*" />
    <allow-intent href="geo:*" />
    <platform name="android">
        <allow-intent href="market:*" />
    </platform>
    <platform name="ios">
        <allow-intent href="itms:*" />
        <allow-intent href="itms-apps:*" />
    </platform>
</widget>

Como puedes observar, se añade por defecto el *plugin whitelist*. Este *plugin* permite declarar permisos para el acceso a diferentes *URLs* mediante las etiquetas __access__ y __allow-intent__. Por otra parte, observa las etiquetas __platform__: lo que se declare dentro de una etiqueta *platform* solo se aplicará a la plataforma en cuestión.

En nuestra aplicación de ejemplo solamente necesitamos acceso a recursos locales de la aplicación, de modo que podemos prescindir de whitelist. Además, como estamos desarrollando la app solamente para Android no necesitamos las etiquetas platform para definir distinciones entre plataformas.

Nuestro config.xml puede quedar así (modifícalo a tu gusto):

<?xml version='1.0' encoding='utf-8'?>
<widget id="site.tecnobillo.miapp" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
    <name>miapp</name>
    <description>
        Aplicación de ejemplo desarrollada con Apache Cordova y Brython.
    </description>
    <author email="[email protected]" href="https://tecnobillo.site">
        José María Sánchez Ruiz (Tecnobillo)
    </author>
    <content src="index.html" />
</widget>

Podrías haber incluido cierta información al crear el proyecto desde el terminal en vez de editarlo ahora:


### Algunas aclaraciones

En realidad no hemos eliminado el plugin whitelist ni las plataformas android e ios, solamente hemos borrado su configuración en config.xml.

A partir de Cordova 9.0.0 se recomienda elimiar las plataformas y plugins del archivo config.xml, a partir de esta versión no se sincronizan los archivos config.xml y package.json.

La configuración de plugins y plataformas debe hacerse en el archivo package.json.

Es conveniente reinstalar los plugins si modificas el dominio inverso de la aplicación en config.xml, ya que cuando ejecutas cordova prepare android se crea un archivo platforms/android/android.json en el que se especifican los plugins instalados. Estos plugins tienen una propiedad PACKAGE_NAME que toma el valor del dominio inverso de la aplicación (el dominio inverso de la aplicación es un convenio para designar el nombre del paquete).

Si abres el archivo package.json verás que no se muestra actualizada la información de la aplicación (lo que cambiaste en config.xml). No te preocupes, al compilar el archivo apk tendrá prioridad la configuración en config.xml sobre package.json.

Cuando ejecutas cordova prepare android se instalan los plugins declarados en package.json que no estén ya instalados. Sin embargo, no es necesario modificar el archivo package.json para instalar plugins, ya que cuando instalas un plugin desde el terminal se agrega automáticamente a package.json.


Compilar Brython - Interesante pero poco práctico

Brython no está pensado para ser distribuido compilado, sino para transpilar el código python al vuelo. No obstante, es posible compilar el código Brython a JavaScript antes de crear el archivo APK.

Cuando se desarrolla una página web con Brython no tiene mucho sentido hacer esto, ya que el script .js generado es mucho mas pesado que el original .py, así que el coste de recuperar el archivo a través de una petición HTTP por parte del navegador no compensa el tiempo que se ahorra en interpretar el código brython original.

Pero en una aplicación creada con Apache Cordova y Brython esta práctica resulta interesante, ya que los scripts son cargados localmente.

Si quieres "trasterar un poco" puedes echar un vistazo a Brython Compiler para Windows, una aplicación que creé con este propósito. Si tu aplicación está formada por varios módulos realmente tendrás problemas, pero de vez en cuando está bien "cacharrear un poco".

Una opción de optimización mas realista es minificar el código. Si desarrollas una aplicación y piensas distribuirla probablemente prefieras que el código esté "obfuscado". También suele ser deseable eliminar los comentarios del código (que en mi caso pueden ser incluso vergonzosos).

Para ello te recomiendo probar python_minifier, que puedes instalar en CPython mediante:

python -m pip install python-minifier

Casi siempre funciona perfectamente pero en ocasiones (en mi caso, al utilizar funciones lambda o decoradores) el código minificado da error. Te invito a que lo pruebes, una vez instalado puedes utilizarlo como en el siguiente ejemplo:

import python_minifier

src = """

def saludar(alguien):
    '''Función que saluda a alguien'''
    print(f'Hola {alguien}')

# Un comentario random
saludar('tecnobillo')

"""

minified_src = python_minifier.minify(src)

with open('minified_script.py', 'w') as f:
    f.write(minified_src)


# RESULTADO:

# def saludar(alguien):'Función que saluda a alguien';print(f"Hola {alguien}")
# saludar('tecnobillo')

Como puedes observar, el código de salida es mas compacto, y los comentarios de una sola línea son eliminados. Los comentarios de comillas triples no se eliminan sino que se convierten en strings de una sola línea (esto es porque python_minifier no sabe si son comentarios o strings multilínea).