• 20 hours
  • Hard

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 12/12/19

Créez vos premières tâches asynchrones

Implémentation

Par défaut donc, toutes les opérations, traitements ou configurations que nous créons jusqu'à maintenant dans nos mini-applications Android sont exécutés sur le Main Thread. Heureusement, ceux-ci n'étaient pas si longs que ça... :p

Mais alors comment allons-nous déporter des opérations/tâches longues sur un Thread dédié ? Eh bien les possibilités sont nombreuses ! Mais nous allons nous intéresser ici plus particulièrement à deux patterns d'Android : HandlerThread/Looper et AsyncTask.

Pour cela, nous allons utiliser une mini-application très simple que j'ai pré-développée, appelée Freezap. Vous pouvez dès maintenant la télécharger à ce commit Github.

Notre mini-application Freezap
Notre mini-application Freezap

Exécutez-la. Vous remarquerez qu'une barre de progression (ProgressBar) s'affiche et est jouée à l'infini. Celle-ci va nous servir d'indicateur quand nous effectuerons une tâche longue sur le Main Thread : si elle se bloque ou si l'animation bugge un peu, c'est que nous exécutons une requête trop longue sur le thread principal. D'ailleurs, appuyez sur le bouton "Execute Action In Main Thread " afin de vous en rendre compte... :-°

Sur ce bouton, j'ai fait exprès d'appeler une méthode qui met 7 secondes à s'exécuter, sur le Thread Principal (Main Thread), le Thread utilisé par défaut :

public static Long executeLongActionDuring7seconds(){

    Log.e("TAG", "Long action is starting...");
    
    Long endTime = System.currentTimeMillis() + 7000;
    while (System.currentTimeMillis() <  endTime) {
        //Loop during 7 secs hehehe...
    }
    
    Log.e("TAG", "Long action is finished !");

    return endTime;
}

Essayons maintenant de lancer cette tâche longue dans un Thread à part pour ne plus freezer notre UI... :magicien:

HandlerThread + Looper

Commençons par les termes barbares HandlerThread et Looper. Ces derniers vont nous permettre d'exécuter du code dans un Thread dédié, de manière relativement simple, sans à avoir à gérer nous-même la création et la gestion d'un Thread manuellement (si vous souhaitez voir ce que vous vous épargnez, allez voir la documentation officielle concernant la création d'un Thread sur Android... ;) ).

Représentation d'un HandlerThread
Représentation d'un HandlerThread

Pour comprendre le fonctionnement d'un HandlerThread, admirez le schéma plus haut :

  1. Nous créons un exécutable (Runnable) qui lancera une tâche longue (comme par exemple celle que j'ai créée plus haut).

  2. Cet exécutable est placé dans une file d'attente d'exécutable (Runnable Queue)

  3. Ensuite, un Looper (voyez cela comme la roue d'un moulin et l'exécutable, de l'eau) va récupérer l'exécutable en tête de liste, et l'ajouter à un Handler, dont la principale mission sera de le lancer. A savoir : un Thread ne peut contenir qu'un seul et même Looper !

  4. Ainsi de suite, tant qu'il y a des tâches (donc des runnables) à exécuter...

Tout cela sera exécuté bien sûr dans un Thread dédié, créé automatiquement par HandlerThread !

Passons maintenant à la pratique. Reprenons notre application Freezap, et créons une classe dédiée à HandlerThread dans le package Utils :

Classe Utils/MyHandlerThread.java :

public class MyHandlerThread extends HandlerThread {

    private WeakReference<ProgressBar> progressBarWeakReference;

    // 1 - Constructor
    public MyHandlerThread(String name, ProgressBar progressBar) {
        super(name);
        progressBarWeakReference = new WeakReference<>(progressBar);
    }

    // 2 - Public method that will start handler
    public void startHandler(){

        // 2.1 - Checking if progressbar is accessible, and setting it visible
        if (progressBarWeakReference.get() != null) progressBarWeakReference.get().setVisibility(View.VISIBLE);

        // 2.2 - Checking if handlerThread is already alive, else we start it.
        if (!this.isAlive()) this.start();

        // 2.3 - Creating a new Handler and setting it the looper of handlerThread
        Handler handler = new Handler(this.getLooper());

        // 2.4 - Executing a new Runnable
        handler.post(new Runnable(){
            @Override
            public void run() {
                // 2.5 - Execute our long task during 7 seconds
                Utils.executeLongActionDuring7seconds();

                // 2.6 - Update UI after task finished (In Main Thread)
                new Handler(Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        if (progressBarWeakReference.get() != null) progressBarWeakReference.get().setVisibility(View.GONE);
                    }
                });
            }
        });
    }
}

Explications : Nous avons créé ici une classe dédiée (vous vous rappelez, on tente de découper un maximum notre code !), qui héritera de HandlerThread, et dont le rôle sera d'exécuter une tâche longue dans un thread dédié.

  • Ligne 1 : Notre constructeur, qui nous permettra de créer et d'appeler cette classe depuis MainActivity. Nous lui avons ajouté ici une ProgressBar en paramètre, pour ensuite lui créer une WeakReference (afin d'éviter les MemoryLeaks, j'en parlerais dans un prochain chapitre).

  • Ligne 2 : Nous créons une méthode publique, qui nous permettra de lancer l'exécution de notre HandlerThread depuis MainActivity.

    • Ligne 2.1 : On vérifie si la ProgressBar est accessible, et nous l'affichons afin d'indiquer visuellement à l'utilisateur qu'il se passe quelque chose (en l'occurrence, l'exécution de notre tâche).

    • Ligne 2.2 : Il est possible que nous ayons déjà lancé précédemment notre HandlerThread. Pour cela, nous vérifions s'il fonctionne déjà et le lançons dans le cas échéant.

    • Ligne 2.3 : Nous créons un nouvel Handler, afin de lui affecter par la suite, un exécutable contenant notre tâche longue. Par défaut, un Handler doit être créé avec un Looper : Nous prenons ici le Looper défini automatiquement par notre HandlerThread.

    • Ligne 2.4 : Puis nous affectons à cet Handler un nouvel exécutable via la méthode  post()  .

    • Ligne 2.5 : Cet exécutable lancera notre fameuse méthode de 7 secondes...

    • Ligne 2.6 : Une fois terminé, on mettra à jour notre interface graphique en stoppant notre ProgressBar. Attention ici, vous ne POUVEZ PAS modifier un élément de votre UI en dehors du MainThread. Comme ici vous êtes dans un thread différent, il faut que vous reveniez dans le MainThread, en lançant un nouvel Handler utilisant le Looper de MainThread. 

Modifions maintenant notre MainActivity afin d'appeler cet HandlerThread quand l'utilisateur clique sur le bouton "Execute Action In Background" :

Aperçu de la classe MainActivity.java :

public class MainActivity extends AppCompatActivity {

    ...

    //FOR DATA
    // 1 - Declaring a HandlerThread
    private MyHandlerThread handlerThread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ...

        //Configure Handler Thread
        this.configureHandlerThread();
    }

    @Override
    protected void onDestroy() {
        // 3 - QUIT HANDLER THREAD (Free precious resources)
        handlerThread.quit();
        super.onDestroy();
    }

    // ------------
    // ACTIONS
    // ------------

    public void onClickButton(View v){
        
        ...
        
        switch (buttonTag){
            
            ...
            
            case 20: // CASE USER CLICKED ON BUTTON "EXECUTE ACTION IN BACKGROUND"
                this.startHandlerThread();
                break;
                
            ...
            
        }
    }

    // -----------------
    // CONFIGURATION
    // -----------------

    // 2 - Configuring the HandlerThread
    private void configureHandlerThread(){
        handlerThread = new MyHandlerThread("MyAwesomeHanderThread", this.progressBar);
    }

    // -------------------------------------------
    // BACKGROUND TASK (HandlerThread & AsyncTask)
    // -------------------------------------------

    // 4 - EXECUTE HANDLER THREAD
    private void startHandlerThread(){
        handlerThread.startHandler();
    }
}

Explications : Nous allons ici lancer notre tâche longue via le HandlerThread, au clic de l'utilisateur.

  • Ligne 1 : Nous déclarons notre HandlerThread

  • Ligne 2 : Nous configurons ce dernier et n'oublions pas de l'appeler dans notre méthode  onCreate()

  • Ligne 3 : On pense également à arrêter son exécution dans la méthode  onDestroy()  de l'activité, afin de fermer correctement le Thread et surtout éviter d'éventuelles fuites de mémoire (Memory Leaks).

  • Ligne 4 : Nous exécutons notre HandlerThread et n'oublions pas d'appeler cette méthode quand un utilisateur clique sur le bouton "Execute Action In Background".

Lancez maintenant votre application et cliquez sur le bouton "Execute Action In Background". Super ! Notre interface graphique ne freeze plus et notre tâche longue s'effectue sans soucis, dans un Thread dédié ! :soleil:

Chouette ! Mais je trouve tout cela un peu complexe tout de même... Il n'y aurait pas quelque chose de plus sympa à utiliser ?

Bien sûr ! En fait, on va en parler immédiatement grâce à l'AsyncTask.

AsyncTask

Bon, vous l'avez vu, effectuer une tâche longue dans un thread dédié en background semble un peu laborieux... C'est d'ailleurs pour cette raison que les développeurs d'Android ont décidé de faciliter ce processus en créant une classe beaucoup plus abordable à utiliser : AsyncTask !

Représentation d'une AsyncTask
Représentation d'une AsyncTask

En regardant d'un peu plus près le schéma ci-dessus, on s'aperçoit qu'une AsyncTask est composée principalement de trois méthodes :

  • onPreExecute()  : Cette méthode sera exécutée, sur MainThread, nous permettant ainsi par exemple, de mettre à jour notre UI en lançant une animation de chargement (ProgressBar).

  • doInBackground()  : Cette méthode sera en revanche exécutée dans un Thread à part, et vous vous en doutez, contiendra une tâche longue destinée à être lancée en arrière-plan.

  • onPostExecute()  : Cette méthode sera exécutée, sur MainThread, une fois que notre tâche longue soit terminée. On pourra ici arrêter l'animation de chargement par exemple (ProgressBar).

Vous avez vu, tout cela semble beaucoup plus accessible non ? :ange:

Passons à la pratique ! Nous allons exécuter cette fois-ci notre tâche longue de 7 secondes dans une AsyncTask. Créons ensemble la classe dédiée à cela, MyAsyncTask, toujours dans le package Utils :

Classe Utils/MyAsyncTask.java :

public class MyAsyncTask extends AsyncTask<Void, Void, Long> {

    // 1 - Implement listeners methods (Callback)
    public interface Listeners {
        void onPreExecute();
        void doInBackground();
        void onPostExecute(Long success);
    }

    // 2 - Declare callback
    private final WeakReference<Listeners> callback;

    // 3 - Constructor
    public MyAsyncTask(Listeners callback){
        this.callback = new WeakReference<>(callback);
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        this.callback.get().onPreExecute(); // 4 - Call the related callback method
        Log.e("TAG", "AsyncTask is started.");
    }

    @Override
    protected void onPostExecute(Long success) {
        super.onPostExecute(success);
        this.callback.get().onPostExecute(success); // 4 - Call the related callback method
        Log.e("TAG", "AsyncTask is finished.");
    }

    @Override
    protected Long doInBackground(Void... voids) {
        this.callback.get().doInBackground(); // 4 - Call the related callback method
        Log.e("TAG", "AsyncTask doing some big work...");
        return Utils.executeLongActionDuring7seconds(); // 5 - Execute our task
    }
}

Explications : Cette classe MyAsyncTask hérite de la classe mère AsyncTask, et son rôle sera d'exécuter une tâche longue dans un thread dédié.

  • Ligne 1 : Nous avons créé ici une interface contenant des méthodes de callback, que nous implémenterons ensuite dans notre activité MainActivity. Ces derniers nous permettront de pouvoir réaliser des actions spécifiques au moment où les méthodes correspondantes seront appelées dans l'AsyncTask.

  • Ligne 2 : Nous déclarons ici le callback en question, que notre activité implémentera. Nous le définissons en WeakReference afin d'éviter là aussi des problèmes de fuite de mémoire.

  • Ligne 3 : Le constructeur qui sera appelé dans notre activité pour créer cet objet AsyncTask.

  • Lignes 4 : Nous appelons à différents endroits le callback correspondant, afin de pouvoir effectuer en même temps dans notre activité, des actions complémentaires.

  • Ligne 5 : Et bien sûr pour terminer, dans notre  doInBackground() , nous exécutons notre longue tâche de 7 secondes.

Appelons maintenant cette AsyncTask dans notre activité.

Extrait de MainActivity.java : 

public class MainActivity extends AppCompatActivity 
                          implements MyAsyncTask.Listeners {
    // Ligne 1 : Implements the MyAsyncTask callback methods
    ...

    // ------------
    // ACTIONS
    // ------------

    public void onClickButton(View v){
        ...
        switch (buttonTag){
            ...
            case 60: // CASE USER CLICKED ON BUTTON "EXECUTE ASYNCTASK"
                this.startAsyncTask();
                break;
            ...
        }
    }

    // -------------------------------------------
    // BACKGROUND TASK (HandlerThread & AsyncTask)
    // -------------------------------------------

    ...

    // ------

    // 3 - We create and start our AsyncTask
    private void startAsyncTask() {
        new MyAsyncTask(this).execute();
    }

    // 2 - Override methods of callback
    @Override
    public void onPreExecute() {
        // 2.1 - We update our UI before task (starting ProgressBar)
        this.updateUIBeforeTask(); 
    }

    @Override
    public void doInBackground() { }

    @Override
    public void onPostExecute(Long taskEnd) {
        // 2.2 - We update our UI before task (stopping ProgressBar)
        this.updateUIAfterTask(taskEnd);
    }

    // -----------------
    // UPDATE UI
    // -----------------

    public void updateUIBeforeTask(){
        progressBar.setVisibility(View.VISIBLE);
    }

    public void updateUIAfterTask(Long taskEnd){
        progressBar.setVisibility(View.GONE);
        Toast.makeText(this, "Task is finally finished at : "+taskEnd+" !", Toast.LENGTH_SHORT).show();
    }
}

Explications : Nous avons ici implémenté l'interface  MyAsyncTask.Listeners  à notre activité et ensuite lancé notre AsyncTask :

  • Ligne 1 : On implémente l'interface de CallBack  MyAsyncTask.Listeners

  • Ligne 2 : Comme nous avons implémenté une interface, il faut maintenant re-déclarer ses méthodes. Celles-ci seront appelées au bon moment, nous permettant de créer des actions complémentaires !

    • Ligne 2.1 : Nous mettons à jour notre interface graphique en affichant la ProgressBar (via les méthodes créées juste en dessous).

    • Ligne 2.2 : Nous mettons à jour notre interface graphique en masquant la ProgressBar (via les méthodes créées juste en dessous).

  • Ligne 3 : Nous créons une méthode qui va créer et exécuter une nouvelle tâche MyAsyncTask. N'oublions pas d'appeler celle-ci dans la méthode  onClickButton()  afin qu'elle soit appelée quand un utilisateur clique sur le bouton "Execute AsyncTask".

Lancez maintenant votre application et appuyez sur le bouton "Execute AsyncTask".

Félicitations ! Vous venez d'ajouter une tâche longue dans une AsyncTask en déportant son exécution en arrière-plan, dans un Thread à part. ;)

Appropriez-vous ce code et jouez un peu avec ! #PracticeMakesPerfect

Example of certificate of achievement
Example of certificate of achievement