T#012 – Popolare ListView con elementi prelevati da un database SQLite nelle applicazioni Android

In questo tutorial di programmazione Android concludiamo l’esempio dedicato alle ListView con elementi presi da un database SQLite. Nei precedenti post abbiamo visto come creare e aggiornare un database, oggi vedremo come prelevare i dati da questo database e come mostrarli in una ListView all’interno della nostra applicazione Android.

Nel tutorial in cui abbiamo visto le ListView abbiamo introdotto anche il concetto di Adapter: una ListView ha sempre il riferimento a un Adapter che si occupa di recuperare i dati e di popolare le celle della lista.

Utilizzo di CursorAdapter per popolare la ListView

Nel caso di ListView che prendono i dati da un database la classe da usare come adapter è CursorAdapter, questa classe si occupa di popolare l’interfaccia grafica partendo da un oggetto Cursor.
Nel nostro esempio il Cursor che ritorna le province da visualizzare viene creato a partire da una query sql che esegue una join fra due tabelle:

1
2
3
4
5
6
public Cursor getAllProvince()
{
	String query = "select p._id, p.codice, p.nome, r.nome nomeRegione from province p "
			+ "inner join regioni r on p.id_regione = r._id order by p.codice";
	return getReadableDatabase().rawQuery(query, null);
}

La classe CursorAdapter è una classe astratta, se vogliamo usarla dobbiamo riscrivere i due metodi definiti ma non implementati:

  • newView: crea una nuova View corrispondente a una riga della ListView
  • bindView: popola una riga in base ai dati ottenuti dal cursore

I due metodi sono lasciati separati in quanto le View usate in una ListView sono “riciclate” automaticamente da Android nel caso di uno scroll: una stessa View creata grazie al metodo newView viene popolata più volte nel metodo bindView.

Passiamo alla pratica, creiamo una classe che estende CursorAdapter e che popoli un layout standard di Android composto da due TextView:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ProvinceCursorAdapter extends CursorAdapter
{
	public ProvinceCursorAdapter(Context context, Cursor c)
	{
		super(context, c);
	}
 
	@Override
	public View newView(Context context, Cursor cursor, ViewGroup parent)
	{
		return LayoutInflater.from(context).inflate(
			android.R.layout.simple_list_item_2, null);
	}
 
	@Override
	public void bindView(View view, Context context, Cursor cursor)
	{
		((TextView) view.findViewById(android.R.id.text1)).setText(
			cursor.getString(cursor.getColumnIndex(ProvinciaTable.NOME)));
		((TextView) view.findViewById(android.R.id.text2)).setText(
			cursor.getString(cursor.getColumnIndex(ProvinciaTable.CODICE)));
	}
}

Nel metodo bindView popoliamo la view mostrando il nome e il codice della provincia prendendo i dati dalle corrispondenti colonne del Cursor.

L’Activity che usa questo adapter estende ListActivity e, all’interno del metodo onCreate, esegue la query e associa l’adapter alla ListView:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CursorAdapterActivity extends ListActivity
{
	private DatabaseHelper databaseHelper;
 
	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		databaseHelper = new DatabaseHelper(this);
		Cursor c = databaseHelper.getAllProvince();
		startManagingCursor(c);
		setListAdapter(new ProvinceCursorAdapter(this, c));
	}
 
	@Override
	protected void onDestroy()
	{
		super.onDestroy();
		databaseHelper.close();
	}
}

Nel metodo onDestroy viene chiusa la connessione al database. Il metodo startManagingCursor associa il cursore all’Activity, in pratica chiamando questo metodo l’Activity si occupa di aprire e chiudere il cursore nel corso del proprio ciclo di vita.

Il risultato finale visto sull’emulatore è il seguente:



La classe SimpleCursorAdapter

L’esempio appena visto è molto comune in Android, per scriverlo più agevolmente è stata creata la classe SimpleCursorAdapter che estende CursorAdapter. Usando SimpleCursorAdapter è possibile creare un adapter che, partendo da un cursore, popola una vista in base a un mapping fra un insieme di colonne contenute nel cursore e un insieme di id di elementi della vista. L’esempio visto prima può essere riscritto in modo più semplice usando un SimpleCursorAdapter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void onCreate(Bundle savedInstanceState)
{
	super.onCreate(savedInstanceState);
	databaseHelper = new DatabaseHelper(this);
	Cursor c = databaseHelper.getAllProvince();
	startManagingCursor(c);
	setListAdapter(new SimpleCursorAdapter(
		this, 
		android.R.layout.simple_list_item_2, 
		c,
		new String[] { ProvinciaTable.NOME, ProvinciaTable.CODICE }, 
		new int[] { android.R.id.text1, android.R.id.text2 }
	));
}

ListView con immagini

Complichiamo un po’ l’esempio, come si può procedere se vogliamo aggiungere anche un’immagine corrispondente alla regione in una cella della nostra lista?
Per semplicità aggiungiamo una immagine per ogni regione nella directory assets del progetto dentro Eclipse. La directory assets contiene risorse che saranno distribuite nella nostra applicazione (andrà a finire dentro il file apk) ma che non sono gestite tramite la classe autogenerata R. Questa directory viene spesso utilizzata per contenere file multimediali (per esempio video o suoni) o altri file non gestiti tramite il framework delle risorse di Android.

Per prima cosa creiamo un metodo che dato il nome di un’immagine carica la corrispondente Bitmap dalla directory assets usando un InputStream:

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
private Bitmap readBitmap(String nomeImmagine)
{
	InputStream is = null;
	try
	{
		is = context.getAssets().open(nomeImmagine);
		return BitmapFactory.decodeStream(is);
	}
	catch (IOException e)
	{
		return null;
	}
	finally
	{
		if (is != null)
		{
			try
			{
				is.close();
			}
			catch (IOException ignored)
			{
			}
		}
	}
}

L’adapter che sfrutta questo metodo estende SimpleCursorAdapter riscrivendo il metodo bindView. In questo metodo viene richiamato lo stesso metodo della classe base (chiamata super.bindView) e viene eseguito il popolamento dell’ImageView usando il metodo setImageBitmap:

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
public class ProvinceSimpleCursorAdapter extends SimpleCursorAdapter
{
	private Context context;
 
	public ProvinceSimpleCursorAdapter(Context context, Cursor c)
	{
		super(context, R.layout.row, c, 
			new String[] { ProvinciaTable.NOME, ProvinciaTable.CODICE, "nomeRegione" }, 
			new int[] { R.id.provincia, R.id.codice, R.id.regione });
		this.context = context;
	}
 
	@Override
	public void bindView(View view, Context context, Cursor cursor)
	{
		super.bindView(view, context, cursor);
		ImageView logo = (ImageView) view.findViewById(R.id.logo);
		String nomeRegione = cursor.getString(cursor.getColumnIndex(
			"nomeRegione"));
		String nomeImmagine = nomeRegione.toLowerCase().replace(' ', '_').
			replace('\'', '_') + ".png";
		Bitmap bitmap = readBitmap(nomeImmagine);
		logo.setImageBitmap(bitmap);
	}
}

Il layout usato contiene tre TextView e una ImageView organizzati all’interno di un RelativeLayout:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical" android:layout_width="fill_parent"
	android:layout_height="fill_parent">
	<ImageView android:layout_width="24dip"
		android:layout_margin="5dip" android:layout_height="24dip" android:id="@+id/logo"
		android:layout_centerVertical="true" />
	<TextView android:layout_width="fill_parent" android:text="Provincia"
		android:layout_height="wrap_content" android:id="@+id/provincia"
		android:textSize="22sp" android:layout_toRightOf="@id/logo" />
	<TextView android:layout_width="fill_parent" android:text="Regione"
		android:layout_height="wrap_content" android:id="@+id/regione"
		android:textSize="14sp" android:layout_below="@id/provincia"
		android:layout_alignParentBottom="true" android:layout_toRightOf="@id/logo" />
	<TextView android:layout_width="wrap_content" android:text="Co"
		android:layout_height="wrap_content" android:id="@+id/codice"
		android:textSize="16sp" android:layout_alignParentRight="true"
		android:layout_centerVertical="true" android:layout_marginRight="5dip" />
</RelativeLayout>

Il metodo onCreate dell’Activity cambia poco rispetto all’esempio precedente:

1
2
3
4
5
6
7
8
9
@Override
public void onCreate(Bundle savedInstanceState)
{
	super.onCreate(savedInstanceState);
	databaseHelper = new DatabaseHelper(this);
	Cursor c = databaseHelper.getAllProvince();
	startManagingCursor(c);
	setListAdapter(new ProvinceSimpleCursorAdapter(this, c));
}

Se eseguiamo il progetto sull’emulatore otteniamo una lista delle province con una immagine per ogni regione, come mostrato nella seguente immagine:



Se volete provare l’esempio descritto (senza fare il download delle 20 icone delle regioni da wikipedia!) potete scaricare i sorgenti del progetto da importare direttamente in Eclipse.

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.

19 comments

  1. Ho importato questo progetto in eclipse per fare delle prove.
    Il problema è che i appare di fianco al progetto l’icona di errore e non riesco a trovare dove.
    di conseguenza non me lo fa eseguire nell emulatore finche non risolvo il problema 🙂

  2. ciao,
    ho problemi nell’importare i sorgenti del progetto; dopo averli scaricati, manca la cartella src; dopo averla creata, inserisco al suo interno la cartella it, ma ho comunque un errore nel progetto…come risolvo?
    grazie

  3. Ciao, avete ragione avevo sbagliato a creare lo zip, mancava la cartella src…
    Ho aggiornato il file, adesso dovrebbe andare.
    Se anche con questo dà un errore sul progetto probabilmente è perchè non hai il runtime giusto di Android installato. Sulle proprietà del progetto se scegli Android devi averne almeno uno selezionato.
    Fabio

  4. Ciao,Perchè se porvo a sostituire l’immagine abruzzo.png in un’altra abruzzo.png (ma ovviamente stesso nome ma immagine diversa) non me la visualizza più?Ed esce un icona particolare?Ci sono delle restrizioni del codice,tipo grandezza o cosa?
    Alfonso

  5. Salve, ho trovato il suo tutoria fatto veramente bene… e ho una domanda da porle… se volessi mettere sulla lista una riga che rimanga ferma e non segua lo scroll in modo tale da avere in questa riga le intestazioni di colonna del database, come dovrei fare?
    Ringrazio tutti anticipatamente.

    1. Il modo più semplice è usare un layout con in alto una TextView e sotto la ListView. Facendo così il TextView non scrolla con la ListView. Altrimenti ci sono soluzioni più elaborate ma più complicate da mettere in pratica…

      1. Potresti fare un esempio pratico… lo so che forse ad altri non può interessare ma io incappo sempre nell’errore “Your content must have a ListView whose id attribute is ‘android.R.id.list'”.
        Se metto quell’id le cose cambiano e il programma va in crash ancora prima di cominciare… quindi ti sarei grato se potessi fare un esempio semplice che possa farmi capire il problema… ti ringrazio anticipatamente.

  6. Ciao, grazie per il tutorial, sembra fatto molto bene. Io sono parecchio scarso e ci metterò un bel po per studiarmelo e fare qualcosa che funzioni 😛
    Nel frattempo volevo chiederti se esiste un modo per creare il database da un file esterno (.sql, .csv, .sqlite … etc…) perchè se io ho già un database popolato e voglio appunto utilizzarlo nella mia app mi sembra un po problematico dover ri-crearlo utilizzando il codice… non so se mi son spiegato

  7. Ciao, ti basta leggere il file che contiene il codice sql e poi eseguirlo usando execSql. Non è troppo complicato, potrebbe essere un buon argomento per un post!
    Ciao, Fabio

  8. ma il db fisicamente dove viene salvato sul disco? non riesco a trovarlo!

  9. ciao volevo chiederti se conosci il modo per consentire all’utente di selezionare il file di testo alla pressione di un button che verrà utilizzato per popolare un database…
    potresti fare un tutorial davvero interessante 😉
    ciao e grazie!

  10. Io ho un problema per popolare la ListView,i dati vengono passati nel cursor,ma quando mando in esecuzione il programma l’app non me la visualizza,credo di fare tutto bene,posto il codice relativo:

    public class DettaglioImpianto extends Activity {

    private DatabaseHelper db_tts;
    private SQLiteDatabase db;
    TextView nomeImpianto;
    TextView indirizzoImpianto;
    ListView listDettaglioImpianto;
    @Override
    public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.dettaglioimpianto);

    listDettaglioImpianto = (ListView)findViewById(R.id.listViewUnitaLogiche);
    nomeImpianto = (TextView)findViewById(R.id.textNomeImpianto);
    indirizzoImpianto = (TextView)findViewById(R.id.textViewDettagliImpianto);

    db_tts = new DatabaseHelper(this);
    db = db_tts.getReadableDatabase(); //accesso in scrittura al db

    String selectDescrizioneImpianto ="SELECT Descrizione FROM Impianto";
    String selectIndirizzoImpianto ="SELECT Indirizzo FROM Impianto";
    String selectUnitaLogiche = "SELECT _id, Descrizione, Reparto, Area, Stabilimento, Piano FROM UnitaLogica WHERE Impianto_ID=1";
    Cursor cursor = db.rawQuery(selectDescrizioneImpianto,null);

    while (cursor.moveToNext())
    {
    nomeImpianto.setText(cursor.getString(0));
    }
    cursor.close();
    cursor = db.rawQuery(selectIndirizzoImpianto,null);
    while (cursor.moveToNext())
    {
    indirizzoImpianto.setText(cursor.getString(0));
    }
    cursor.close();
    cursor =db.rawQuery(selectUnitaLogiche,null);
    Log.i("","numero colonne cursore : "+ cursor.getColumnCount());
    cursor.moveToFirst();
    Log.i("","id " + cursor.getString(0));
    Log.i("","Descrizione UL" + cursor.getString(1));
    Log.i("","Reparto UL" + cursor.getString(2));
    Log.i("","Area UL" + cursor.getString(3));
    Log.i("","Stabilimento UL" + cursor.getString(4));
    Log.i("","Piano UL" + cursor.getString(5));

    String [] from = new String[]{UnitaLogica.Descrizione,UnitaLogica.Reparto,UnitaLogica.Area};
    int [] to = new int[]{R.id.textViewDescrizioneUL, R.id.textViewRepartoUL, R.id.textViewAreaUL};//in queste textView

    Log.i("","" +cursor.getCount());
    SimpleCursorAdapter adapter = new SimpleCursorAdapter(getApplicationContext(),R.layout.row_unitalogica,cursor,from,to);
    // UnitaLogicaAdapter adapter = new UnitaLogicaAdapter(getApplicationContext(), cursor);
    listDettaglioImpianto.setAdapter(adapter);

    cursor.close();
    db.close();

    }

    }

    Proprio non capisco dov’è il problema

  11. Io ho problemi di performance.
    Da quel che ho capito qui
    http://www.lundici.it/2012/02/android-adapters/
    Non sempre newView viene chiamato. Questo perchè le view sono riciclate automaticamente dal sistema.
    Il problema è conservare l’holder pattern, per evitare di usare findViewById. Questo codice lo conserva?

  12. Salve. Volevo sapere se questo tutorial funziona con Android 1.6.
    Ho provato ad implementarlo campbiando i campi ma sembra non funzionare.

    Grazie

  13. Innanzi tutto la ringrazio per l’ottimo tutorial! Avrei solo 2 domande, giusto per chiarirmi dei mini dubbi:
    1. perchè nella classe ProvinceSimpleCursorAdapter è implementato solo il metodo BindView?Non si dovrebbe necessariamente implementare anche il metodo newView?

    2. perchè nel costruttore del ProvinceSimpleCursorAdapter quando definiamo i nomi delle colonne della tabella:
    super(…
    new String[] { ProvinciaTable.NOME, ProvinciaTable.CODICE, “nomeRegione” }, …

    al posto di “nomeRegione” non è stato messo ProvinciaTable.id_Regione o RegioneTable.NOME? Non dovrebbe dare errore visto che il campo “nomeRegione” non esiste in nessuna precedente definizione delle tabelle?

    Sperando in un riscontro La ringrazio anticipatamente per le delucidazioni.

Leave a comment

Your email address will not be published.