Ir para o conteúdo

Tutorial 8 - Suavizando o Processo

Até agora, nosso aplicativo tem sido relativamente simples: exibir widgets da GUI, chamar uma biblioteca simples de terceiros e exibir a saída em uma caixa de diálogo. Todas essas operações ocorrem muito rapidamente e nosso aplicativo permanece responsivo.

Entretanto, em um aplicativo do mundo real, precisaremos executar tarefas ou cálculos complexos que podem levar algum tempo para serem concluídos e, à medida que essas tarefas forem executadas, queremos que o aplicativo permaneça responsivo. Vamos fazer uma alteração em nosso aplicativo que pode levar um pouco de tempo para ser concluída e ver as alterações que precisam ser feitas para acomodar esse comportamento.

Acesso a uma API

Uma tarefa comum e demorada que um aplicativo precisa executar é fazer uma solicitação em uma API da Web para recuperar dados e exibir esses dados ao usuário. Às vezes, as APIs da Web levam um ou dois segundos para responder, portanto, se estivermos chamando uma API como essa, precisamos garantir que nosso aplicativo não deixe de responder enquanto esperamos que a API da Web retorne uma resposta.

Este é um aplicativo de brinquedo, portanto não temos uma API real com a qual trabalhar, então usaremos um endpoint de API de amostra como fonte de dados. Se você abrir https://tutorial.beeware.org/tutorial/message.json no seu navegador, receberá uma carga JSON com uma mensagem.

A biblioteca padrão do Python contém todas as ferramentas necessárias para acessar uma API. Entretanto, as APIs incorporadas são de nível muito baixo. Elas são boas implementações do protocolo HTTP, mas exigem que o usuário gerencie muitos detalhes de baixo nível, como redirecionamento de URL, sessões, autenticação e codificação de carga útil. Como um "usuário normal de navegador", você provavelmente está acostumado a considerar esses detalhes como garantidos, pois o navegador os gerencia para você.

Como resultado, as pessoas desenvolveram bibliotecas de terceiros que envolvem as APIs incorporadas e fornecem uma API mais simples que se aproxima da experiência cotidiana do navegador. Vamos usar uma dessas bibliotecas para acessar a API {JSON} uma biblioteca chamada httpx. O Briefcase usa a httpx internamente, portanto, ela já está em seu ambiente local - não é necessário instalá-la separadamente para usá-la aqui.

Vamos adicionar uma chamada de API httpx ao nosso aplicativo. Modifique a configuração requires em nosso pyproject.toml para incluir o novo requisito:

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

Adicione uma importação na parte superior do app.py para importar o httpx:

import httpx

Para tornar nosso tutorial assíncrono, modifique o manipulador de eventos say_hello() para que ele tenha a seguinte aparência:

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']}",
        )
    )

Isso alterará a chamada de retorno say_hello() para que, quando for chamada, ela o faça:

  • faça uma solicitação GET na API do espaço reservado JSON para obter o post 42;
  • decodificar a resposta como JSON;
  • extrair o corpo da postagem; e
  • incluir o corpo dessa postagem como o texto da caixa de diálogo "mensagem", no lugar do texto gerado pelo Faker.

Vamos executar nosso aplicativo atualizado no modo de desenvolvedor do Briefcase para verificar se a alteração funcionou. Como adicionamos um novo requisito, precisamos informar ao modo de desenvolvedor para reinstalar os requisitos, usando o argumento -r:

(beeware-venv) $ briefcase dev -r

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

Ao inserir um nome e pressionar o botão, você verá uma caixa de diálogo semelhante:

Caixa de diálogo do tutorial 8 do Hello World, no
macOS

(beeware-venv) $ briefcase dev -r

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

Ao inserir um nome e pressionar o botão, você verá uma caixa de diálogo semelhante:

Caixa de diálogo do tutorial 8 do Hello World, no
Linux

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

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

Ao inserir um nome e pressionar o botão, você verá uma caixa de diálogo semelhante:

Caixa de diálogo do Tutorial 8 do Hello World, no
Windows

Não é possível executar um aplicativo Android no modo de desenvolvedor - use as instruções para a plataforma de desktop escolhida.

Não é possível executar um aplicativo iOS no modo de desenvolvedor - use as instruções para a plataforma de desktop escolhida.

A menos que você tenha uma conexão de Internet muito rápida, poderá perceber que, ao pressionar o botão, a GUI do seu aplicativo trava um pouco. O sistema operacional pode até manifestar isso com um cursor "beachball" ou "spinner" para indicar que o aplicativo não está respondendo.

A menos que você tenha uma conexão de Internet muito rápida, poderá perceber que, ao pressionar o botão, a GUI do seu aplicativo trava um pouco. Isso ocorre porque a solicitação da Web que fizemos é síncrona. Quando nosso aplicativo faz a solicitação da Web, ele espera que a API retorne uma resposta antes de continuar. Enquanto espera, ele não permite que o aplicativo seja redesenhado e, como resultado, o aplicativo trava.

Loops de eventos da GUI

Para entender por que isso acontece, precisamos nos aprofundar nos detalhes de como funciona um aplicativo de GUI. Os detalhes variam de acordo com a plataforma, mas os conceitos de alto nível são os mesmos, independentemente da plataforma ou do ambiente de GUI que você estiver usando.

Um aplicativo de GUI é, basicamente, um único loop que se parece com:

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

Esse loop é chamado de Event Loop. (Esses não são nomes de métodos reais - é uma ilustração do que está acontecendo no "pseudocódigo").

Quando você clica em um botão, arrasta uma barra de rolagem ou digita uma tecla, está gerando um "evento". Esse "evento" é colocado em uma fila, e o aplicativo processará a fila de eventos quando tiver a oportunidade de fazê-lo. O código do usuário que é acionado em resposta ao evento é chamado de manipulador de eventos. Esses manipuladores de eventos são invocados como parte da chamada process_events().

Depois que um aplicativo tiver processado todos os eventos disponíveis, ele redraw() a GUI. Isso leva em conta todas as alterações que os eventos causaram na exibição do aplicativo, bem como qualquer outra coisa que esteja acontecendo no sistema operacional - por exemplo, as janelas de outro aplicativo podem obscurecer ou revelar parte da janela do nosso aplicativo, e o redesenho do nosso aplicativo precisará refletir a parte da janela que está visível no momento.

O detalhe importante a ser observado: enquanto um aplicativo estiver processando um evento, ele não pode redesenhar e não pode processar outros eventos.

Isso significa que qualquer lógica de usuário contida em um manipulador de eventos precisa ser concluída rapidamente. Qualquer atraso na conclusão do manipulador de eventos será observado pelo usuário como uma desaceleração (ou parada) nas atualizações da GUI. Se esse atraso for longo o suficiente, seu sistema operacional poderá informar isso como um problema - os ícones "bola de praia" do macOS e "botão giratório" do Windows são o sistema operacional informando que seu aplicativo está demorando demais em um manipulador de eventos.

Operações simples como "atualizar um rótulo" ou "recomputar o total das entradas" são fáceis de concluir rapidamente. Entretanto, há muitas operações que não podem ser concluídas rapidamente. Se estiver realizando um cálculo matemático complexo, ou indexando todos os arquivos em um sistema de arquivos, ou realizando uma grande solicitação de rede, não é possível "simplesmente fazer isso rapidamente" - as operações são inerentemente lentas.

Então, como realizamos operações de longa duração em um aplicativo de GUI?

Programação assíncrona

O que precisamos é de uma maneira de informar a um aplicativo no meio de um manipulador de eventos de longa duração que não há problema em liberar temporariamente o controle de volta para o loop de eventos, desde que possamos retomar de onde paramos. Cabe ao aplicativo determinar quando essa liberação pode ocorrer; mas se o aplicativo liberar o controle para o loop de eventos regularmente, poderemos ter um manipulador de eventos de longa duração e manter uma interface de usuário responsiva.

Podemos fazer isso usando a programação assíncrona. A programação assíncrona é uma maneira de descrever um programa que permite que o intérprete execute várias funções ao mesmo tempo, compartilhando recursos entre todas as funções executadas simultaneamente.

As funções assíncronas (conhecidas como co-rotinas) precisam ser explicitamente declaradas como assíncronas. Elas também precisam declarar internamente quando existe uma oportunidade de mudar o contexto para outra co-rotina.

Em Python, a programação assíncrona é implementada usando as palavras-chave async e await e o módulo asyncio na biblioteca padrão. A palavra-chave async nos permite declarar que uma função é uma co-rotina assíncrona. A palavra-chave await fornece uma maneira de declarar quando existe uma oportunidade de mudar o contexto para outra co-rotina. O módulo asyncio fornece algumas outras ferramentas e primitivas úteis para codificação assíncrona.

Tornando o tutorial assíncrono

Para tornar nosso tutorial assíncrono, modifique o manipulador de eventos say_hello() para que ele tenha a seguinte aparência:

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']}",
        )
    )

Há apenas 4 alterações nesse código em relação à versão anterior:

  1. O cliente criado é um AsyncClient() assíncrono, em vez de um Client() síncrono. Isso informa ao httpx que ele deve operar no modo assíncrono, e não no modo síncrono.
  2. O gerenciador de contexto usado para criar o cliente é marcado como async. Isso informa ao Python que há uma oportunidade de liberar o controle à medida que o gerenciador de contexto entra e sai.
  3. A chamada get é feita com uma palavra-chave await. Isso instrui o aplicativo que, enquanto aguardamos a resposta da rede, ele pode liberar o controle para o loop de eventos. Já vimos essa palavra-chave antes - também usamos await ao exibir a caixa de diálogo. O motivo desse uso é o mesmo da solicitação HTTP - precisamos informar ao aplicativo que, enquanto a caixa de diálogo é exibida e estamos aguardando que o usuário pressione um botão, não há problema em liberar o controle de volta para o loop de eventos.

Também é importante observar que o próprio manipulador é definido como async def, em vez de apenas def. Isso informa ao Python que o método é uma corrotina assíncrona. Fizemos essa alteração no Tutorial 3, quando adicionamos a caixa de diálogo. Você só pode usar instruções await dentro de um método declarado como async def.

A Toga permite que você use métodos regulares ou co-rotinas assíncronas como manipuladores; a Toga gerencia tudo nos bastidores para garantir que o manipulador seja chamado ou aguardado conforme necessário.

Se você salvar essas alterações e executar novamente o aplicativo (com o briefcase dev no modo de desenvolvimento ou atualizando e executando novamente o aplicativo empacotado), não haverá nenhuma alteração óbvia no aplicativo. No entanto, ao clicar no botão para acionar a caixa de diálogo, você poderá notar uma série de melhorias sutis:

  • O botão retorna a um estado "não clicado", em vez de ficar preso em um estado "clicado".
  • O ícone "bola de praia"/"botão giratório" não será exibido.
  • Se você mover/redimensionar a janela do aplicativo enquanto aguarda a exibição da caixa de diálogo, a janela será redesenhada.
  • Se você tentar abrir um menu de aplicativo, o menu será exibido imediatamente.

Agora podemos executar o aplicativo completo. No entanto, como adicionamos um requisito extra (httpx), também precisamos atualizar os requisitos do nosso aplicativo; podemos fazer isso passando -r para briefcase run. Isso atualizará os requisitos do nosso aplicativo, reconstruirá o aplicativo e, em seguida, o iniciará:

(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

Você deverá ver seu aplicativo em execução e permanecer responsivo quando pressionar o botão e o conteúdo da rede for recuperado.

Próximos passos

Esta foi uma amostra do que você pode fazer com as ferramentas fornecidas pelo projeto BeeWare. No decorrer deste tutorial, você terá:

  • Criou um novo projeto de aplicativo GUI;
  • Inicie o app no modo de desenvolvedor
  • Criou o aplicativo como um binário autônomo para um sistema operacional de desktop;
  • Empacotou esse projeto para distribuição a outras pessoas;
  • Execute o aplicativo em um simulador e/ou dispositivo móvel;
  • Execute o aplicativo como um aplicativo da Web;
  • Adicionou uma dependência de terceiros ao seu aplicativo; e
  • Modificou o aplicativo para que ele permaneça responsivo.

Então, o que fazer a partir daqui?