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:

(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:

(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:

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:
- O cliente criado é um
AsyncClient()assíncrono, em vez de umClient()síncrono. Isso informa aohttpxque ele deve operar no modo assíncrono, e não no modo síncrono. - 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. - A chamada
geté feita com uma palavra-chaveawait. 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 usamosawaitao 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?
- Se quiser ir além, há alguns tutoriais de tópicos adicionais que detalham aspectos específicos do desenvolvimento de aplicativos.
- Se quiser saber mais sobre como criar interfaces de usuário complexas com o Toga, consulte a documentação do Toga. A Toga também tem seu próprio tutorial demonstrando como usar vários recursos do kit de ferramentas de widget.
- Se quiser saber mais sobre os recursos do Briefcase, você pode se aprofundar na documentação do Briefcase.