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:
- config.xml - Archivo de configuración en el que especificamos nombre de aplicación, icono, etc.
- www/index.html - Archivo en el que definimos la estructura de la interfaz gráfica de la aplicación.
- www/css/index.css - Archivo en el que definimos el estilo de la interfaz gráfica de la aplicación.
- www/app.py - Archivo en el que definimos la lógica de la aplicación (Python).
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:
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:
Hemos enlazado Brython en el documento (que instalamos anteriormente):
<script src="js/brython.js"></script> <script src="js/brython_stdlib.js"></script>
Hemos enlazado una hoja de estilos CSS (que crearemos posteriormente):
<link rel='stylesheet' href='css/index.css'/>
Hemos enlazado el documento con un script Python (que crearemos posteriormente):
<script type="text/python3"> from browser import ajax, run_script ajax.get('app.py', oncomplete=lambda req: run_script(req.text)) </script>
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í:
Horrible, ¿verdad?
Se debe a que todavía no hemos definido estilos en el archivo index.css.
Presta atención a los siguientes puntos:
Todo el esqueleto de la aplicación está definido dentro de un div con id="app".
He creado un div con id="resultado" cuyo texto es 0:
<div id='resultado'>0</div>
Este div será la etiqueta donde mostrar el resultado de los cálculos
He creado un div con id="clear-buttons" que contiene dos botones:
<div id='clear-buttons'> <button id='del_all' class='clear_btn'>DEL</button> <button id='del_one' class='clear_btn'><pre><<</pre></button> </div>
Estos botones son [ DEL ] y [ << ], que servirán para eliminar el resultado del cálculo y para eliminar el último carácter del resultado respectivamente. He asignado un id a cada uno para referirme a ellos en el archivo app.py y una clase para darles un estilo común en el archivo index.css.
He creado un div con id="buttons" donde incluyo 4 divs, cada uno de los cuales contiene 4 botones. Estos botones son los números del [ 0 ] al [ 9 ] y los operadores matemáticos básicos:
sumar [ + ], restar [ - ], multiplicar [ x ], dividir [ / ] y calcular [ = ]).
__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:__
- El selector se refiere a todos los elementos HTML*. La propiedad
box-sizing:border-box;
la utilizo para que las
dimensiones de los elementos (ancho y alto) se calculen teniendo en cuenta el padding y el borde de los elementos.
- Me gusta usar esta propiedad para desarrollar aplicaciones móviles porque permite tener un control muy preciso sobre las dimensiones de los elementos,
tengan o no padding y/o border.
- Como esta propiedad no influye sobre las propiedad margin, a continuación declaro
margin:0px;
. Mi intención es que la interfaz ocupe toda
la pantalla del dispositivo y que todo elemento sea visible sin tener que hacer scroll horizontal ni vertical.
- Para el selector body utilizo
width:100vw;
yheight:100vh;
para asegurarme de que el cuerpo del documento ocupa
todo el ancho y alto de la pantalla.
- Utilizando las unidades vw y vh sobre body puedo utilizar las unidades porcentuales (%) para el resto
de elementos del documento, por lo que para el div con id="app" (#app) indico width:100%;
y height:100%;
.
- Además indico
display:flex;
yflex-direction:column;
para #app.
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:
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!
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
ycalc_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étododocument.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.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'
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.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'
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í.