T#019 – Scaricare immagini da visualizzare in una ListView (Parsing JSON – parte 2)

Nel precedente tutorial di programmazione Android abbiamo visto come effettuare una chiamata json verso il server di Twitter per mostrare un elenco di tweet corrispondenti a una ricerca. La singola cella della ListView contenuta nell’activity conteneva solo il testo del tweet, vediamo in questo post come aggiungere anche l’immagine dell’autore del tweet visualizzato.

Per prima cosa creiamo una nuova classe che conterrà i dati di un singolo tweet, i campi della classe saranno l’id dell’utente, l’url dell’immagine del profilo e il testo del tweet:

1
2
3
4
5
6
7
8
9
10
public class Tweet
{
	private long userId;
 
	private String profileImageUrl;
 
	private String text;
 
	//getter e setter...
}

AsyncTask con parsing dei tweet

Modifichiamo anche l’AsyncTask in modo che gestisca oggetti di tipo Tweet e non semplicemente stringhe. In pratica cambiamo il secondo parametro generic e lo facciamo diventare AsyncTask<Void, Tweet, Void>. Il metodo che esegue il download e il parsing diventa:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected Void doInBackground(Void... params)
{
	try
	{
		JSONObject obj = getJSONObject("http://search.twitter.com/search.json?q=devapp");
		JSONArray jsonArray = obj.getJSONArray("results");
		for (int i = 0; i < jsonArray.length(); i++)
		{
			JSONObject jsonObject = jsonArray.getJSONObject(i);
			Tweet t = new Tweet();
			t.setUserId(jsonObject.getLong("from_user_id"));
			t.setProfileImageUrl(jsonObject.getString("profile_image_url"));
			t.setText(jsonObject.getString("text"));
			publishProgress(t);
		}
	}
	catch (IOException ignored)
	{
	}
	catch (JSONException ignored)
	{
	}
	return null;
}

Il metodo che si occupa di aggiornare l’interfaccia grafica aggiungerà gli oggetti passati (questa volta di tipo Tweet) nell’adapter:

1
2
3
4
5
6
7
protected void onProgressUpdate(Tweet... values)
{
	for (Tweet tweet : values)
	{
		adapter.add(tweet);
	}
}

Adapter con download delle immagini

Passiamo ad analizzare l’adapter che si occupa di popolare le singole righe della ListView. Per prima cosa vediamo il layout di una riga, un LinearLayout che contiene una immagine e un testo:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="fill_parent" android:layout_height="fill_parent"
	android:orientation="horizontal" android:gravity="center_vertical">
	<ImageView android:layout_width="48dip"
		android:layout_height="48dip" android:layout_margin="2dip" android:id="@+id/image" />
	<TextView android:layout_width="wrap_content"
		android:textSize="16sp" android:paddingLeft="5dip"
		android:paddingRight="5dip" android:layout_height="wrap_content"
		android:id="@+id/title" />
</LinearLayout>

L’immagine dell’utente deve essere scaricata da internet a partire da un url, il codice del metodo che effettua il download e crea la bitmap è il seguente (in pratica fa tutto la classe BitmapFactory):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private Bitmap downloadBitmap(String fileUrl)
{
	InputStream is = null;
	try
	{
		HttpURLConnection conn = (HttpURLConnection) new URL(fileUrl).openConnection();
		conn.connect();
		is = conn.getInputStream();
 
		return BitmapFactory.decodeStream(is);
	}
	catch (MalformedURLException e)
	{
	}
	catch (IOException e)
	{
	}
	finally
	{
		if (is != null)
			try
			{
				is.close();
			}
			catch (IOException e)
			{
			}
	}
	return null;
}

Ora che abbiamo tutti i pezzi non ci resta che metterli insieme e scrivere il metodo che si occupa di popolare la View di una singola riga. La cosa più semplice è mettere il download dell’immagine direttamente nel metodo getView dell’adapter in questo modo:

1
2
3
4
5
6
7
8
public View getView(int position, View convertView, ViewGroup parent)
{
	convertView = super.getView(position, convertView, parent);
	Tweet t = getItem(position);
	ImageView image = (ImageView) convertView.findViewById(R.id.image);
	image.setImageBitmap(downloadBitmap(t.getProfileImageUrl()));
	return convertView;
}

Ma siamo certi che questa sia la soluzione giusta al problema? Provate a eseguire l’applicazione su un device (o sul simulatore) e a scorrere la lista. Se avete problemi potete scaricare i sorgenti completi del progetto. Il risultato finale dovrebbe essere simile a questo:



Usando questa soluzione lo scorrimento della lista è fluido? La risposta, purtroppo, è no! 🙁

Il motivo è abbastanza facile da trovare, ogni volta che viene popolata una cella viene scaricata una immagine da internet. Con il codice che abbiamo scritto in questo post il download è sincrono e finchè non è finito stiamo ad aspettare! Per questo ogni volta che scorriamo la lista e compare una nuova riga si blocca lo scorrimento fintanto che il download non è terminato.

Download asincrono

Molti di voi avranno già pensato alla soluzione, eseguiamo il download in un thread in background usando per esempio un AsyncTask. Vi lascio 5 minuti per implementare questa soluzione da voi, non di più visto che ormai la classe AsyncTask non ha più segreti!

Avete finito?

Che ne dite, non è più fluido lo scrolling della lista ora? Non dovendo aspettare la fine del download il metodo getView (che viene eseguito nel thread principale dell’applicazione) è sicuramente più veloce.

Ma purtroppo non è finita qui, abbiamo un altro problema…

Vi ricordate che le celle di una ListView vengono riusate? In pratica se sono visibili 7 celle alla volta, ma la nostra lista contiene 1000 elementi, non verranno create 1000 celle ma solo 7 e al momento dello scroll le celle che diventano nascoste vengono riusate per le nuove celle divenute visibili. Per questo motivo eseguendo il download in un thread separato nessuno ci assicura che alla fine del download la cella per cui avevamo scaricato l’immagine sia ancora visibile. O meglio, molto probabilmente la cella sarà visibile ma potrebbe contenere i dati di un altro elemento. Questo succede se durante il download dell’immagine la cella diventa invisibile e quindi viene riusata per un altro elemento.

Purtroppo gestire questa cosa non è proprio semplice, per fortuna è una cosa abbastanza frequente da fare e quindi ci sono delle librerie che gestiscono tutto. Nel prossimo post vedremo proprio come utilizzare una di queste librerie nel nostro progetto.

By Fabio Collini

Da agosto 2009 sono un freelance android developer, ho rilasciato due applicazioni nell'Android Market: Apps Organizer e Folder Organizer. Presso OmniaGroup ricopro il ruolo di Tech Leader nell'ambito di un progetto di rich internet application che utilizza JSF, JPA(EclipseLink) ed EJB3.

2 comments

  1. Interessante ma non riesco a raggiungere la prima parte del tutorial!

Comments are closed.