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:
- El cliente que se crea es un
AsyncClient()asíncrono, en lugar de unClient()síncrono. Esto indica ahttpxque debe operar en modo asíncrono, en lugar de síncrono. - 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. - La llamada
getse hace con una palabra claveawait. 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 - también usamos
awaitcuando 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é?
- Si quieres ir más allá, hay algunos tutoriales temáticos adicionales que profundizan en aspectos concretos del desarrollo de aplicaciones.
- Si quieres saber más sobre cómo construir interfaces de usuario complejas con Toga, puedes sumergirte en la documentación de Toga. Toga también tiene su propio tutorial demostrando cómo usar varias características del widget toolkit.
- Si desea saber más sobre las capacidades de Briefcase, puede sumergirse en la documentación de Briefcase.