Asynchronous Updates
L'API Environment.runLater() fournit un mécanisme permettant de mettre à jour l'interface utilisateur en toute sécurité à partir de threads en arrière-plan dans les applications webforJ. Cette fonctionnalité expérimentale permet des opérations asynchrones tout en maintenant la sécurité des threads pour les modifications de l'interface utilisateur.
The webforj-handling-timers-and-async skill can schedule timers, debouncers, and async work safely on the UI thread. After installing the webforJ AI plugin, ask your assistant:
- "Refresh this dashboard every 30 seconds."
- "Add a search-as-you-type debouncer."
- "Run this CPU-heavy work in the background and update the progress bar."
Comprendre le modèle de thread
webforJ impose un modèle de thread strict où toutes les opérations de l'interface utilisateur doivent s'exécuter sur le thread Environment. Cette restriction existe parce que :
- Contraintes de l'API webforJ : L'API webforJ sous-jacente est liée au thread qui a créé la session
- Affinité des threads des composants : Les composants de l'interface utilisateur conservent un état qui n'est pas sûr pour les threads
- Distribution des événements : Tous les événements UI sont traités séquentiellement sur un seul thread
Ce modèle à thread unique empêche les conditions de course et maintient un état cohérent pour tous les composants de l'interface utilisateur, mais crée des défis lors de l'intégration avec des tâches de calcul asynchrones et de longue durée.
API RunLater
L'API Environment.runLater() fournit deux méthodes pour planifier des mises à jour de l'interface utilisateur :
// Planifier une tâche sans valeur de retour
public static PendingResult<Void> runLater(Runnable task)
// Planifier une tâche qui renvoie une valeur
public static <T> PendingResult<T> runLater(Supplier<T> supplier)
Les deux méthodes renvoient un PendingResult qui suit l'achèvement de la tâche et permet d'accéder au résultat ou à toute exception survenue.
Héritage du contexte de thread
L'héritage automatique du contexte est une fonctionnalité critique de Environment.runLater(). Lorsqu'un thread en cours d'exécution dans un Environment crée des threads enfants, ces enfants héritent automatiquement de la capacité à utiliser runLater().
Comment fonctionne l'héritage
Tout thread créé à partir d'un thread Environment a automatiquement accès à cet Environment. Cet héritage se produit automatiquement, donc vous n'avez pas besoin de passer de contexte ou de configurer quoi que ce soit.
@Route
public class DataView extends Composite<Div> {
private final ExecutorService executor = Executors.newCachedThreadPool();
public DataView() {
// Ce thread a le contexte Environment
// Les threads enfants héritent automatiquement le contexte
executor.submit(() -> {
String data = fetchRemoteData();
// Peut utiliser runLater car le contexte a été hérité
Environment.runLater(() -> {
dataLabel.setText(data);
loadingSpinner.setVisible(false);
});
});
}
}
Threads sans contexte
Les threads créés en dehors du contexte Environment ne peuvent pas utiliser runLater() et généreront une IllegalStateException :
// Initialiseur statique - pas de contexte Environment
static {
new Thread(() -> {
Environment.runLater(() -> {}); // Génère IllegalStateException
}).start();
}
// Threads de minuterie système - pas de contexte Environment
Timer timer = new Timer();
timer.schedule(new TimerTask() {
public void run() {
Environment.runLater(() -> {}); // Génère IllegalStateException
}
}, 1000);
// Threads de bibliothèque externe - pas de contexte Environment
httpClient.sendAsync(request, responseHandler)
.thenAccept(response -> {
Environment.runLater(() -> {}); // Génère IllegalStateException
});
Comportement d'exécution
Le comportement d'exécution de runLater() dépend du thread qui l'appelle :
Depuis le thread UI
Lorsqu'il est appelé depuis le thread Environment lui-même, les tâches s'exécutent synchroniquement et immédiatement :
button.onClick(e -> {
System.out.println("Avant : " + Thread.currentThread().getName());
PendingResult<String> result = Environment.runLater(() -> {
System.out.println("À l'intérieur : " + Thread.currentThread().getName());
return "terminé";
});
System.out.println("Après : " + result.isDone()); // true
});
Avec ce comportement synchrone, les mises à jour de l'interface utilisateur des gestionnaires d'événements sont appliquées immédiatement et n'entraînent pas de surcharge de mise en file d'attente inutile.
Depuis les threads en arrière-plan
Lorsqu'il est appelé depuis un thread en arrière-plan, les tâches sont mise en file d'attente pour une exécution asynchrone :
@Override
public void onDidCreate() {
CompletableFuture.runAsync(() -> {
// Cela s'exécute sur le thread ForkJoinPool
System.out.println("Arrière-plan : " + Thread.currentThread().getName());
PendingResult<Void> result = Environment.runLater(() -> {
// Cela s'exécute sur le thread Environment
System.out.println("Mise à jour UI : " + Thread.currentThread().getName());
statusLabel.setText("Traitement terminé");
});
// result.isDone() serait faux ici
// La tâche est mise en file d'attente et sera exécutée de manière asynchrone
});
}
webforJ traite les tâches soumises depuis des threads en arrière-plan dans un ordre FIFO strict, préservant la séquence des opérations même lorsqu'elles sont soumises à partir de plusieurs threads en concurrence. Avec cette garantie d'ordre, les mises à jour de l'interface utilisateur sont appliquées dans l'ordre exact où elles ont été soumises. Ainsi, si le thread A soumet la tâche 1, puis le thread B soumet la tâche 2, la tâche 1 s'exécutera toujours avant la tâche 2 sur le thread UI. Le traitement des tâches dans l'ordre FIFO empêche les incohérences dans l'interface utilisateur.
Annulation des tâches
Le PendingResult renvoyé par Environment.runLater() prend en charge l'annulation, vous permettant d'empêcher l'exécution des tâches mises en file d'attente. En annulant les tâches en attente, vous pouvez éviter les fuites de mémoire et empêcher les opérations de longue durée de mettre à jour l'interface utilisateur après qu'elles ne soient plus nécessaires.
Annulation basique
PendingResult<Void> result = Environment.runLater(() -> {
updateUI();
});
// Annuler si nous ne sommes pas encore exécutés
if (!result.isDone()) {
result.cancel();
}
Gestion de plusieurs mises à jour
Lors de l'exécution d'opérations de longue durée avec des mises à jour fréquentes de l'interface utilisateur, suivez tous les résultats en attente :
public class LongRunningTask {
private final List<PendingResult<?>> pendingUpdates = new ArrayList<>();
private volatile boolean isCancelled = false;
public void startTask() {
CompletableFuture.runAsync(() -> {
for (int i = 0; i <= 100; i++) {
if (isCancelled) return;
final int progress = i;
PendingResult<Void> update = Environment.runLater(() -> {
progressBar.setValue(progress);
});
// Suivre pour une éventuelle annulation
pendingUpdates.add(update);
Thread.sleep(100);
}
});
}
public void cancelTask() {
isCancelled = true;
// Annuler toutes les mises à jour UI en attente
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}
Gestion du cycle de vie des composants
Lorsque les composants sont détruits (par exemple, lors de la navigation), annuler toutes les mises à jour en attente pour éviter les fuites de mémoire :
@Route
public class CleanupView extends Composite<Div> {
private final List<PendingResult<?>> pendingUpdates = new ArrayList<>();
@Override
protected void onDestroy() {
super.onDestroy();
// Annuler toutes les mises à jour en attente pour éviter les fuites de mémoire
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}
Considérations de conception
-
Exigence de contexte : Les threads doivent avoir hérité d'un contexte
Environment. Les threads de bibliothèques externes, les minuteries système et les initialisateurs statiques ne peuvent pas utiliser cette API. -
Prévention des fuites de mémoire : Suivez toujours et annulez les objets
PendingResultdans les méthodes de cycle de vie des composants. Les lambdas mises en file d'attente capturent des références aux composants de l'interface utilisateur, empêchant la collecte des ordures si elles ne sont pas annulées. -
Exécution FIFO : Toutes les tâches s'exécutent dans un ordre strict FIFO, indépendamment de leur importance. Il n'y a pas de système de priorité.
-
Limitations de l'annulation : L'annulation empêche uniquement l'exécution des tâches mises en file d'attente. Les tâches déjà en cours d'exécution s'achèveront normalement.
Étude de cas complète : LongTaskView
Ce qui suit est une mise en œuvre complète, prête pour la production, démontrant toutes les meilleures pratiques pour des mises à jour UI asynchrones :
Analyse de l'étude de cas
Cette mise en œuvre démontre plusieurs modèles critiques :
1. Gestion des pools de threads
private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "LongTaskView-Worker");
t.setDaemon(true);
return t;
});
- Utilise un exécuteur à thread unique pour prévenir l'épuisement des ressources
- Crée des threads daemon qui ne permettront pas l'arrêt de la JVM
2. Suivi des mises à jour en attente
private final List<PendingResult<?>> pendingUIUpdates = new ArrayList<>();
Chaque appel à Environment.runLater() est suivi pour permettre :
- L'annulation lorsque l'utilisateur clique sur annuler
- La prévention des fuites de mémoire dans
onDestroy() - Un nettoyage approprié pendant le cycle de vie du composant
3. Annulation coopérative
private volatile boolean isCancelled = false;
Le thread en arrière-plan vérifie ce drapeau à chaque itération, permettant :
- Une réponse immédiate à l'annulation
- Une sortie propre de la boucle
- La prévention de mises à jour supplémentaires de l'UI
4. Gestion du cycle de vie
@Override
protected void onDestroy() {
super.onDestroy();
cancelTask(); // Réutilise la logique d'annulation
currentTask = null;
executor.shutdown();
}
Critique pour prévenir les fuites de mémoire en :
- Annulant toutes les mises à jour UI en attente
- Interrompant les threads en cours d'exécution
- Arrêtant l'exécuteur
5. Test de réactivité de l'UI
testButton.onClick(e -> {
int count = clickCount.incrementAndGet();
showToast("Clique #" + count + " - L'UI est réactive !", Theme.GRAY);
});
Démontre que le thread de l'interface utilisateur reste réactif pendant les opérations en arrière-plan.