Tutorial 8 - Suavizando o Processo

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

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

  1. O método é definido como async def, em vez de apenas def. Isso informa ao Python que o método é uma co-rotina assíncrona.

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

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

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

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

Próximos passos

Agora temos um aplicativo que é elegante e responsivo, mesmo quando está esperando em uma API lenta. Mas como podemos ter certeza de que o aplicativo continuará funcionando à medida que continuarmos a desenvolvê-lo? Como testamos nosso aplicativo? Consulte o Tutorial 9 para descobrir…