Saltar a contenido

Tutorial 8 - Suavizar

Hasta ahora, nuestra aplicación ha sido relativamente simple - mostrando widgets GUI, llamando a una simple librería de terceros, y mostrando la salida en un diálogo. Todas estas operaciones ocurren muy rápidamente, y nuestra aplicación sigue respondiendo.

Sin embargo, en una aplicación del mundo real, necesitaremos realizar tareas complejas o cálculos que pueden tardar un poco en completarse - y mientras se realizan esas tareas, queremos que nuestra aplicación siga respondiendo. Vamos a hacer un cambio en nuestra aplicación que podría tomar un poco de tiempo para completar, y ver los cambios que hay que hacer para dar cabida a ese comportamiento.

Acceso a una API

Una tarea que suele llevar mucho tiempo en una aplicación es realizar una solicitud a una API web para recuperar datos y mostrarlos al usuario. Las API web a veces tardan uno o dos segundos en responder, por lo que si llamamos a una API de este tipo, debemos asegurarnos de que nuestra aplicación no deje de responder mientras esperamos a que la API web devuelva una respuesta.

Esta es una aplicación de juguete, por lo que no disponemos de una API real con la que trabajar, así que utilizaremos un punto final de API de muestra como fuente de datos. Si abre https://tutorial.beeware.org/tutorial/message.json en su navegador, obtendrá una carga JSON con un mensaje.

La biblioteca estándar de Python contiene todas las herramientas necesarias para acceder a una API. Sin embargo, las API incorporadas son de muy bajo nivel. Son buenas implementaciones del protocolo HTTP, pero requieren que el usuario gestione muchos detalles de bajo nivel, como la redirección de URL, las sesiones, la autenticación y la codificación de la carga útil. Como "usuario normal de navegador", probablemente estés acostumbrado a dar por sentados estos detalles, ya que el navegador los gestiona por ti.

Como resultado, la gente ha desarrollado bibliotecas de terceros que envuelven las APIs incorporadas y proporcionan una API más simple que se acerca más a la experiencia cotidiana del navegador. Vamos a utilizar una de esas bibliotecas para acceder a la API {JSON} una biblioteca llamada httpx. Briefcase usa httpx internamente, así que ya está en tu entorno local - no necesitas instalarla por separado para usarla aquí.

Vamos a añadir una llamada a la API httpx a nuestra aplicación. Modifica la configuración requires en nuestro pyproject.toml para incluir el nuevo requisito:

requires = [
    "faker",
    "httpx",
]

Añade un import al principio de app.py para importar httpx:

import httpx

Para hacer que nuestro tutorial sea asíncrono, modifica el manejador de eventos say_hello() para que se vea así:

async def say_hello(self, widget):
    fake = faker.Faker()
    with httpx.Client() as client:
        response = client.get("https://tutorial.beeware.org/tutorial/message.json")

    payload = response.json()

    await self.main_window.dialog(
        toga.InfoDialog(
            greeting(self.name_input.value),
            f"A message from {fake.name()}: {payload['body']}",
        )
    )

Esto cambiará la llamada de retorno say_hello() para que cuando sea invocada, lo haga:

  • realice una solicitud GET en la API de marcador de posición JSON para obtener el puesto 42;
  • decodificar la respuesta como JSON;
  • extraer el cuerpo del mensaje; y
  • incluir el cuerpo de ese mensaje como texto del cuadro de diálogo "mensaje", en lugar del texto generado por Faker.

Vamos a ejecutar nuestra aplicación actualizada en el modo desarrollador de Briefcase para comprobar que nuestro cambio ha funcionado. Como hemos añadido un nuevo requisito, tenemos que decirle al modo desarrollador que reinstale los requisitos, utilizando el argumento -r:

(beeware-venv) $ briefcase dev -r

[helloworld] Installing requirements...
...
[helloworld] Starting in dev mode...
===========================================================================

Cuando introduzcas un nombre y pulses el botón, deberías ver un cuadro de diálogo parecido a:

Hola Mundo Tutorial 8 diálogo, en macOS](../images/macOS/tutorial-8.png)

(beeware-venv) $ briefcase dev -r

[helloworld] Installing requirements...
...
[helloworld] Starting in dev mode...
===========================================================================

Cuando introduzcas un nombre y pulses el botón, deberías ver un cuadro de diálogo parecido a:

Hola Mundo Tutorial 8 diálogo, en Linux

(beeware-venv) C:\...>briefcase dev -r

[helloworld] Installing requirements...
...
[helloworld] Starting in dev mode...
===========================================================================

Cuando introduzcas un nombre y pulses el botón, deberías ver un cuadro de diálogo parecido a:

Hola Mundo Tutorial 8 diálogo, en Windows

No puedes ejecutar una aplicación Android en modo desarrollador: utiliza las instrucciones para la plataforma de escritorio que hayas elegido.

No puedes ejecutar una aplicación para iOS en modo desarrollador: sigue las instrucciones de la plataforma de escritorio que hayas elegido.

A menos que tengas una conexión a Internet realmente rápida, puede que notes que cuando pulsas el botón, la interfaz gráfica de tu aplicación se bloquea un poco. El sistema operativo puede incluso manifestarlo con un cursor "bola de playa" o "spinner" para indicar que la aplicación no responde.

A menos que tengas una conexión a Internet realmente rápida, puede que notes que cuando pulsas el botón, la GUI de tu aplicación se bloquea un poco. Esto se debe a que la petición web que hemos realizado es síncrona. Cuando nuestra aplicación realiza la petición web, espera a que la API devuelva una respuesta antes de continuar. Mientras espera, no permite a la aplicación redibujar - y como resultado, la aplicación se bloquea.

Bucles de eventos GUI

Para entender por qué ocurre esto, tenemos que profundizar en los detalles del funcionamiento de una aplicación GUI. Los detalles varían en función de la plataforma, pero los conceptos de alto nivel son los mismos, independientemente de la plataforma o el entorno GUI que utilices.

Una aplicación GUI es, fundamentalmente, un único bucle parecido a:

while not app.quit_requested():
    app.process_events()
    app.redraw()

Este bucle se llama Bucle de Evento. (Estos no son nombres de métodos reales - es una ilustración de lo que está pasando en "pseudo-código").

Cuando haces clic en un botón, arrastras una barra de desplazamiento o tecleas una tecla, estás generando un "evento". Ese "evento" se coloca en una cola, y la aplicación procesará la cola de eventos la próxima vez que tenga la oportunidad de hacerlo. El código de usuario que se activa en respuesta al evento se denomina manejador de eventos. Estos manejadores de eventos son invocados como parte de la llamada process_events().

Una vez que una aplicación ha procesado todos los eventos disponibles, redibujará() la GUI. Esto tiene en cuenta cualquier cambio que los eventos hayan causado en la pantalla de la aplicación, así como cualquier otra cosa que esté ocurriendo en el sistema operativo - por ejemplo, las ventanas de otra aplicación pueden oscurecer o revelar parte de la ventana de nuestra aplicación, y el redibujado de nuestra aplicación tendrá que reflejar la parte de la ventana que es visible en ese momento.

El detalle importante a tener en cuenta: mientras una aplicación está procesando un evento, no puede redibujar, y no puede procesar otros eventos.

Esto significa que cualquier lógica de usuario contenida en un manejador de eventos necesita completarse rápidamente. Cualquier retraso en la finalización del manejador de eventos será observado por el usuario como una ralentización (o detención) en las actualizaciones de la interfaz gráfica de usuario. Si este retraso es lo suficientemente largo, tu sistema operativo puede reportarlo como un problema - los iconos "beachball" de macOS y "spinner" de Windows son el sistema operativo diciéndote que tu aplicación está tardando demasiado en un manejador de eventos.

Operaciones sencillas como "actualizar una etiqueta" o "volver a calcular el total de las entradas" son fáciles de completar rápidamente. Sin embargo, hay muchas operaciones que no pueden completarse rápidamente. Si realizas un cálculo matemático complejo, o indexas todos los archivos de un sistema de ficheros, o realizas una petición de red de gran tamaño, no puedes "hacerlo rápido": las operaciones son intrínsecamente lentas.

Entonces, ¿cómo realizar operaciones de larga duración en una aplicación GUI?

Programación asíncrona

Lo que necesitamos es una manera de decirle a una aplicación en medio de un manejador de eventos de larga duración que está bien liberar temporalmente el control de nuevo al bucle de eventos, siempre y cuando podamos reanudar donde lo dejamos. Depende de la aplicación determinar cuándo puede ocurrir esta liberación; pero si la aplicación libera el control al bucle de eventos regularmente, podemos tener un manejador de eventos de larga duración y mantener una interfaz de usuario responsiva.

Podemos hacerlo utilizando programación asíncrona. La programación asíncrona es una forma de describir un programa que permite al intérprete ejecutar varias funciones al mismo tiempo, compartiendo recursos entre todas las funciones que se ejecutan simultáneamente.

Las funciones asíncronas (conocidas como corrutinas) deben declararse explícitamente como asíncronas. También necesitan declarar internamente cuándo existe la oportunidad de cambiar el contexto a otra co-rutina.

En Python, la programación asíncrona se implementa utilizando las palabras clave async y await, y el módulo asyncio de la biblioteca estándar. La palabra clave async nos permite declarar que una función es una co-rutina asíncrona. La palabra clave await proporciona una forma de declarar cuando existe la oportunidad de cambiar el contexto a otra co-rutina. El módulo asyncio proporciona algunas otras herramientas útiles y primitivas para la codificación asíncrona.

Hacer que el tutorial sea asíncrono

Para hacer que nuestro tutorial sea asíncrono, modifica el manejador de eventos say_hello() para que se vea así:

async def say_hello(self, widget):
    fake = faker.Faker()
    async with httpx.AsyncClient() as client:
        response = await client.get("https://jsonplaceholder.typicode.com/posts/42")

    payload = response.json()

    await self.main_window.dialog(
        toga.InfoDialog(
            greeting(self.name_input.value),
            f"A message from {fake.name()}: {payload['body']}",
        )
    )

Sólo hay 4 cambios en este código con respecto a la versión anterior:

  1. El cliente que se crea es un AsyncClient() asíncrono, en lugar de un Client() síncrono. Esto indica a httpx que debe operar en modo asíncrono, en lugar de síncrono.
  2. El gestor de contexto utilizado para crear el cliente está marcado como async. Esto le indica a Python que existe la oportunidad de liberar el control a medida que se entra y se sale del gestor de contexto.
  3. La llamada get se hace con una palabra clave await. Esto indica a la aplicación que mientras esperamos la respuesta de la red, la aplicación puede liberar el control al bucle de eventos. Hemos visto esta palabra clave antes
  4. también usamos await cuando mostramos el cuadro de diálogo. La razón para su uso es la misma que para la petición HTTP - necesitamos decirle a la aplicación que mientras se muestra el cuadro de diálogo, y estamos esperando a que el usuario pulse un botón, está bien liberar el control de nuevo al bucle de eventos.

También es importante tener en cuenta que el propio manejador se define como async def, en lugar de sólo def. Esto indica a Python que el método es una coroutina asíncrona. Hicimos este cambio en el Tutorial 3 cuando añadimos el cuadro de diálogo. Sólo puedes usar sentencias await dentro de un método declarado como async def.

Toga te permite utilizar métodos regulares o co-rutinas asíncronas como manejadores; Toga gestiona todo entre bastidores para asegurarse de que el manejador es invocado o esperado según sea necesario.

Si guardas estos cambios y vuelves a ejecutar la aplicación (ya sea con briefcase dev en modo desarrollo, o actualizando y volviendo a ejecutar la aplicación empaquetada), no habrá ningún cambio obvio en la aplicación. Sin embargo, al hacer clic en el botón para activar el cuadro de diálogo, puede notar una serie de mejoras sutiles:

  • El botón vuelve a un estado "no pulsado", en lugar de quedar atrapado en un estado "pulsado".
  • El icono de la "bola de playa"/"ruleta" no aparecerá.
  • Si mueves o cambias el tamaño de la ventana de la aplicación mientras esperas a que aparezca el cuadro de diálogo, la ventana volverá a dibujarse.
  • Si intentas abrir el menú de una aplicación, el menú aparecerá inmediatamente.

Ya podemos ejecutar la aplicación completa. Sin embargo, como hemos añadido un requisito extra (httpx) también necesitamos actualizar los requisitos de nuestra aplicación; podemos hacerlo pasando -r a briefcase run. Esto actualizará los requisitos de nuestra aplicación, la reconstruirá y la lanzará:

(beeware-venv) $ briefcase run -r
(beeware-venv) $ briefcase run -r
(beeware-venv) C:\...>briefcase run -r
(beeware-venv) $ briefcase run android -r
(beeware-venv) $ briefcase run iOS -r

Deberías ver que tu aplicación se ejecuta y sigue respondiendo cuando pulsas el botón y se recupera el contenido de la red.

Siguientes pasos

Esto ha sido una muestra de lo que puedes hacer con las herramientas proporcionadas por el proyecto BeeWare. En el transcurso de este tutorial, usted tiene:

  • Creado un nuevo proyecto de aplicación GUI;
  • Ejecuta esa aplicación en modo desarrollo;
  • Creé la aplicación como un binario independiente para un sistema operativo de escritorio;
  • Empaquetado de ese proyecto para su distribución a otros;
  • Ejecuta la aplicación en un simulador y/o dispositivo móvil;
  • Ejecute la aplicación como una aplicación web;
  • Has añadido una dependencia de terceros a tu aplicación; y
  • Modificado la aplicación para que siga respondiendo.

Entonces - ¿ahora qué?