Lors du développement d'applications Android, il est crucial de comprendre que l'interface utilisateur (UI) ne peut être modifiée que depuis le fil (thread) principal. Tenter de mettre à jour un composant graphique depuis un thread d'arrière-plan entraînera une exception. Plusieurs mécanismes existent pour coordonner ce travail entre threads, le plus classique étant l'utilisation de la classe Handler.
L'approche directe (invalide)
Une erreur courante pour les développeurs venant de Java standard est de lancer un thread et de modifier directement une vue.
// CECI EST INCORRECT ET PROVOQUERA UNE EXCEPTION
new Thread(() -> {
monVue.invalidate(); // Échec : accès UI depuis un thread d'arrière-plan
}).start();
Cela viole le modèle de thread unique d'Android pour l'UI.
Combinaison Thread et Handler
La méthode fondamentale consiste à créer un Handler associé au thread principal. Ce handler peut recevoir des messages ou exécuter des Runnable envoyés depuis d'autres threads, garantissant ainsi que le code s'exécute dans le bon contexte.
// Déclarer un Handler lié au Looper du thread principal (par défaut)
final Handler uiHandler = new Handler(Looper.getMainLooper());
// Dans un thread d'arrière-plan
new Thread(() -> {
// ... opération longue ...
// Publication d'un résultat sur le thread UI via le Handler
uiHandler.post(() -> {
resultatTextView.setText("Données chargées !");
});
}).start();
Pour une communication plus structurée, on peut utiliser des messages avec un identifiant (what).
private static final int MSG_MAJ_VUE = 1;
// Le handler qui traite les messages
Handler messageHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG_MAJ_VUE) {
int valeur = msg.arg1;
compteurView.setText(String.valueOf(valeur));
}
}
};
// Code dans un thread de travail pour envoyer un message
Message msg = messageHandler.obtainMessage(MSG_MAJ_VUE, compteur, 0);
messageHandler.sendMessage(msg);
Utilisation de Timer et TimerTask (non recommandé)
La classe Timer de Java peut être utilisée pour planifier des tâches répétitives. Cependant, son utilisation directe dans Android est déconseillée car la tâche s'exécute dans un thread de la bibliothèque et doit elle-même faire appel à un Handler pour interagir avec l'UI.
// Création d'un Timer et d'une tâche qui utilise un Handler pour l'UI
Timer tempo = new Timer();
Handler tempoHandler = new Handler(Looper.getMainLooper());
TimerTask tachePeriodique = new TimerTask() {
@Override
public void run() {
tempoHandler.post(() -> {
titleTextView.setText("Mise à jour périodique !");
});
}
};
// Planification : délai initial de 1 seconde, répétition toutes les 2 secondes
tempo.schedule(tachePeriodique, 1000, 2000);
Pour des tâches répétitives simples, les méthodes du Handler lui-même sont souvent plus adaptées.
Planification avec Handler.postDelayed()
Le Handler offre une méthode directe pour exécuter un Runnable après un certain délai, et de le répéter en le repostent.
private Handler delaiHandler = new Handler(Looper.getMainLooper());
private int compteur = 0;
private boolean actif = false;
private Runnable tacheRepetitive = new Runnable() {
@Override
public void run() {
if (actif) {
compteur++;
compteurTextView.setText("Total : " + compteur);
// Reprogrammer la même tâche dans 1,5 seconde
delaiHandler.postDelayed(this, 1500);
}
}
};
// Pour démarrer
public void demarrerCompteur() {
actif = true;
delaiHandler.postDelayed(tacheRepetitive, 1500); // Premier délai
}
// Pour arrêter
public void arreterCompteur() {
actif = false;
delaiHandler.removeCallbacks(tacheRepetitive); // Annuler la tâche en attente
}
Concepts fondamentaux sous-jacents
Handler : Objet permettant d'envoyer et de traiter des messages (Message) ou des Runnable liés au fil d'exécution de son Looper associé. C'est le principle mécanisme de communication inter-threads pour l'UI.
Looper : Boucle de mesages infinie attachée à un thread. Le thread principal possède un Looper par défaut. Pour qu'un thread d'arrière-plan ait son propre Looper, on peut utiliser HandlerThread.
Message : Objet contenant des données (champ what, entiers arg1/arg2, Bundle de données) et un Handler cible. Il est placé dans la file d'attente du Looper et distribué au Handler.handleMessage().
Bien que fondamental, l'utilisation directe des Handler et des threads bruts peut être complexe et sujette aux erreurs (fuites de mémoire, état de l'application). Pour les tâches asynchrones modernes sur Android, des abstractions de plus haut niveau comme les coroutines Kotlin (avec lifecycleScope, viewModelScope) ou les ViewModels avec LiveData et StateFlow sont aujourd'hui recommandées, car elles simplifient grandement la gestion du cycle de vie et la communication avec l'UI.