Tâches d’arrière-plan

Objectif

L’objectif est de programmer des tâches d’arrière-plan qui interagissent avec l’IHM (thread UI).

Ici, nous allons tout simplement afficher la date et l’heure en proposant plusieurs solutions à base de Timer/TimerTask, de threads ou AsyncTask.

Les threads

Toute application Android est composée d’une multitude de threads.

Le thread UI (User Interface) est le fil d’exécution d’une activité. Il est responsable de l’affichage et des interactions avec l’utilisateur et surtout c’est le seul thread qui doit modifier l’affichage.

Remarque : on ne peut pas effectuer des traitements consommateurs en temps dans le thread UI car celui-ci se “figerait” et ne répondrait plus aux actions de l’utilisateur. D’autre part si une activité réagit en plus de cinq secondes, elle sera tuée par l’ActivityManager qui la considérera comme morte.

Il faudra donc créer et exécuter des threads d’arrière-plan pour les traitements lourds.

En Java, il y a plusieurs façons de créer et exécuter un thread. Le principe de base revient à dériver (extends) une classe Thread ou à implémenter (implements) l’interface Runnable et à écrire le code du thread dans la méthode run(). Ensuite, on appelera la méthode start() pour démarrer le thread et la méthode stop() pour l’arrêter.

  • l’interface Runnable :
// Méthode 1 : écriture d'une classe
// Une classe "Thread"
public class UnThread implements Runnable
{
    // Le code du thread
    public void run()
    {
    }
}

// Instanciation
UnThread unThread = new UnThread();
Thread leThread = new Thread(unThread);

// Démarrage
leThread.start(); // -> appel run()

On utilise plus souvent les classes anonymes (on ne lui donne pas de nom et son implémentation suit immédiatement sa déclaration) :

// Méthode 2 :
Thread leThread = new Thread(new Runnable() {
    // Le code du thread
    public void run() {        
    }
});

// Démarrage
leThread.start(); // -> appel run()
  • la classe Thread :
class UnThread extends Thread {
    // Le code du thread
    public void run() {

    }
}

// Instanciation
UnThread leThread = new UnThread();

// Démarrage
leThread.start(); // -> appel run()

Documentation Android : Thread

Timer

La classe Timer permet de programmer des tâches pour une exécution dans un thread d’arrière-plan. Les tâches peuvent être programmées pour une exécution unique (single shot) ou pour une exécution répétée à intervalles réguliers. La classe Timer fournit donc un minuteur ou temporisateur.

La programmation du timer se fait par la méthode schedule(TimerTask task, long delay, long period) en lui indiquant en paramétres :

  • task : la tâche à exécuter qui est un objet de type TimerTask
  • delay : le délai en ms après lequel la tâche sera exécutée
  • period : la période en ms de répétition de l’exécution de la tâche

Si on ne précise pas le paramétre period, la tâche sera programmée pour un exécution unique.

La classe Timer fournit aussi la méthode cancel() pour terminer le timer.

Documentation Android : Timer

TimerTask

La classe TimerTask permet de créer une tâche qui peut être planifiée pour une exécution unique (single shot) ou répétée par un Timer. Le code de la tâche s’implémentera par la redéfinition de sa méthode run().

La classe TimerTask fournit les méthodes cancel() pour annuler la tâche et scheduledExecutionTime() qui retourne le temps d’exécution.

Documentation Android : TimerTask

AsyncTask

Depuis la version 1.5 d’Android, la classe AsyncTask permet une nouvelle façon d’effectuer des tâches en arrière-plan.

Elle s’exécutera dans deux threads distincts : le thread UI et le thread de traitement.

Les méthodes à redéfinir de la classe AsynchTask sont :

  • doInBackground() est la méthode qui s’exécute dans un autre thread. Seule cette méthode est exécutée dans un thread à part, les autres méthodes s’exécutent dans le thread UI.

  • onPreExecute() est appelée par le thread UI avant l’appel à doInBackground().

  • onPostExecute() est appelée lorsque la méthode doInBackground() est terminée.

  • onProgressUpdate() est appelée par la méthode publishProgress() à l’intérieur de la méthode doInBackground().

L’idée est que si on fait un traitement lourd qui n’a besoin que de donner son avancement et sa fin (typiquement charger des données), le mieux est l’utilisation de l’AsyncTask.

Documentation Android : AsyncTask

Solution n°1 : runOnUiThread()

Si la tâche doit interagir avec l’IHM, on pourra utiliser runOnUiThread() pour exécuter le code dans le thread UI.

public class MainActivity extends Activity 
{
    Button btnStart, btnCancel;
    TextView textView;
 
    Timer timer;
    TimerTask myTimerTask;

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

        btnStart = (Button)findViewById(R.id.start);
        btnCancel = (Button)findViewById(R.id.cancel);
        textView = (TextView)findViewById(R.id.counter);
        
        btnStart.setOnClickListener(new OnClickListener(){
            public void onClick(View arg0) {
                if(timer != null){
                    timer.cancel();
                }

                timer = new Timer();
                
                doTask();
            }
        });
  
        btnCancel.setOnClickListener(new OnClickListener(){
            public void onClick(View v) {
                if (timer!=null){
                    timer.cancel();
                    timer = null;
                }
            }
        });
    }

    public void doTask() {
        myTimerTask = new TimerTask() {
            public void run() {
                Calendar calendar = Calendar.getInstance();
                SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd:MMMM:yyyy HH:mm:ss a");
                final String strDate = simpleDateFormat.format(calendar.getTime());

                runOnUiThread(new Runnable() {
                    public void run() {
                        textView.setText(strDate);
                    }
                });
            }
        };

        timer.schedule(myTimerTask, 1000, 1000);
    }
}

Solution n°2 : Handler et les objets Runnable

Si la tâche doit interagir avec l’IHM, on pourra utiliser la classe Handler pour communiquer avec celle-ci.

L’Handler est associé à l’activité qui le déclare et travaille au sein du thread UI. Le handler se chargera de mettre à jour l’IHM. Le thread peut communiquer avec cet handler au moyen de messages ou des objets Runnable.

public class MainActivity extends Activity 
{
    //...
    final Handler handler = new Handler();

    //...
    
    public void doTask(){ 
        myTimerTask = new TimerTask() {
                public void run() {
                        handler.post(new Runnable() {
                                public void run() {
                                    Calendar calendar = Calendar.getInstance();
                                    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd:MMMM:yyyy HH:mm:ss a");
                                    final String strDate = simpleDateFormat.format(calendar.getTime());
                                    textView.setText(strDate);
                                }
                        });
                }
        };

        timer.schedule(myTimerTask, 1000, 1000);
    }
}

Solution n°3 : Handler et les messages

Si la tâche doit interagir avec l’IHM, on pourra utiliser la classe Handler pour communiquer avec celle-ci.

L’Handler est associé à l’activité qui le déclare et travaille au sein du thread UI. Le handler se chargera de mettre à jour l’IHM. Le thread peut communiquer avec cet handler au moyen de messages ou des objets Runnable.

Pour cela :

  • le thread récupère un objet Message du pool du handler par la méthode Message.obtain(). Il peut ensuite ajouter un code (what) et d’autre(s) paramètre(s) (arg1, arg2 ou obj) ;

  • le thread envoie le message au handler en utilisant la méthode sendMessage() qui envoie le message et le place à la fin de la queue,

  • le handler doit surcharger sa méthode handleMessage() pour traiter les messages qui lui sont envoyés.

On modifie l’activité :

public class MainActivity extends Activity 
{
    //...
    
    final private Handler handler = new Handler()
    {
        public void handleMessage(Message msg)
        {
            super.handleMessage(msg);
            if(msg.what == 1)
            {
                // Modifie l'IHM
                textView.setText((String)msg.obj);
            }
            
            ...
            
        }
    };

    //...

    public void doTask() {
        myTimerTask = new TimerTask() {
            public void run() {
                Calendar calendar = Calendar.getInstance();
                SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd:MMMM:yyyy HH:mm:ss a");
                final String strDate = simpleDateFormat.format(calendar.getTime());

                Message msg = Message.obtain();
                msg.what = 1;
                msg.obj = strDate;
                handler.sendMessage(msg);
            }
        };

        timer.schedule(myTimerTask, 1000, 1000);
    }
}

Solution n°4 : un thread d’arrière plan

Si la tâche doit interagir avec l’IHM, on pourra utiliser un simple thread d’arrière plan et utiliser la classe Handler comme vu dans la solution n°3.

Ici on n’utile plus le timer mais un simple sleep() en millisecondes fourni par la classe Thread.

On modifie l’activité :

public class MainActivity extends Activity 
{
    //...

    public void doTask() {
        Thread leThread = new Thread(new Runnable() {
            public void run() {
                String strDate;
                while(true)
                {
                    try
                    {
                        Calendar calendar = Calendar.getInstance();
                        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss a");
                        strDate = simpleDateFormat.format(calendar.getTime());
                        
                        Message msg = Message.obtain();
                        msg.what = 1;
                        msg.obj = strDate;
                        handler.sendMessage(msg);

                        Thread.sleep(1000);
                    }
                    catch (Exception e)
                    {
                        e.getLocalizedMessage();
                    }
                }
            }
        });

        // Démarrage
        leThread.start(); // -> appel run()
    }
}

Solution n°5 : AsyncTask

La classe AsyncTask est générique et attend trois paramètres AsyncTask<Type1, Type2, Type3> où :

  • Type1 est nécessaire au traitement (doInBackground())
  • Type2 est passé à sa tâche pour indiquer sa progression (onProgressUpdate())
  • Type3 est passé à sa tâche lorsque elle est finie (onPostExecute()) et qui est donc la valeur de retour de doInBackground()

Attention : l’exemple d’afficher la date et l’heure n’est pas en soi adapté à l’utilisation d’un AsyncTask !

public class MainActivity extends Activity 
{
    //...
    
    public void doTask() {
        // Lancement de l'AsyncTask
        new MyAsyncTask().execute();
    }

    class MyAsyncTask extends AsyncTask<Void, String, Boolean>
    {
        protected Boolean doInBackground(Void... unused) {
            try {
                while (true) {
                    Calendar calendar = Calendar.getInstance();
                    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss a");
                    String strDate = simpleDateFormat.format(calendar.getTime());

                    Thread.sleep(1000);

                    publishProgress(strDate);
                }
            }
            catch (InterruptedException t)
            {
                return false;
            }
        }

        protected void onProgressUpdate(String... msg)
        {
            textView.setText(msg[0]);
        }

        protected void onPostExecute(Boolean etat)
        {
            if(etat)
                Toast.makeText(getApplicationContext(), "Terminé avec succes", Toast.LENGTH_SHORT).show();
        }
    }
}