Aller au contenu

Tutoriel 8 - Le rendre lisse

Jusqu'à présent, notre application a été relativement simple : affichage des widgets de l'interface graphique, appel d'une bibliothèque tierce simple et affichage des résultats dans une boîte de dialogue. Toutes ces opérations se déroulent très rapidement et notre application reste réactive.

Cependant, dans une application réelle, nous devrons effectuer des tâches ou des calculs complexes qui peuvent prendre un certain temps, et nous voulons que notre application reste réactive au fur et à mesure de l'exécution de ces tâches. Modifions notre application pour qu'elle prenne un peu de temps et voyons les changements à apporter pour tenir compte de ce comportement.

Accéder à une API

L'une des tâches les plus courantes et les plus fastidieuses qu'une application doit effectuer consiste à demander à une API web de récupérer des données et de les afficher à l'utilisateur. Les API web mettent parfois une ou deux secondes à répondre, de sorte que si nous appelons une API de ce type, nous devons veiller à ce que notre application ne devienne pas inactive pendant que nous attendons que l'API web renvoie une réponse.

Il s'agit d'une application ludique, nous ne disposons donc pas d'une véritable API avec laquelle travailler. Nous utiliserons donc un exemple de point de terminaison API comme source de données. Si vous ouvrez https://tutorial.beeware.org/tutorial/message.json dans votre navigateur, vous obtiendrez une charge utile JSON avec un message.

La bibliothèque standard de Python contient tous les outils dont vous avez besoin pour accéder à une API. Cependant, les API intégrées sont de très bas niveau. Ce sont de bonnes implémentations du protocole HTTP, mais elles exigent de l'utilisateur qu'il gère de nombreux détails de bas niveau, comme la redirection d'URL, les sessions, l'authentification et l'encodage des données utiles. En tant qu'"utilisateur de navigateur normal", vous avez probablement l'habitude de considérer ces détails comme allant de soi, puisque le navigateur les gère pour vous.

En conséquence, des bibliothèques tierces ont été développées pour envelopper les API intégrées et fournir une API plus simple qui correspond mieux à l'expérience quotidienne du navigateur. Nous allons utiliser l'une de ces bibliothèques pour accéder à l'API {JSON} Placeholder API - une bibliothèque appelée httpx. Briefcase utilise httpx en interne, donc elle est déjà dans votre environnement local - vous n'avez pas besoin de l'installer séparément pour l'utiliser ici.

Ajoutons un appel API httpx à notre application. Modifions le paramètre requires dans notre pyproject.toml pour inclure la nouvelle exigence :

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

Ajoutez un import au début de app.py pour importer httpx :

import httpx

Puis modifiez le callback say_hello() pour qu'il ressemble à ceci :

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

Ceci modifiera le callback say_hello() de telle sorte que lorsqu'il est invoqué, il le fera :

  • effectuer une requête GET sur l'API JSON pour obtenir le poste 42 ;
  • décoder la réponse en JSON ;
  • extraire le corps du message ; et
  • inclure le corps de ce message dans le texte de la boîte de dialogue "message", à la place du texte généré par Faker.

Lançons notre application mise à jour dans le mode développeur de Briefcase pour vérifier que notre changement a fonctionné. Comme nous avons ajouté une nouvelle exigence, nous devons demander au mode développeur de réinstaller les exigences, en utilisant l'argument -r :

(beeware-venv) $ briefcase dev -r

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

Lorsque vous entrez un nom et que vous appuyez sur le bouton, une boîte de dialogue doit s'afficher :

Tutoriel Hello World 8 dialogue, sur macOS

(beeware-venv) $ briefcase dev -r

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

Lorsque vous entrez un nom et que vous appuyez sur le bouton, une boîte de dialogue doit s'afficher :

Tutoriel Hello World 8 dialogue, sur Linux

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

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

Lorsque vous entrez un nom et que vous appuyez sur le bouton, une boîte de dialogue doit s'afficher :

Tutoriel Hello World 8 dialogue, sur
Windows

Vous ne pouvez pas exécuter une application Android en mode développeur - utilisez les instructions pour la plateforme de bureau que vous avez choisie.

Vous ne pouvez pas exécuter une application iOS en mode développeur - utilisez les instructions pour la plateforme de bureau que vous avez choisie.

À moins que vous ne disposiez d'une connexion internet très rapide, vous remarquerez peut-être que lorsque vous appuyez sur le bouton, l'interface graphique de votre application se bloque pendant un petit moment. Le système d'exploitation peut même manifester ce blocage par un curseur "beachball" ou "spinner" pour indiquer que l'application ne répond pas.

A moins que vous ne disposiez d'une connexion internet très rapide, vous remarquerez peut-être que lorsque vous appuyez sur le bouton, l'interface graphique de votre application se bloque pendant un petit moment. C'est parce que la requête web que nous avons faite est synchrone. Lorsque notre application effectue la requête web, elle attend que l'API renvoie une réponse avant de continuer. Pendant cette attente, l'API ne permet pas à l'application de se redessiner, ce qui a pour effet de bloquer l'application.

Boucles d'événements de l'interface graphique

Pour comprendre pourquoi cela se produit, nous devons entrer dans les détails du fonctionnement d'une application GUI. Les spécificités varient en fonction de la plate-forme, mais les concepts de haut niveau sont les mêmes, quelle que soit la plate-forme ou l'environnement d'interface graphique que vous utilisez.

Une application GUI est, fondamentalement, une boucle unique qui ressemble à quelque chose comme :

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

Cette boucle est appelée boucle d'événements. (Il ne s'agit pas de noms de méthodes réels, mais d'une illustration de ce qui se passe dans le "pseudo-code").

Lorsque vous cliquez sur un bouton, faites glisser une barre de défilement ou tapez une touche, vous générez un "événement". Cet "événement" est placé dans une file d'attente, et l'application traitera la file d'événements lorsqu'elle en aura l'occasion. Le code utilisateur déclenché en réponse à l'événement est appelé event handler (gestionnaire d'événement). Ces gestionnaires d'événements sont invoqués dans le cadre de l'appel process_events().

Une fois qu'une application a traité tous les événements disponibles, elle va redraw() l'interface graphique. Cela prend en compte tous les changements que les événements ont causés à l'affichage de l'application, ainsi que tout ce qui se passe dans le système d'exploitation - par exemple, les fenêtres d'une autre application peuvent masquer ou révéler une partie de la fenêtre de notre application, et le redessin de notre application devra refléter la partie de la fenêtre qui est actuellement visible.

Détail important : pendant qu'une application traite un événement, elle ne peut pas redessiner, et elle ne peut pas traiter d'autres événements.

Cela signifie que toute logique utilisateur contenue dans un gestionnaire d'événements doit être exécutée rapidement. Tout retard dans l'exécution du gestionnaire d'événements sera observé par l'utilisateur sous la forme d'un ralentissement (ou d'un arrêt) des mises à jour de l'interface graphique. Si ce délai est suffisamment long, votre système d'exploitation peut signaler qu'il s'agit d'un problème - les icônes macOS "beachball" et Windows "spinner" indiquent que votre application prend trop de temps dans un gestionnaire d'événements.

Des opérations simples comme "mettre à jour une étiquette" ou "recalculer le total des entrées" sont faciles à réaliser rapidement. Cependant, de nombreuses opérations ne peuvent pas être effectuées rapidement. Si vous effectuez un calcul mathématique complexe, si vous indexez tous les fichiers d'un système de fichiers ou si vous effectuez une requête réseau importante, vous ne pouvez pas "faire vite" - les opérations sont intrinsèquement lentes.

Alors, comment effectuer des opérations à long terme dans une application GUI ?

Programmation asynchrone

Ce dont nous avons besoin, c'est d'un moyen de dire à une application au milieu d'un gestionnaire d'événements de longue durée qu'il est acceptable de relâcher temporairement le contrôle dans la boucle d'événements, tant que nous pouvons reprendre là où nous nous sommes arrêtés. C'est à l'application de déterminer quand cette libération peut avoir lieu ; mais si l'application libère le contrôle dans la boucle d'événements régulièrement, nous pouvons avoir un gestionnaire d'événements de longue durée et maintenir une interface utilisateur réactive.

Nous pouvons le faire en utilisant la programmation asynchrone. La programmation asynchrone est une façon de décrire un programme qui permet à l'interpréteur d'exécuter plusieurs fonctions en même temps, en partageant les ressources entre toutes les fonctions qui s'exécutent simultanément.

Les fonctions asynchrones (appelées co-routines) doivent être explicitement déclarées comme étant asynchrones. Elles doivent également déclarer en interne lorsqu'il est possible de changer de contexte et de passer à une autre co-routine.

En Python, la programmation asynchrone est implémentée à l'aide des mots-clés async et await, et du module asyncio dans la bibliothèque standard. Le mot-clé async nous permet de déclarer qu'une fonction est une co-routine asynchrone. Le mot-clé await permet de déclarer qu'il existe une opportunité de changer de contexte vers une autre co-routine. Le module `asyncio](https://docs.python.org/3/library/asyncio.html) fournit d'autres outils et primitives utiles pour le codage asynchrone.

Rendre le didacticiel asynchrone

Pour rendre notre tutoriel asynchrone, modifiez le gestionnaire d'événement say_hello() pour qu'il ressemble à ceci :

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

Il n'y a que 4 changements dans ce code par rapport à la version précédente :

  1. Le client créé est un AsyncClient() asynchrone, plutôt qu'un Client() synchrone. Cela indique à httpx qu'il doit fonctionner en mode asynchrone, plutôt qu'en mode synchrone.
  2. Le gestionnaire de contexte utilisé pour créer le client est marqué comme async. Cela indique à Python qu'il y a une opportunité de relâcher le contrôle lorsque le gestionnaire de contexte est entré et sorti.
  3. L'appel get est fait avec un mot-clé await. Cela indique à l'application que pendant que nous attendons la réponse du réseau, l'application peut laisser le contrôle à la boucle d'événements. Nous avons déjà vu ce mot-clé auparavant - nous utilisons également await lors de l'affichage de la boîte de dialogue. La raison de cette utilisation est la même que pour la requête HTTP - nous devons dire à l'application que pendant que la boîte de dialogue est affichée, et que nous attendons que l'utilisateur appuie sur un bouton, il est acceptable de redonner le contrôle à la boucle d'événements.

Il est également important de noter que le gestionnaire lui-même est défini comme async def, plutôt que comme def. Cela indique à Python que la méthode est une coroutine asynchrone. Nous avons fait ce changement dans le Tutoriel 3 lorsque nous avons ajouté la boîte de dialogue. Vous ne pouvez utiliser les instructions await qu'à l'intérieur d'une méthode déclarée comme async def.

Toga vous permet d'utiliser des méthodes normales ou des co-programmes asynchrones en tant que gestionnaires ; Toga gère tout en coulisses pour s'assurer que le gestionnaire est invoqué ou attendu selon les besoins.

Si vous sauvegardez ces changements et relancez l'application en mode développement, il n'y aura pas de changements évidents dans l'application. Cependant, lorsque vous cliquez sur le bouton pour déclencher le dialogue, vous pouvez remarquer un certain nombre d'améliorations subtiles :

  • Le bouton revient à l'état "décliqué" au lieu d'être bloqué à l'état "cliqué".
  • L'icône "beachball"/"spinner" n'apparaît pas.
  • Si vous déplacez ou redimensionnez la fenêtre de l'application en attendant que la boîte de dialogue s'affiche, la fenêtre se redessinera.
  • Si vous essayez d'ouvrir un menu d'application, le menu s'affiche immédiatement.

Nous pouvons maintenant exécuter l'application complète. Cependant, comme nous avons ajouté une exigence supplémentaire (httpx), nous devons également mettre à jour les exigences de notre application ; nous pouvons le faire en passant -r à briefcase run. Cela mettra à jour les exigences de notre application, puis recompilera l'application, et enfin la lancera :

(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

Vous devriez voir votre application fonctionner et rester réactive lorsque vous appuyez sur le bouton et que le contenu du réseau est récupéré.

Étapes suivantes

Il s'agit d'un avant-goût de ce que vous pouvez faire avec les outils fournis par le projet BeeWare. Au cours de ce tutoriel, vous avez :

  • Création d'un nouveau projet d'application GUI ;
  • Exécuter l'application en mode développeur
  • L'application a été conçue comme un binaire autonome pour un système d'exploitation de bureau ;
  • Il a emballé ce projet pour le distribuer à d'autres personnes ;
  • Exécuter l'application sur un simulateur et/ou un appareil mobile ;
  • Exécuter l'application en tant qu'application web ;
  • Ajout d'une dépendance tierce à votre application ; et
  • Modifier l'application pour qu'elle reste réactive.

Alors, quelle est la suite des événements ?