Ir para o conteúdo

Tutorial 8 - Tornar Suuuuaaave

Até agora, a nossa aplicação tem sido relativamente simples - mostrar widgets de GUI, chamar uma biblioteca simples de terceiros, e mostrar o resultado num diálogo. Todas essas operações ocorrem muito rapidamente, e a nossa aplicação permanece funcional.

Entretanto, numa aplicação do mundo real, vamos precisar de executar tarefas ou cálculos complexos que podem levar algum tempo para serem concluídos - e conforme essas tarefas forem executadas, queremos que a nossa aplicação permaneça funcional. Vamos fazer uma alteração na nossa aplicação que pode levar algum tempo a ser concluída, e ver as alterações que precisam ser feitas para acomodar esse comportamento.

Aceder a uma API

Uma tarefa comum e demorada que uma aplicação precisa executar é fazer uma solicitação a uma API Web para recuperar dados, e mostrar esses dados ao utilizador. Por vezes, as APIs Web levam um ou dois segundos a responder, assim se estivermos a chamar uma API dessas, precisamos garantir que nossa aplicação não deixe de responder enquanto esperamos que a API Web devolva uma resposta.

Esta é uma aplicação de brincar, portanto não temos uma API real com a qual trabalhar, então vamos usar um ponto final de API de exemplo como fonte de dados. Se abrir https://tutorial.beeware.org/tutorial/message.json no seu navegador, receberá uma resposta JSON com uma mensagem.

A biblioteca standard do Python contém todas as ferramentas que vai precisar para aceder a uma API. No entanto, as APIs incorporadas são de nível muito baixo. Elas são boas implementações do protocolo HTTP - mas exigem que o utilizador faça gestão de muitos detalhes de baixo nível, como redirecionar de URL, sessões, autenticação e codificação de carga útil. Como um "utilizador normal de navegador", está provavelmente acostumado a considerar esses detalhes como garantidos, pois o navegador gere-os por si.

Como resultado, pessoas desenvolveram bibliotecas de terceiros que envolvem as APIs integradas e fornecem uma API mais simples mais próxima da experiência diária dum navegador. Vamos usar uma dessas bibliotecas — uma biblioteca chamada httpx — para aceder a uma API simples.

Vamos adicionar uma chamada API httpx à nossa aplicação. Primeiro, tal como fizemos com o faker em passo anterior, precisamos informar o briefcase para instalar o httpx quando ele compilar a nossa aplicação. Modifique a configuração requires no nosso pyproject.toml para incluir o novo requisito:

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

Adicione uma importação no topo do app.py para importar o httpx:

import httpx

Depois modifique a chamada de retorno say_hello() para ficar como isto:

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"Uma mensagem de {fake.name()}: {payload['body']}",
        )
    )

Isso vai alterar a chamada de retorno say_hello() para que, quando é invocada, vai fazer:

  • faça um pedido GET na API do tutorial para recuperar uma mensagem;
  • descodificar a resposta como JSON;
  • extrair o corpo da mensagem; e
  • incluir o corpo dessa mensagem como o texto do diálogo, no lugar do texto gerado pelo Faker.

Vamos correr a nossa aplicação atualizada 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] A instalar os pré-requisitos...
...
[helloworld] A iniciar no modo de desenvolvimento...
===========================================================================

Ao inserir um nome e pressionar o botão, deverá ver um diálogo semelhante a isto:

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

(beeware-venv) $ briefcase dev -r

[helloworld] A instalar os pré-requisitos...
...
[helloworld] A iniciar no modo de desenvolvimento...
===========================================================================

Ao inserir um nome e pressionar o botão, deverá ver um diálogo semelhante a isto:

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

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

[helloworld] A instalar pré-requisitos...
...
[helloworld] A iniciar no modo de desenvolvimento...
===========================================================================

Ao inserir um nome e pressionar o botão, deverá ver um diálogo semelhante a isto:

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

Não pode executar uma aplicação Android no modo de desenvolvedor - use as instruções para a sua plataforma de ambiente de trabalho escolhida.

Não pode executar uma aplicação iOS no modo de desenvolvedor - use as instruções para a plataforma de ambiente de trabalho escolhida.

A menos que tenha uma ligação de Internet realmente rápida, poderá perceber que ao pressionar o botão, a GUI da sua aplicação trava um pouco. O sistema operativo pode até manifestar isso com um cursor "ampulheta" ou "rotativo" para indicar que a aplicação não está a responder.

Isso acontece porque o pedido Web que fizemos é síncrono. Quando a nossa aplicação faz um pedido Web, ele espera que a API retorne uma resposta antes de continuar. Enquanto espera, não permissão à aplicação para redesenhar - e como resultado, a aplicação trava.

Ciclos de Eventos da GUI

Para entender porque isto acontece, precisamos aprofundar os detalhes de como funciona um aplicação 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 estiver a usar.

Um aplicação de GUI é, fundamentalmente, um único ciclo que se parece com:

enquanto não app.pedido_de_terminar():
    app.processa_eventos()
    app.redesenha()

Esse ciclo é chamado de Event Loop. (Esses não são nomes de métodos reais - é uma ilustração do que está a acontecer usando "pseudo-código").

Quando você clica num botão, arrasta uma barra de deslocamento, ou carrega numa tecla, está a gerar um "evento". Esse "evento" é colocado numa fila, e a aplicação vai processar a fila de eventos na próxima oportunidade de o fazer. O código do utilizador que é acionado em resposta ao evento é chamado de manipulador de eventos. Esses manipuladores de eventos são invocados como parte da chamada process_events().

Assim que a aplicação tiver processado todos os eventos disponíveis, ela vai redesenhar (redraw()) a GUI. Isso leva em conta todas as alterações que os eventos causaram na exibição da aplicação, bem como qualquer outra coisa que esteja a acontecer no sistema operativo - por exemplo, as janelas de outra aplicação podem obscurecer ou revelar parte da janela da nossa aplicação, e o redesenhar da nossa aplicação vai precisar de refletir a parte da janela que está visível no momento.

O detalhe importante a ser observado: enquanto uma aplicação estiver a processar um evento, ela não pode redesenhar e não pode processar outros eventos.

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

Operações simples como "atualizar uma etiqueta" ou "recalcular o total das entradas" são fáceis de concluir rapidamente. No entanto, há muitas operações que não podem ser concluídas rapidamente. Se estiver a realizar um cálculo matemático complexo, ou a indexar todos os ficheiros num sistema de ficheiros, ou a realizar uma grande solicitação de trabalho em rede, não pode "simplesmente fazer isso rapidamente" - estas operações são por natureza lentas.

Assim - como realizamos operações de longa duração numa aplicação de GUI?

Programação assíncrona

O que precisamos é de uma maneira de informar a aplicação no meio de um manipulador de eventos de longa duração que não há problema em libertar temporariamente o controle de volta para o ciclo de eventos, desde que se possa retomar de onde paramos. Cabe à aplicação determinar quando essa libertação pode ocorrer; mas se a aplicação libertar o controle para o ciclo de eventos regularmente, poderemos ter um manipulador de eventos de longa duração e manter uma UI a responder.

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 em simultâneo.

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 standard. A palavra-chave async permite-nos 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 úteis e primitivas para codificação assíncrona.

Tornar o tutorial assíncrono

Para tornar nosso tutorial assíncrono, modifique o manipulador de eventos say_hello() para que 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"Uma mensagem de {fake.name()}: {payload['body']}",
        )
    )

Há apenas três alterações nesta chamada de retorno a partir da 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 gestor de contexto usado para criar o cliente é marcado como async. Isso informa o Python que há uma oportunidade de libertar o controle à medida que se entra e sai do gestor de contexto.
  3. A chamada get é feita com uma palavra-chave await. Isso instruí a aplicação que, enquanto aguardamos a resposta da rede, ela pode libertar o controle para o ciclo de eventos. Já vimos essa palavra-chave antes - também usamos await ao mostrar a caixa de diálogo. O motivo desse uso é o mesmo da solicitação HTTP - precisamos informar a aplicação que, enquanto o diálogo é mostrado e estamos a aguardar que o utilizador pressione um botão, não há problema em libertar o controle de volta para o ciclo de eventos.

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

A Toga permite que use métodos regulares ou co-rotinas assíncronas como manipuladores; Toga gere tudo nos bastidores para garantir que o manipulador é invocado ou aguarda conforme necessário.

Se salvar estas alterações e executar novamente a aplicação em modo de desenvolvimento, não vai haver nenhuma alteração óbvia na aplicação. No entanto, ao clicar no botão para acionar o diálogo, vai poder notar uma série de melhorias subtis:

  • O botão retorna a um estado "não clicado", em vez de ficar preso num estado "clicado".
  • O ícone "bola de praia"/"botão giratório" não vai aparecer.
  • Se mover/redimensionar a janela da aplicação enquanto aguarda o aparecer do diálogo, a janela é redesenhada.
  • Se tentar abrir um menu de aplicação, o menu é mostrado imediatamente.

Agora podemos executar a aplicação completa. No entanto, como adicionamos um requisito extra (httpx), também temos de atualizar os requisitos da nossa aplicação; podemos fazer isso ao passar -r ao briefcase run. Isto vai atualizar os requisitos da nossa aplicação, recompilar a aplicação, e depois inicia-la:

(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

Deverá ver a sua aplicação a correr e a permanecer responsiva quando pressionar o botão e o conteúdo da rede for recuperado.

Próximos passos

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

  • Criou um novo projeto de aplicação GUI;
  • Correr essa aplicação em modo de desenvolvedor;
  • Criou a aplicação como um binário autónomo para um sistema operativo de ambiente de trabalho;
  • Empacotou esse projeto para distribuir a outras pessoas;
  • Correu a aplicação num simulador e/ou dispositivo móvel;
  • Correu a aplicação como uma aplicação Web;
  • Adicionou uma dependência de terceiros à sua aplicação; e
  • Modificou a aplicação para que ela permaneça responsiva.

Então - o que fazer a partir daqui?