Calculadora Python para Android (Cordova + Brython)




Desarrollo de una Calculadora para Android utilizando el Framework Apache Cordova y el intérprete Brython


En esta sección voy a crear una aplicación sencilla para Android utilizando Python.

Esta aplicación es una Calculadora que, aunque sencilla, es un ejemplo bastante completo de cómo pueden desarrollarse aplicaciones para la plataforma Android utilizando el Framework Apache Cordova.

Aunque este Framework está dirigido a programadores web con experiencia en JavaScript, permite utilizar cualquier librería JS: Podemos utilizar el intérprete Brython (escrito en JavaScript) para desarrollar Apps escribiendo unicamente HTML, CSS y Python.

Antes de empezar pongo a tu disposición los archivos que crearemos en el proyecto y el apk final:


CREAR EL PROYECTO CALCULADORA


Lo primero que tenemos que hacer es crear un Proyecto Cordova. Si no tienes instalado el Framework Cordova o no sabes como crear un proyecto puedes visitar la página principal de esta sección.

A continuación crearé el proyecto Cordova, agregaré la plataforma Android al proyecto, eliminaré los archivos innecesarios generados por defecto e instalaré Brython:

cordova create calculadora site.tecnobillo.calculadora Calculadora
cd calculadora
cordova platform add android
cd www
rm index.html css/index.css js/index.js
rm -r img
cd js
python3 -m brython --install
rm *.txt *.html

Ya tenemos preparado el proyecto calculadora. Crearemos la aplicación mediante 4 archivos en el proyecto calculadora:

Cuando tengamos todo listo compilaremos nuestro archivo APK mediante:

cordova prepare android
cordova compile android



CONFIG.XML

Archivo calculadora/config.xml


Lo primero que debemos hacer es editar el archivo config.xml para especificar el nombre de la aplicación, icono, etc. Yo lo dejaré del siguiente modo:

<?xml version='1.0' encoding='utf-8'?>
<widget id="site.tecnobillo.calculadora" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
    <name>Calculadora</name>
    <description>
        Calculadora Sencilla.
    </description>
    <author email="[email protected]" href="https://tecnobillo.site">
        José María Sánchez Ruiz (Tecnobillo)
    </author>
    <content src="index.html" />
    <icon src="calculadora.png" />
</widget>

Si utilizas este config.xml debes copiar un archivo calculadora.png en el directorio raíz del proyecto (el mismo donde está config.xml), ya que lo he especificado en el atributo src de la etiqueta icon.
He utilizado el siguiente icono:

icono-calculadora

Cambia mis datos por los tuyos (autor, email, sitio web, etc).
Fíjate en el atributo src de la etiqueta content: Indica que el contenido que debe cargarse al ejecutarse la aplicación es el del archivo index.html.



INDEX.HTML

Archivo calculadora/www/index.html


Una vez editado el archivo config.xml, el siguiente paso es crear un archivo index.html en el directorio www. Copia lo siguiente en dicho archivo:

<!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">

<meta charset='utf-8'/>

<title>Calculadora</title>

<script src="js/brython.js"></script>
<script src="js/brython_stdlib.js"></script>
<link rel='stylesheet' href='css/index.css'/>

<script type="text/javascript">

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

</script>

</head>

<body onload='startBrython();'>


<!-- AQUÍ VAMOS A DEFINIR EL ESQUELETO DE LA APLICACIÓN -->


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

<script type="text/python3">

from browser import ajax, run_script

ajax.get('app.py', oncomplete=lambda req: run_script(req.text))

</script>

</body>

</html>

Si quieres conocer los detalles de este archivo puedes ver una descripción completa en la página principal de esta sección.

Fíjate en los siguientes puntos:

Presta atención al comentario HTML del documento:

<!-- AQUÍ VAMOS A DEFINIR EL ESQUELETO DE LA APLICACIÓN -->

Justo debajo copia el siguiente fragmento HTML, que será el esqueleto de la aplicación:

<div id='app'>

    <div id='resultado'>0</div>

    <div id='clear-buttons'>
        <button id='del_all' class='clear_btn'>DEL</button>
        <button id='del_one' class='clear_btn'><pre><<</pre></button>
    </div>

    <div id='buttons'>

        <div>
            <button>7</button>
            <button>8</button>
            <button>9</button>
            <button>/</button>
        </div>
        <div>
            <button>4</button>
            <button>5</button>
            <button>6</button>
            <button>x</button>
        </div>
        <div>
            <button>1</button>
            <button>2</button>
            <button>3</button>
            <button>-</button>
        </div>
        <div>
            <button>0</button>
            <button>,</button>
            <button id='calcular'>=</button>
            <button>+</button>
        </div>

    </div>

</div>

Si en este punto compilases el archivo APK, la aplicación se vería así:

App solo HTML

Horrible, ¿verdad?

Se debe a que todavía no hemos definido estilos en el archivo index.css.

Presta atención a los siguientes puntos:


No voy a explicar mas sobre HTML: Mi intención es mostrar como crear una aplicación en Python. Si no conoces HTML y CSS deberías buscar otro tutorial, pero en ese caso probablemente no te interese crear aplicaciones con Cordova y Brython.



INDEX.CSS

Archivo calculadora/www/css/index.css


Crea un archivo index.css en el directorio www/css y copia lo siguiente:

* {
    box-sizing:border-box;                /* Hacemos que el ancho y alto de cada elemento se calcule incluyendo el espacio ocupado por las propiedades css border y padding */
    margin:0px;                           /* Eliminamos el margen de todos los elementos del documento HTML, ya que la propiedad margin no la controlamos con la propiedad anterior */
}

body {
    width:100vw;                          /* Hacemos que el cuerpo del documento ocupe todo el ancho de la pantalla, pero sin ser mayor que la pantalla (no queremos scroll) */
    height:100vh;                         /* Hacemos que el cuerpo del documento ocupe todo el alto de la pantalla, pero sin ser mayor que la pantalla (no queremos scroll) */
}


#app {
    width:100%;                           /* El div principal debe ocupar todo el ancho disponible (como el ancho es 100vw para body, #app ocupará todo el ancho de la pantalla) */
    height:100%;                          /* El div principal debe ocupar todo el alto disponible (como el alto el 100vh para body, #app ocupará todo el alto de la pantalla) */
    display:flex;                         /* Facilita el diseño de una estructura sin utilizar float o position */
    flex-direction:column;                /* Hacemos que los elementos dentro de #app se organicen en una columna */
}

#resultado {
    width:100%;                           /* La etiqueta en la que mostraremos el resultado de un cálculo es un div que debe ocupar todo el ancho disponible */
    height:40%;                           /* Hacemos que ocupe el 40% del alto disponible, el resto lo ocuparán los botones */
    font-size:4em;                        /* Aumentamos el tamaño del texto del div para que sea adecuado en la pantalla de un dispositivo móvil */
    display:flex;                         /* Utilizamos flex para centrar el texto (números) del div mediante las propiedades justify-content y align-items */
    justify-content:center;
    align-items:center;
    overflow:auto;                        /* Para que cuando el número mostrado sea muy grande no descoloque la interfaz (al ocupar un ancho mayor que el de la pantalla) */
}

button {
    outline:none;                         /* Eliminamos los efectos por defecto de todos los botones */
    border:1px solid black;               /* Definimos el borde para todos los botones */
}

#clear-buttons {
    width:100%;                           /* Este div, como los demás, debe ocupar todo el ancho disponible */
    height:10%;                           /* En este caso queremos que solamente ocupe el 10% del alto disponible */
    display:flex;
    justify-content:center;
    align-items:center;
}

.clear_btn {
    width:50%;                            /* Como en #clear-buttons tenemos 2 botones, queremos que cada uno ocupe la mitad del ancho disponible */
    height:100%;                          /* Queremos que cada uno de los .clear_btn ocupe todo el alto disponible (que ocupen toda la altura del div #clear-buttons) */
    font-size:1.5em;
    background:#6495ED;
    color:#FFFAF0;
    border-top:1.5px solid black;
}

#buttons {
    width:100%;
    height:50%;
}

#buttons > div {
    width:100%;
    height:25%;                           /* Como tenemos 4 filas de botones (4 divs) queremos que cada una ocupe el 25% del alto de su contenedor: 4 divs x 25% height = 100% height */
    display:flex;
}

#buttons > div > button {
    width:25%;                            /* Como cada fila contiene 4 botones, queremos que cada uno ocupe el 25% del ancho, y entre todos ocupen el 100%: 4 botones x 25% width = 100% width */
    font-size:2em;
    background:#B0C4DE;
}


Voy a comentar el CSS que considero mas importante:


No voy a entrar en detalles sobre los valores que toma la propiedad css display, ni tampoco voy a hablar de otras cuestiones mas o menos básicas de CSS.

Ya hemos terminado de desarrollar la interfaz gráfica de la aplicación:

App HTML + CSS

Esto ya se ve mejor

Sin embargo, la aplicación todavía no hace nada, carece de funcionalidad...
Vamos a darle funcionalidad en el archivo app.py: Ha llegado el momento de programar en Python.



APP.PY

Archivo calculadora/www/app.py


Crea un archivo app.py en el directiorio www.

¡MANOS A LA OBRA!

  1. Crear variables que se refieran a los botones HTML y al div resultado:

    from browser import document, alert, bind
    
    del_all_button = document['del_all']
    del_one_button = document['del_one']
    calc_button = document['calcular']
    other_buttons = [btn for btn in document.select('button') if btn not in (del_all_button, del_one_button, calc_button)]
    resultado = document['resultado']
    

    En Brython podemos acceder a los elementos HTML a través del objeto document. Aunque document no es un diccionario, podemos acceder a los elementos del documento indicando su id, como si de un diccionario se tratase.

    Obtenemos así los botones del_all_button, del_one_button y calc_button. Obtenemos además el div resultado: resultado.

    El resto de botones los almacenamos en una lista que identificamos como other_buttons. Creamos esta lista mediante una lista de compresión. El método document.select permite acceder a elementos HTML utilizando cualquier selector css. En este caso, document.select('button') devuelve una lista que contiene todas las etiquetas html button.

  2. Manejar el evento click de other_buttons:

    La variable other_buttons contiene los botones [ 0 ], [ 1 ], [ 2 ], [ 3 ], [ 4 ], [ 5 ], [ 6 ], [ 7 ], [ 8 ], [ 9 ], [ + ], [ - ], [ x ] y [ / ].

    Tenemos que definir lo que sucede cuando se hace click en estos botones. Para ello creamos una función y la asignamos como manejador (handler) del evento click para todos los botones:

    def other_button_click(e):
        pass
    
    for btn in other_buttons:
        btn.bind('click', other_button_click)
    

    La función other_button_click no hace nada, vamos a darle funcionalidad:

    def other_button_click(e):
    
        # Si el texto del div es 0 (texto inicial) lo borramos (no queremos un 0 a la izquierda del número, pero cuando no hay número mostramos 0)
        if resultado.text == '0':
            resultado.text = ''
    
        resultado.text = resultado.text + e.target.text # Al hacer click en un botón, al texto del div debe sumarse el texto del botón pulsado (e.target.text)
    
        # El texto del div debe empezar por un número o por los signos + o -, pero núnca por los operadores x o /
        if resultado.text.startswith('x') or resultado.text.startswith('/'):
            resultado.text = '0'
    
  3. Manejar el evento click de calc_button:

    @bind(calc_button, 'click')
    def calcular(e):
    
        # La operacion es el texto del div resultado
        operacion = resultado.text
    
        # Sustituimos los caracteres que Python no reconoce como operadores por los operadores correspondientes
        operacion_python = operacion.replace('x', '*').replace(',', '.')
    
        try:
            # Intentamos calcular la expresión mediante la función predefinida eval
            calculo = eval(operacion_python)
    
            # Redondeamos el resultado del cálculo para que solo se muestren 2 decimales
            calculo = round(calculo, 2)
    
            # Asignamos el resultado del cálculo al texto del div, pero antes cambiamos los puntos (.) por comas (,)
            resultado.text = str(calculo).replace('.', ',')
    
        except:
            # Si se produce un error de cálculo mostramos una alerta y asignamos el texto "0" al resultado
            alert(f'Error de Cálculo:\n\n {resultado.text} = ?')
            resultado.text = '0'
    

    En vez de utilizar @bind(calc_button, 'click') podrías haber utilizado, justo después de definir la función calcular, calc_button.bind('click', calcular). Funcionaría exactamente igual, y en este caso no tendrías por qué importar el objeto bind.

  4. Manejar el evento click de del_all_button:

    @bind(del_all_button, 'click')
    def del_all(e):
        # Mostramos el texto "0" en el div resultado
        resultado.text = '0'
    
  5. Manejar el evento click de del_one_button:

    @bind(del_one_button, 'click')
    def del_one(e):
        # Si el número tiene un sólo dígito mostramos el texto "0"
        if len(resultado.text) == 1:
            resultado.text = '0'
    
        # Si el número tiene mas de un dígito eliminamos el último (el de mas a la derecha)
        else:
            resultado.text = resultado.text[:-1]
    


El archivo app.py quedaría finalmente así:

from browser import document, alert, bind


del_all_button = document['del_all']
del_one_button = document['del_one']
calc_button = document['calcular']
other_buttons = [btn for btn in document.select('button') if btn not in (del_all_button, del_one_button, calc_button)]
resultado = document['resultado']



def other_button_click(e):

    if resultado.text == '0':
        resultado.text = ''

    resultado.text = resultado.text + e.target.text

    if resultado.text.startswith('x') or resultado.text.startswith('/'):
        resultado.text = '0'


for btn in other_buttons:
    btn.bind('click', other_button_click)



@bind(calc_button, 'click')
def calcular(e):

    operacion = resultado.text
    operacion_python = operacion.replace('x', '*').replace(',', '.')

    try:
        calculo = eval(operacion_python)
        calculo = round(calculo, 2)
        resultado.text = str(calculo).replace('.', ',')

    except:
        alert(f'Error de Cálculo:\n\n {resultado.text} = ?')
        resultado.text = '0'



@bind(del_all_button, 'click')
def del_all(e):
    resultado.text = '0'



@bind(del_one_button, 'click')
def del_one(e):

    if len(resultado.text) == 1:
        resultado.text = '0'

    else:
        resultado.text = resultado.text[:-1]


Ya puedes crear el archivo APK e instalarlo en tu dispositivo:

cordova prepare android
cordova compile android

El APK se genera en el directorio calculadora/platforms/android/app/build/outputs/apk/debug y se llama app-debug.apk.

Si quieres crear una versión de lanzamiento de la aplicación (app-release.apk) puedes ver como hacerlo aquí.







Animación Aplicación Calculadora (Android Python)