Implémenter correctement un AsyncTask pour alimenter un ListView

home-bugdroidCet article a pour but de vous proposer et de vous expliquer en détail le template que j’utilise pour utiliser les AsyncTask dans mes applications. Cela permet d’effectuer des travaux en tâche de fond sans bloquer l’utilisateur. On s’en sert notamment pour :

  • Effectuer de gros calculs
  • Appeler des services Web sur Internet

Ce template permet de laisser une tâche s’exécuter pendant que l’utilisateur patiente ou fait autre chose (appel téléphonique, lecture de ses mails, SMS, …).

La plupart des articles/tutoriaux que j’ai pu lire sur le sujet liaient fortement l’AsyncTask à l’Activity. Hors, cela pose un problème lorsque l’Activity est détruite ( lors d’une rotation de l’écran, réception d’un appel, …). Il faut souvent recommencer la tâche demandée à zéro car l’AsyncTask est détruite avec l’Activity 👿. C’est pas top d’autant plus qu’on souhaite charger des données en tâche de fond :lol:. J’ai pu en lire d’autre qui proposaient des solutions relativement complexes à mettre en place (pour pas dire des usines à gaz !).

L’idée que je présente ici est de lier l’Asynctask non pas à l’Activity mais à la classe Application présente dans toute les applications Android et qui ne sera pas détruite 😀. Une fois les données chargée l’Activity (ici une ListActivity) sera alertée pour se mettre à jour. J’ai essayé à ce que ce soit le plus simple et le plus clair possible.

La première chose à faire est donc de créer une classe héritant d’Application et de la déclarer dans le manifest :

FruitListApplication.java

package com.blogdebenoit.fruitlist;

import android.app.Application;

public class FruitListApplication extends Application {

	/**
	 * Static property to easily access to application instance
	 */
	public static FruitListApplication application = null;

	/**
	 * Flag specifying if task is loading data or not
	 */
	public boolean loading = false;

	/**
	 * Fruit List
	 */
	public String[] fruitList = new String[]{};

	@Override
	public void onCreate() {
		super.onCreate();

		application = this;
	}

}

AndroidManifest.xml


 <application
 android:name="com.blogdebenoit.fruitlist.FruitListApplication"
 android:allowBackup="true"
 android:icon="@drawable/ic_launcher"
 android:label="@string/app_name"
 android:theme="@style/AppTheme" >
...
</application>

Cette classe Application dispose d’un flag loading qui permettra de connaître à tout moment si les données dont ont a besoin sont en cours de chargement ou non.

Ensuite il nous faut bien sûr un AsyncTask (c’est le sujet de cette article 🙂 ). Celle-ci simule une tâche longue en faisant simplement une pause de quelques secondes avant de renvoyer une liste de données. Bien sûr à la place de cette pause il faudra coder l’appel à un web service par exemple. Elle est incluse et instanciée directement dans la méthode de mise à jour mais on peut très bien en faire une classe dans un fichier à part :

	@Override
	public void onCreate() {
		super.onCreate();

		application = this;

		updateFruitList();
	}

	/**
	 * Update fruit list by calling a web service
	 */
	public void updateFruitList() {
		loading = true;

		taskStateChanged(true);

		// Task retrieving a fruit list
		AsyncTask<Void, Void, String[]> task = new AsyncTask<Void, Void, String[]>() {

			@Override
			protected String[] doInBackground(Void... params) {
				// Wait for some seconds to simulate a call to a very far web
				// service !
				try {
					Thread.sleep(5000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				return new String[] { "Banana", "Pear", "Blueberry", "Fig",
						"Lemon", "Orange", "Peach" };
			}

			protected void onPostExecute(String[] result) {
				super.onPostExecute(result);

				fruitList = result;
				taskStateChanged(false);
			}

			@Override
			protected void onCancelled() {
				super.onCancelled();

				fruitList = new String[]{};
				taskStateChanged(false);
			}
		};
		task.execute();
	}

	/**
	 * Update task state and send a broadcast message
	 *
	 * @param loading
	 */
	private void taskStateChanged(boolean loading) {
		// update flag
		this.loading = loading;

		Intent intent = new Intent("fruit.task");
		intent.putExtra("loading", loading);
		sendBroadcast(intent);
	}

Cette méthode qui met la liste de fruit à jour est publique car elle sera appelée si l’utilisateur réclame une mise à jour des données par exemple.

Il faut aussi noter que cette classe Application permet facilement de connaître l’état de la tâche (en cours / finie) à l’aide du flag loading ou à l’aide d’un Broadcast. Cela sera utile si l’utilisateur patiente (c’est rare mais ça arrive 🙂 ) sur sa liste où les données chargées seront présentées.

On dispose donc d’une classe Application qui permet :

  • de charger des données
  • de prévenir quand le chargement est fini
  • de les rendre disponibles

Occupons nous maintenant de la présentation de ces données dans une ListActivity. Le rôle de celle-ci est de faire patienter l’utilisateur pendant que les données se chargent. Il est possible pour cela d’utiliser un ProgressDialog mais le cycle de vie des AlertDialog étant compliquée 👿 (voir AsynsTask & ProgressDialog), j’ai préféré utiliser une simple ProgressBar qui sera affiché au centre de l’écran. Voici un exemple de ListActivity :

FruitListActivity.java

package com.blogdebenoit.fruitlist;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;

public class FruitListActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fruit_list);

        loadedLayout = (LinearLayout) findViewById(R.id.loaded);
        loadingLayout = (FrameLayout) findViewById(R.id.loading);
    }

}

activity_fruit_list.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <LinearLayout
        android:id="@+id/loaded"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:visibility="gone" >

        <ListView
            android:id="@android:id/list"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:drawSelectorOnTop="false" />

        <TextView
            android:id="@android:id/empty"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="center"
            android:text="@string/no_data" />
    </LinearLayout>

    <FrameLayout
        android:id="@+id/loading"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="visible" >

        <ProgressBar
            android:id="@android:id/progress"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="@string/no_data" />
    </FrameLayout>

</LinearLayout>

Ce layout est composé de 2 parties :

  • Un ListView avec un TextView. Ce dernier sera affiché s’il n’y a aucune données dans la liste (classique jusque là)
  • Un ProgressBar qui sera destiné à faire patienter l’utilisateur pendant que l’AsyncTask fera son travail

Comme toute ListActivity nous avons besoin d’un Adapter qui convertira les données de FruitListApplication.fruitList en vue à afficher dans le ListeView :

private FruitAdapter adapter;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_fruit_list);

		loadedLayout = (LinearLayout) findViewById(R.id.loaded);
		loadingLayout = (FrameLayout) findViewById(R.id.loading);

		adapter = new FruitAdapter();
		setListAdapter(adapter);
	}

	/**
	 * A simple adapter providing views from a fruit list
	 */
	private class FruitAdapter extends BaseAdapter {

		/* private view holder class */
		private class ViewHolder {
			TextView text;
		}

		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			ViewHolder holder = null;

			LayoutInflater mInflater = (LayoutInflater) getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
			if (convertView == null) {
				convertView = mInflater.inflate(
						android.R.layout.simple_list_item_1, null);
				holder = new ViewHolder();
				holder.text = (TextView) convertView
						.findViewById(android.R.id.text1);
				convertView.setTag(holder);
			} else {
				holder = (ViewHolder) convertView.getTag();
			}

			String str = (String) getItem(position);

			holder.text.setText(str);

			return convertView;
		}

		@Override
		public int getCount() {
			return FruitListApplication.application.fruitList.length;
		}

		@Override
		public Object getItem(int position) {
			return FruitListApplication.application.fruitList[position];
		}

		@Override
		public long getItemId(int position) {
			return position;
		}
	}

Ce BaseAdapter est relativement classique si ce n’est qu’il va chercher ses données directement dans la classe Application.

Ensuite, il faut disposer d’une fonction qui permet d’afficher le ProgressBar ou le ListView suivant les cas. Cette méthode sera invoqués lorsque l’Activity sera affichée à l’utilisateur, c’est-à-dire dans la méthode onResume :

@Override
	protected void onResume() {
		super.onResume();

		updateViews(FruitListApplication.application.loading);
	}

	/**
	 * Change views visibility
	 *
	 * @param loading specify if task is loading or not
	 */
	private void updateViews(boolean loading) {
		adapter.notifyDataSetChanged();
		if (loading) {
			loadedLayout.setVisibility(View.GONE);
			loadingLayout.setVisibility(View.VISIBLE);
		} else {
			loadedLayout.setVisibility(View.VISIBLE);
			loadingLayout.setVisibility(View.GONE);
		}
	}

Enfin, pour être averti que la tâche de chargement des données alors que l’on est déjà sur le ListActivity, nous allons utiliser un BroadcastReceiver qui fera la même chose que la méthode onResume :

@Override
	protected void onResume() {
		super.onResume();

		// Unregister when activity become visible
		registerReceiver(receiver, new IntentFilter("fruit.task"));

		// Update view when activity become visible
		updateViews(WebServiceApplication.application.loading);
	}

	@Override
	protected void onPause() {
		super.onPause();

		// Unregister when activity is no more visible
		unregisterReceiver(receiver);
	}

	/**
	 * This broadcast receiver will be called when task state change
	 */
	public BroadcastReceiver receiver = new BroadcastReceiver() {

		// @Override
		public void onReceive(Context context, Intent intent) {
			// Task state has changed
			updateViews (intent.getBooleanExtra("loading", false));
		}

	};

On n’enregistre le BroadcastReceiver que lorsque le ListActivity est visible. Pas besoin de mettre à jour les vues lorsque l’utilisateur fait autre chose 🙂 On l’enregistre dans le onResume et on le désenregistre dans le onPause.

Maintenant il ne reste plus qu’à exécuter l’application pour  voir une liste de fruits s’afficher lorsque le chargement se fini. Ce chargement s’effectuera même si l’Activty est cachée par une autre application ou détruite par le système Android lorsque l’on tourne l’écran par exemple.

J’espère que cette application est relativement claire à comprendre et que cela vous aidera dans le développement de vos prochaines application ;). Si vous avez des remarques ou des questions sur ce code n’hésitez pas à poster un commentaire :).

Voici pour finir le projet complet reprenant le code précédemment exposé (cliquer sur Fichier puis Télécharger) :

FruitList.zip

J’ai aussi mis cette application sur le Play Store si vous souhaitez la tester rapidement :

FruitList

Publicités

Étiquettes : , , , , , , , , , , , , , , , ,

Trackbacks / Pingbacks

  1. AsynsTask & ProgressDialog « Benoît - 26 janvier 2013

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :