Kivy - Desarrollo de Aplicaciones con CPython para Android

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)


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

2. Instalar buildozer


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:

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:

  1. Definimos los botones btnToast y btnShare:

    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 y btnShare 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.

  2. 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étodo self.add_widget.

  3. 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étodo build para que devuelva una instancia de Box al ejecutar el método run (el método run invocará al método build). A su vez, Box contiene instancias de los botones que definimos previamente.

  4. Ejecución de la aplicación:

    if __name__ == "__main__":
        MainApp().run()
    

    Hemos instanciado MainApp y seguidamente hemos invocado a su método run, que llama al método build y devuelve el Box que contiene los botones btnToast y btnShare.

    El método run no sólo invoca al método build, 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):

main.py screenshot

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.

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

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

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:



Ejemplo de aplicación Android con Kivy y pyjnius