Kivy - Desarrollo de Aplicaciones con CPython para Android
Introducción a Kivy
Kivy es un framework multiplataforma de código abierto para la creación de interfaces gráficas de usuario. Está especialmente dirigido a plataformas móviles (Android e iOS), pero también puede ejecutarse en Windows. Linux y macOS.
Una aplicación creada con el framework kivy puede empaquetarse con pyinstaller para ejecutarse en equipos de escritorio, pero además el proyecto kivy incluye subproyectos que permiten distribuir las aplicaciones creadas con el framework kivy en Android e iOS.
Estructura del Proyecto Kivy (GitHub)
Framework kivy - Framework de interfaz de usuario de código abierto escrito en Python, que se ejecuta en Windows, Linux, macOS, Android e iOS.
pyjnius - Accede a clases Java desde Python - documentación.
plyer - Wrapper independiente de la plataforma para APIs dependientes de la plataforma.
buildozer - Empaquetador python genérico para Android e iOS.
- python-for-android - Convierte tu aplicación Python en una APK de Android.
- kivy-ios - Conjunto de herramientas para compilar Python / Kivy / otras bibliotecas para iOS.
NOTA: python-for-android y kivy-ios no son "subproyectos" de buildozer, pero los he representado como tal para entender que nosotros como desarrolladores crearemos aplicaciones con kivy, pyjnius y/o plyer y las compilaremos con buildozer tanto para iOS como para Android.
Mientras que el Framework kivy nos permite crear interfaces gráficas de usuario multiplataforma y pyinstaller permite distribuir aplicaciones creadas con Python en equipos de escritorio, buildozer hace posible su distribución en dispositivos móviles.
Por una parte buildozer utiliza python-for-android para distribuir aplicaciones en Android (no confundir kivy's python-for-android con sl4a's python-for-android), y por otra utiliza kivy-ios para distribuir aplicaciones en iOS.
pyjnius es un paquete para acceder a clases Java desde Python y plyer es un envoltorio (wrapper) para las APIs de iOS y Android independiente de la plataforma. Mediante pyjnius y plyer podremos acceder a las funciones nativas del dispositivo móvil.
Como dije anteriormente, no confundas el python-for-android de kivy con el python-for-android de sl4a: kivy tiene un módulo android que no tiene nada que ver con el módulo android de sl4a (el módulo android de sl4a y el módulo androidhelper de QPython son el mismo módulo con diferente nombre).
A continuación me centraré en kivy desde la perspectiva del desarrollo de aplicaciones para Android (en este artículo no hablaré de la distribución de aplicaciones para escritorio ni para iOS).
Instalación del Entorno de Desarrollo
Puedes instalar kivy en Windows y ejecutarlo sin ningún problema. De hecho, puedes utilizar pyinstaller para crear ejecutables (.exe) de tus aplicaciones creadas con kivy, pero buildozer solamente está disponible para Linux, de modo que no podrás compilar a apk en Windows. No obstante, en Windows 10 puedes utilizar el Subsistema de Windows para Linux para trabajar con Linux.
La instalación que voy a explicar la he probado en Xubuntu 18.04, en el Subsistema de Windows para Linux (Ubuntu 18.04), y en Ubuntu MATE 20.04. En los tres entornos la compilación con buildozer funciona perfectamente, pero para el Subsistema de Windows no he logrado ejecutar aplicaciones sin compilar, ya que el subsistema linux no tiene interzaz gráfica (tampoco tuve éxito instalando Xming, aunque supongo que habrá alguna manera de hacerlo).
En Windows mi solución fué instalar todo el entorno en el subsistema linux para poder compiar apks, y para ejecutar y testear la interfaz instalé kivy en el intérprete de python para windows.
INSTALACIÓN EN LINUX
NOTA IMPORTANTE: Kivy todavía no soporta Python3.8 o superior (25 de Noviembre de 2020). Debes instalar python3.7 si tienes una versión mas reciente de python:
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt-get update
sudo apt-get install python3.7
Si has instalado python3.7 de esta manera debes sustituir python3
por python3.7
en todos los comandos que veremos a continuación.
1. Instalar kivy
Instalar virtualenv en python3:
$ python3 -m pip install --upgrade --user pip setuptools virtualenv
Crear un entorno virtual llamado kivyenv:
Voy a crear el entorno virtual en el directorio
$HOME
:$ python3 -m virtualenv kivyenv
Se ha creado el directorio
~/kivyenv
que contiene el entorno en el que vamos a instalar *kivy.Activar el entorno kivyenv
$ source ~/kivyenv/bin/activate
Fíjate en que se ha añadido el prefijo (kivyenv) al prompt del terminal. Si ahora ejecutamos el comando python3 en el terminal se ejecutará el intérprete de kivyenv, no el de nuestra instalación de python3 en el equipo. No importa en que directorio te encuentres (pwd), estás trabajando con tu kivyenv en este terminal.
Instalar kivy en nuestro kivyenv:
(kivyenv) $ python3 -m pip install kivy (kivyenv) $ python3 -m pip install kivy_examples (kivyenv) $ python3 -m pip install ffpyplayer
Ya puedes utilizar kivy desde el entorno virtual kivyenv, pero antes de continuar vamos a instalar buildozer (también en el kivyenv), que nos permitirá crear un apk ejecutable a partir de nuestro código python.
2. Instalar buildozer
Instalar buildozer en nuestro kivyenv:
buildozer se encargará de crear nuestros apks instalables en dispositivos android. Para funcionar correctamente es necesario instalar, además, cython y virtualenv en nuestro kivyenv. Por otra parte, tenemos que instalar el jdk8 para que buildozer pueda compilar el archivo apk. Los comandos para realizar la instalación completa son los siguientes:
(kivyenv) $ python3 -m pip install --upgrade buildozer (kivyenv) $ sudo apt update (kivyenv) $ sudo apt install -y git zip unzip openjdk-8-jdk python3-pip autoconf libtool pkg-config zlib1g-dev libncurses5-dev libncursesw5-dev libtinfo5 cmake libffi-dev (kivyenv) $ sudo apt install libssl-dev (kivyenv) $ python3 -m pip install --upgrade cython virtualenv
También hemos ejecutado
sudo apt install libssl-dev
para evitar un documentado error durante la ejecución de buildozer/p4a.Configurar JAVA_HOME y el PATH del sistema:
Abre el archivo
~/.bashrc
con cualquier editor de texto y al final del archivo añade lo siguiente:export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 export PATH=$PATH:$JAVA_HOME/bin export PATH=$PATH:~/.local/bin
Si no añades al archivo
~/.bashrc
la variable de entornoJAVA_HOME
puede que buildozer funcione correctamente, pero si en tu equipo tienes instaladas otras versiones del java development kit (jdk) y/o android studio puede que cuando buildozer ejecute javac esté utilizando una versión diferente e incompatible del jdk y la compilación falle (esto me dió muchos quebraderos de cabeza).
En cualquier caso, siempre debes añadirexport PATH=$PATH:~/.local/bin
.Finalmente ejecuta
source ~/.bashrc
para aplicar los cambios sin reiniciar el equipo.
NOTAS
Hemos instalado kivy y buildozer en un virtualenv llamado kivyenv. Hay varios motivos por los que hemos realizado la instalación en un entorno virtual y no directamente en la instalación de python3 de nuestro equipo:
- kivy tiene muchas dependencias que pueden corromperse al instalar otros módulos y paquetes en nuestra instalación de python3.
- pyinstaller generalmente crea ejecutables de mayor tamaño cuando se trabaja fuera de un virtualenv, debido al mecanismo de búsqueda de dependencias entre módulos y paquetes.
- Aunque la documentación oficial de kivy recomienda realizar la instalación en un virtualenv lo cierto es que he intentado instalarlo en la instalación de python3 del equipo y no he tenido éxito.
Si has prestado atención a los comandos ejecutados para instalar buildozer te habrás fijado en que, entre otras cosas, buildozer depende de Cython. Por otra parte, dentro de nuestro kivyenv hemos instalado virtualenv: esto aunque parezca "redundante" es necesario, ya que buildozer creará un entorno virtual para nuestra aplicación, que compilará para la plataforma móvil deseada (iOS o Android).
No es necesario instalar pyjnius ni plyer, ya que buildozer se encargará de instalarlos si son necesarios (vía pip) en el momento de crear la distribución para la plataforma seleccionada. Tampoco tenemos que preocuparnos de instalar python-for-android: buildozer se encarga de todo.
Ejemplo: APK para Android (kivy + pyjnius)
Para empezar a trabajar en una aplicación deber abrir un terminal y activar tu entorno virtual para kivy. Si creaste kivyenv en el directorio $HOME (~) puedes activarlo desde el terminal ejecutando:
source ~/kivyenv/bin/activate
El kivyenv está activado en el terminal actual, si quieres utilizar varios terminales debes activar kivyenv en cada uno de ellos.
Vamos a crear un proyecto llamado ejemplo_kivy:
(kivyenv) $ cd ~
(kivyenv) $ mkdir ejemplo_kivy
(kivyenv) $ cd ejemplo_kivy
(kivyenv) $ touch main.py second.py
En nuestro proyecto de ejemplo hemos creado dos archivos (main.py y second.py). En main.py crearemos una sencilla interfaz gráfica y en second.py definiremos las funciones manejadoras de eventos.
main.py
Vamos a grear una GUI que muestre dos botones, uno debajo del otro. Lo primero que tenemos que hacer es importar kivy.app.App
, kivy.uix.boxlayout.BoxLayout
y kivy.uix.button.Button
:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
Un objeto de la clase App devolverá un BoxLayout que contiene dos Buttons, pero vamos a personalizar estas clases mediante programación orientada a objetos:
Definimos los botones
btnToast
ybtnShare
:class btnToast(Button): text = 'Toast' def on_press(self): print('QUIERO MOSTRAR UN TOAST') class btnShare(Button): text = 'Share' def on_press(self): print('QUIERO COMPARTIR UN TEXTO POR WHATSAPP')
Las clases
btnToast
ybtnShare
heredan de la clase Button. Para cada una de ellas definimos su propiedad text y su función on_press, que se ejecutará cuando se presione el botón en cuestión.Definimos una clase
Box
que hereda de BoxLayout:class Box(BoxLayout): orientation = 'vertical' def __init__(self): super(Box, self).__init__() self.add_widget(btnToast()) self.add_widget(btnShare())
La clase
Box
es un BoxLayout (hereda de este). Extendemos la clase definiendo la orientación de su contenido, que establecemos en "vertical" para que los botones se muestren uno debajo del otro. Dentro del método___init__
añadimos los botones que definimos anteriormente invocando al métodoself.add_widget
.Definimos una clase
MainApp
que hereda de App:class MainApp(App): def build(self): return Box()
La clase
MainApp
que acabamos de definir hereda de App. Sobreescribimos su métodobuild
para que devuelva una instancia deBox
al ejecutar el métodorun
(el método run invocará al método build). A su vez,Box
contiene instancias de los botones que definimos previamente.Ejecución de la aplicación:
if __name__ == "__main__": MainApp().run()
Hemos instanciado
MainApp
y seguidamente hemos invocado a su métodorun
, que llama al métodobuild
y devuelve elBox
que contiene los botonesbtnToast
ybtnShare
.El método
run
no sólo invoca al métodobuild
, sino que pone en marcha el bucle de eventos de la aplicación.
Nuestro archivo main.py queda finalmente así:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
class btnToast(Button):
text = 'Toast'
def on_press(self):
print('QUIERO MOSTRAR UN TOAST')
class btnShare(Button):
text = 'Share'
def on_press(self):
print('QUIERO COMPARTIR UN TEXTO POR WHATSAPP')
class Box(BoxLayout):
orientation = 'vertical'
def __init__(self):
super(Box, self).__init__()
self.add_widget(btnToast())
self.add_widget(btnShare())
class MainApp(App):
def build(self):
return Box()
if __name__ == "__main__":
MainApp().run()
Si ahora ejecutas la aplicación mediante:
(kivyenv) $ python3 main.py
Verás lo siguiente (he redimensionado la interfaz):
Al hacer click en los botones verás que se imprime en el terminal el texto que especificamos dentro del método on_press
para cada uno de los botones.
Sin embargo, vamos a hacer que nuestra aplicación realmente haga algo interesante en un dispositivo móvil. De momento importa el módulo second que vamos a definir a continuación y redefine el método on_press
para cada uno de los botones:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
import second # Módulo second (second.py) que definiremos a continuación
class btnToast(Button):
text = 'Toast'
def on_press(self):
second.toast('Toast generado con pyjnius') # Método second.toast que definiremos a continuación
class btnShare(Button):
text = 'Share'
def on_press(self):
second.share('Texto comparitdo con pyjnius') # Método second.share que definiremos a continuación
class Box(BoxLayout):
orientation = 'vertical'
def __init__(self):
super(Box, self).__init__()
self.add_widget(btnToast())
self.add_widget(btnShare())
class MainApp(App):
def build(self):
return Box()
if __name__ == "__main__":
MainApp().run()
Antes de ejecutar la aplicación vamos a definir second.py porque obviamente ahora nos daría error.
second.py
Te voy a mostrar el script second.py finalizado y a continuación lo explicaré:
from kivy.utils import platform
if platform == 'android':
from jnius import autoclass, cast
from android.runnable import run_on_ui_thread
context = autoclass('org.renpy.android.PythonActivity').mActivity
Toast = autoclass('android.widget.Toast')
String = autoclass('java.lang.String')
Intent = autoclass('android.content.Intent')
def charseq(text):
return cast('java.lang.CharSequence', String(text))
@run_on_ui_thread
def toast(text):
text = charseq(text)
mi_toast = Toast.makeText(context, text, Toast.LENGTH_SHORT)
mi_toast.show()
@run_on_ui_thread
def share(text):
text, subject = charseq(text), charseq(text.replace(' ', '-'))
intent = Intent()
intent.setAction(Intent.ACTION_SEND)
intent.putExtra(Intent.EXTRA_SUBJECT, subject)
intent.putExtra(Intent.EXTRA_TEXT, text)
intent.setType('text/plain')
context.startActivity(intent)
else:
toast = print
share = print
La primera sentencia del script es la siguiente:
from kivy.utils import platform
La variable platform
contiene el nombre de la plataforma en la que estamos ejecutando la aplicación. Nos permite comprobar si estamos en Android o no:
if platform == 'android':
# Código para Android
pass
else:
# Código para otras plataformas
pass
Queremos definir dos funciones: toast
y share
En el bloque else
hacemos que toast
y share
sean la misma función print
. De este modo conseguimos que al ejecutar la aplicación en Windows o Linux no nos de error:
from kivy.utils import platform
if platform == 'android':
# Código para Android
pass
else:
# Código para otras plataformas
toast = print
share = print
Si dejas second.py así y ejecutas la aplicación mediante:
(kivyenv) $ python3 main.py
La aplicación funciona perfectamente. Al hacer click en cada uno de los botones se muestra el texto que especifícamos en el método on_press
de cada uno de los botones.
Ahora vamos a definir estas dos funciones para la plataforma android: Dentro del bloque if platform == 'android':
escribimos lo siguiente:
if platform == 'android':
from jnius import autoclass, cast
from android.runnable import run_on_ui_thread
context = autoclass('org.renpy.android.PythonActivity').mActivity
Toast = autoclass('android.widget.Toast')
String = autoclass('java.lang.String')
Intent = autoclass('android.content.Intent')
def charseq(text):
return cast('java.lang.CharSequence', String(text))
Lo primero que he hecho ha sido importar jnius.autoclass
y jnius.cast
, que nos permiten manejar clases Java desde Python.
Mediante
jnius.autoclass
defino las variablescontext
,Toast
,String
eIntent
.jnius.cast
lo utilizamos para convertir strings python en secuencias de caracteres java. Defino la funcióncharseq
para que realice esta tarea:def charseq(text): return cast('java.lang.CharSequence', String(text))
La función run_on_ui_thread
es una función decoradora del módulo android.runnable. Con ella vamos a "decorar" las funciones toast
y share
para que se ejecuten en el "hilo de la interfaz gráfica":
Función
toast
:@run_on_ui_thread def toast(text): text = charseq(text) # Convertimos el texto pasado como argumento en una secuencia de caracteres Java mi_toast = Toast.makeText(context, text, Toast.LENGTH_SHORT) # Creamos el toast pasando el contexto, texto y la duración mi_toast.show() # Mostramos el toast
Función
share
:@run_on_ui_thread def share(text): # El texto y el asunto deben ser secuencias de caracteres Java # El asunto será el "nombre de archivo" si al compartir se requiere crear un archivo # En este subject solo difiere de text en que en ver de espacios tiene guiones text, subject = charseq(text), charseq(text.replace(' ', '-')) # Creamos un "intent" cuya acción es enviar datos intent = Intent() intent.setAction(Intent.ACTION_SEND) # Establecemos asunto/nombre_archivo (subject) y texto (text) a enviar intent.putExtra(Intent.EXTRA_SUBJECT, subject) intent.putExtra(Intent.EXTRA_TEXT, text) # Indicamos el tipo de datos que estamos enviando (text/plain) intent.setType('text/plain') # Iniciamos la actividad en el contexto (variable que definimos anteriormente) context.startActivity(intent)
Ya está todo listo para distribuir la aplicación en Android. Vamos a crear el apk con buildozer.
### Crear APK con *buildozer*
Ejecuta los siguientes comandos en el directorio ejemplo_kivy:
(kivyenv) $ buildozer init
(kivyenv) $ buildozer -v android debug
- buildozer init - Crea el archivo buildozer.spec, que es el archivo de configuración de la aplicación donde puedes definir título, icono, dependencias, etc.
- buildozer -v android debug - Crea el archivo apk de depuración que puedes instalar en dispositivos android. El apk generado al finalizar el proceso se encuentra en el directorio ejemplo_kivy/bin.
La primera vez que ejecutes buildozer -v android debiug tardará mucho tiempo en finalizar la compilación, ya que buildozer descargará de internet todo lo necesario para generar el archivo apk. Cuando lo ejecutes una vez y posteriormente modifiques la aplicación tardará mucho menos tiempo en recompilar.
Al instalar el apk en un dispositivo Android y presionar cada uno de los botones verás como se muestra un toast y se abre el diálogo de compartir datos con otras aplicaciones: