Tutorial 8 - Suavizar

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):
    async with httpx.AsyncClient() as client:
        response = await client.get("https://jsonplaceholder.typicode.com/posts/42")

    payload = response.json()

    self.main_window.info_dialog(
        greeting(self.name_input.value),
        payload["body"],
    )

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

  1. El método se define como async def, en lugar de sólo def. Esto indica a Python que el método es una co-rutina asíncrona.

  2. 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.

  3. 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.

  4. La llamada get se hace con una palabra clave await. Esto indica a la aplicación que, mientras esperamos la respuesta de la red, puede ceder el control al bucle de eventos.

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 «bola de playa»/»spinner» no aparece

  • 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.

Siguientes pasos

Ahora tenemos una aplicación que es ágil y sensible, incluso cuando está esperando en una API lenta. Pero, ¿cómo podemos asegurarnos de que la aplicación sigue funcionando a medida que continuamos desarrollándola? ¿Cómo probamos nuestra aplicación? Visita Tutorial 9 para descubrirlo…