Il salvataggio di dati su file potrebbe essere sufficiente in molti casi per la
memorizzazione di grandi quantità di informazione. In fin dei conti le API di I/O fornite dal linguaggio Java permettono di trattare dati binari e testuali, salvare strutture dati serializzate ed altro ancora.
Qualcosa però a cui il programmatore è particolarmente abituato è il database relazionale e l'interazione mediante linguaggio SQL tanto da sentirne il bisogno anche in Android. L'evidenza di soddisfare questa necessità richiedeva che venisse individuato un prodotto dotato di determinati requisiti: open-source, ampiamente diffuso, mantenuto e documentato da una comunità prospera, efficiente e soprattutto che non richiedesse l'esecuzione di un servizio continuo in background. La soluzione esisteva già nel mondo del software libero e risiedeva in SQLite.
SQLite
SQLite può essere considerato una delle tipologie di database relazionali più usate al mondo, non solo perchè in ogni tecnologia di programmazione esistono
apposite API per la sua integrazione ma anche perchè praticamente ogni installazione di app mobile che abbia bisogno di memorizzare dati
ha a disposizione un database SQLite.
SQLite rispetta tutti i requisiti di efficienza e disponibilità di cui si è detto.Si tratta, in realtà, di una libreria software che permette di gestire in un unico file un database relazionale.
Oltretutto è un progetto in continua espansione che mette a disposizione molti aspetti dei moderni DBMS: View, Trigger, transazioni, indici oltre al comunissimo e comodissimo interfacciamento con linguaggio SQL.
Nota: per chi non lo sapesse, SQL è il linguaggio per inviare comandi ad un database ed estrapolarne dati. È il formalismo con cui vengono realizzate le ben note query. In questa sede non ci si dilungherà sull'argomento ma se ne darà per assodata la conoscenza da parte del lettore. Qualora così non fosse, si ritiene opportuno un approfondimento su specifica documentazione.
Database nelle proprie App
Per avere un database SQLite nella propria App Android, non è necessario scaricare né installare niente: semplicemente basta chiedere. La libreria SQLite infatti è già inclusa nel sistema operativo e le API disponibili nel framework offrono tutto il supporto necessario.
Tali funzionalità sono il meccanismo di base per l'interazione tra app Android e SQLite ma si tenga conto
che al giorno d'oggi esiste un approccio di pù alto livello, più produttivo e completo, il framework
che rappresenta la soluzione di riferimento
per gestire un database relazionale in un'app Android.
Questi i passi:
- creare la struttura del database. Il programmatore dovrà preparare uno script SQL che crei la struttura interna del database (tabelle, viste ecc.). Nel realizzarla, potrà procedere nella maniera che gli è più congeniale scrivendola a mano o aiutandosi con uno strumento visuale come Sqliteman. L'importante è che alla fine di questa fase abbia una stringa contenente i comandi di creazione;
- creare una classe Java che estenda
SQLiteOpenHelper
. Questa classe, che nel seguito chiameremo helper, servirà a gestire la nascita e l'aggiornamento del database su memoria fisica e a recuperare un riferimento all'oggetto SQLiteDatabase, usato come accesso ai dati; - creare una classe per l'interazione con il database. Solitamente questa ha due caratteristiche: (1) contiene un riferimento all'oggetto helper definito al punto precedente, (2) contiene i metodi con cui, dalle altre componenti dell'app, verranno richieste operazioni e selezioni sui dati.
Puntualizziamo che i tre step appena enunciati non sono assolutamente obbligatori, esistono infatti modalità alternative di azione. Sono tuttavia una prassi molto comune e funzionale per l'approntamento di un database a supporto di un'app. Tanto verrà dimostrato con l'esempio a seguire. Se ne consiglia perciò l'osservanza.
Esempio pratico
Verrà creata un'Activity che gestisce un piccolo scadenziario. I dati inseriti saranno costituiti da un oggetto, un testo che costituisce il vero promemoria ed una data.
Mettiamo subito in pratica i primi due step: creazione del database e della classe helper.
public class DBhelper extends SQLiteOpenHelper
{
public static final String DBNAME="BILLBOOK";
public DBhelper(Context context) {
super(context, DBNAME, null, 1);
}
@Override
public void onCreate(SQLiteDatabase db)
{
String q="CREATE TABLE "+DatabaseStrings.TBL_NAME+
" ( _id INTEGER PRIMARY KEY AUTOINCREMENT," +
DatabaseStrings.FIELD_SUBJECT+" TEXT," +
DatabaseStrings.FIELD_TEXT+" TEXT," +
DatabaseStrings.FIELD_DATE+" TEXT)";
db.execSQL(q);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
{ }
}
Per questioni “organizzative” del codice, i nomi dei campi e della tabella sono stati definiti in costanti nella seguente classe:
public class DatabaseStrings
{
public static final String FIELD_ID="_id";
public static final String FIELD_SUBJECT="oggetto";
public static final String FIELD_TEXT="testo";
public static final String FIELD_DATE="data";
public static final String TBL_NAME="Scadenze";
}
Notiamo che per prima cosa viene creato un costruttore al cui interno si invoca quello della classe base:
super(context, DBNAME, null, 1);
Tra gli argomenti passati ne notiamo due in particolare:
- il nome del database: è il secondo parametro, di tipo String, valorizzato con la costante DBNAME. Questo è il nome che il database avrà nello spazio disco dell'applicazione;
- la versione del database: è il quarto argomento, di tipo intero e valore 1.
Inoltre è stato fatto l'override di due metodi:
onCreate
: viene invocato nel momento in cui non si trova nello spazio dell'applicazione un database con nome indicato nel costruttore. Da ricordare che onCreate verrà invocato una sola volta, quando il database non esiste ancora. Il parametro passato in input è un riferimento all'oggetto che astrae il database. La classe SQLiteDatabase è importantissima in quanto per suo tramite invieremo i comandi di gestione dei dati. Il metodoonCreate
contiene la query SQL che serve a creare il contenuto del database. Ciò rappresenta l'applicazione del primo step, enunciato in precedenza. Notare che al suo interno non c'è alcun comandoCREATE DATABASE
in quanto il database stesso è già stato creato dal sistema. Il comando SQL di creazione verrà invocato medianteexecSQL
;onUpgrade
: viene invocato nel momento in cui si richiede una versione del database più aggiornata di quella presente su disco. Questo metodo contiene solitamente alcune query che permettono di adeguare il database alla versione richiesta.
La classe in cui gestiremo il database prende il nome di DbManager
, ne vediamo subito il codice:
public class DbManager
{
private DBhelper dbhelper;
public DbManager(Context ctx)
{
dbhelper=new DBhelper(ctx);
}
public void save(String sub, String txt, String date)
{
SQLiteDatabase db=dbhelper.getWritableDatabase();
ContentValues cv=new ContentValues();
cv.put(DatabaseStrings.FIELD_SUBJECT, sub);
cv.put(DatabaseStrings.FIELD_TEXT, txt);
cv.put(DatabaseStrings.FIELD_DATE, date);
try
{
db.insert(DatabaseStrings.TBL_NAME, null,cv);
}
catch (SQLiteException sqle)
{
// Gestione delle eccezioni
}
}
public boolean delete(long id)
{
SQLiteDatabase db=dbhelper.getWritableDatabase();
try
{
if (db.delete(DatabaseStrings.TBL_NAME, DatabaseStrings.FIELD_ID+"=?", new String[]{Long.toString(id)})>0)
return true;
return false;
}
catch (SQLiteException sqle)
{
return false;
}
}
public Cursor query()
{
Cursor crs=null;
try
{
SQLiteDatabase db=dbhelper.getReadableDatabase();
crs=db.query(DatabaseStrings.TBL_NAME, null, null, null, null, null, null, null);
}
catch(SQLiteException sqle)
{
return null;
}
return crs;
}
}
Prima cosa da notare: la classe contiene un riferimento al DbHelper.
I metodi che vengono implementati mostrano tre operazioni basilari da svolgere sulla tabella del db: save per salvare una nuova scadenza, delete per cancellarne una in base all'id, query per recuperarne l'intero contenuto.
Da questi metodi, emerge un modus operandi comune. Infatti per lavorare su un oggetto SQLiteDatabase, la prima cosa da fare è recuperarne un riferimento. Lo si può fare con i metodi di SQLiteOpenHelper, getReadableDatabase()
e getWriteableDatabase()
che restituiscono, rispettivamente, un riferimento al database “in sola lettura” e uno che ne permette la modifica.
Sull'oggetto SQliteDatabase recuperato, si svolge una delle quattro operazioni CRUD, le azioni fondamentali della persistenza (Create, Read, Update, Delete).
Nelle API Android per Sqlite esiste almeno un metodo per ogni tipo di azione:
query
: esegue la lettura sulle tabelle: mette in pratica il SELECT sui dati. I suoi svariati overload predispongono argomenti per ogni parametro che può essere inserito in una interrogazione di questo tipo (selezione, ordinamento, numero massimo di record, raggruppamento, etc.);delete
: per la cancellazione di uno o più record della tabella;insert
: per l'inserimento. Riceve in input una stringa che contiene il nome della tabella e la lista di valori di inizializzazione del nuovo record mediante la classe ContentValues. Questa è una struttura a mappa che accetta coppie chiave/valore dove la chiave rappresenta il nome del campo della tabella;update
: esegue modifiche. Il metodo associa i parametri usati nell'insert e nel delete.
Tutti questi metodi non richiedono un uso esplicito di SQL. Chi ne avesse bisogno o preferisse per altre ragioni scrivere totalmente i propri comandi e query può utilizzare metodi di SqliteDatabase come execSQL
e rawQuery
.
Vale anche la pena sottolineare che i metodi appena indicati offrono una versione “parametrica” delle condizioni di selezione dei record (la classica clausola WHERE di SQL che spesso è indispensabile in selezioni, cancellazioni e aggiornamenti). Ciò è visibile nella classe DbManager, nel metodo che si occupa della cancellazione:
db.delete(DatabaseStrings.TBL_NAME, DatabaseStrings.FIELD_ID+"=?", new String[]{Long.toString(id)})>0
In questi casi, la classe SQLiteDatabase vuole che una stringa raccolga la parte fissa del contenuto della clausola WHERE
sostituendo le parti variabili con punti interrogativi. Gli argomenti attuali verranno passati ad ogni invocazione in un array di stringhe. Nell'esecuzione della query ogni punto interrogativo verrà, in ordine, sostituito con un parametro dell'array.
Altra classe cui fare attenzione, è Cursor. Rappresenta un puntatore ad un set di risultati della query. Somiglia a quell'elemento che in altre tecnologie prende il nome di RecordSet
o ResultSet
. Un oggetto Cursor può essere spostato per puntare ad una riga differente del set di risultati. Ciò viene fatto con i metodi moveToNext
, moveToFirst
, moveToLast
e così via.
Una volta che il cursore ha raggiunto la riga desiderata si può passare alla lettura dei dati con metodi specifici in base al tipo di dato (getString
, getLong
ecc.) indicando il nome del campo.
Ad esempio, se l'oggetto crs di classe Cursor punta ad un insieme di righe della tabella Scadenze, una volta indirizzato sulla riga desiderata si potrà leggere il campo relativo all'oggetto con:
crs.getString(crs.getColumnIndex(DatabaseStrings.FIELD_SUBJECT))
Con getColumnIndex
viene trovato l'indice del campo.
L'Activity ed il CursorAdapter
L'interfaccia utente che si occuperà di interagire con il db è molto semplice.
Costituita da un form per l'inserimento di nuove scadenze e da una ListView sottostante che mostra i record presenti nel db, permette tuttavia di sperimentare le funzionalità sinora descritte.
public class MainActivity extends AppCompatActivity
{
private DbManager db=null;
private CursorAdapter adapter;
private ListView listview=null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
db=new DbManager(this);
listview= findViewById(R.id.listview);
Cursor crs=db.query();
adapter=new CursorAdapter(this, crs, 0)
{
@Override
public View newView(Context ctx, Cursor arg1, ViewGroup arg2)
{
View v=getLayoutInflater().inflate(R.layout.listactivity_row, null);
return v;
}
@Override
public void bindView(View v, Context arg1, Cursor crs)
{
String oggetto=crs.getString(
crs.getColumnIndex(DatabaseStrings.FIELD_SUBJECT));
String data=crs.getString(crs.getColumnIndex(DatabaseStrings.FIELD_DATE));
TextView txt= v.findViewById(R.id.txt_subject);
txt.setText(oggetto);
txt= v.findViewById(R.id.txt_date);
txt.setText(data);
ImageButton imgbtn= v.findViewById(R.id.btn_delete);
imgbtn.setOnClickListener((View view) -> {
int position=listview.getPositionForView(v);
long id=adapter.getItemId(position);
if (db.delete(id))
adapter.changeCursor(db.query());
});
}
@Override
public long getItemId(int position)
{
Cursor crs=adapter.getCursor();
crs.moveToPosition(position);
return crs.getLong(crs.getColumnIndex(DatabaseStrings.FIELD_ID));
}
};
listview.setAdapter(adapter);
}
public void salva(View v)
{
EditText sub= findViewById(R.id.oggetto);
EditText txt= findViewById(R.id.testo);
EditText date= findViewById(R.id.data);
if (sub.length()>0 && date.length()>0)
{
db.save(sub.getEditableText().toString(),
txt.getEditableText().toString(),
date.getEditableText().toString());
adapter.changeCursor(db.query());
}
}
}
L'Activity gestisce l'interazione con il database appellandosi all'oggetto DbManager istanziato. I comandi di salvataggio e cancellazione che l'utente impartisce vengono, rispettivamente, attivati mediante:
- il pulsante con etichetta Salva, il cui click innesca il metodo
salva
della MainAcitivity, grazie al valore assegnato all'attributo onClick nel layout XML:
<Button android:layout_span="2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="salva" android:background="@drawable/button_selector" style="@style/form_big_textstyle" android:text="Salva"/>
- il pulsante presente in ogni riga della ListView, azionato mediante l'
onClickListener
dichiarato anch'esso nell'Activity principale.
Fin qui niente di particolarmente sorprendente. L'elemento di maggiore novità è l'Adapter che è stato usato: il CursorAdapter. Il suo scopo è trasformare ogni riga del risultato della query in una View.
Nell'esempio, il layout usato per mostrare la singola riga è il seguente:
<RelativeLayout
android:layout_height="wrap_content"
android:layout_width="400dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:padding="5dp"
android:layout_toLeftOf="@+id/btn_delete"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/big_textstyle"
android:id="@+id/txt_subject"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/small_textstyle"
android:id="@+id/txt_date"/>
</LinearLayout>
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:src="@android:drawable/ic_menu_delete"
android:id="@+id/btn_delete"
/>
</RelativeLayout>
Il CursorAdapter lo tratterà mediante override di due metodi in particolare:
newView
: crea la View cui associare i dati prelevati dal database. Ciò viene fatto, in questo e molti altri casi, mediante inflating;bindView
: riceve in input una View, da completare con i dati di un singolo record del cursore, e il Cursor già posizionato sulla riga giusta. Grazie all'implementazione di newView, questo metodo riceverà sempre una View pronta, creata per l'occasione o “riciclata” da quelle già esistenti. Le operazioni dominanti in bindView riguarderanno il recupero di controlli presenti nella View ed il loro completamento con i dati presi dal Cursor.
Ciò che viene fatto all'interno dei predetti metodi non dovrebbe stupire più in quanto sono le stesse operazioni fatte per gli Adapter customizzati già presentati in questa guida.
Un terzo metodo frutto di override nel CursorAdapter è getItemId
. Fornisce l'id del record in base alla posizione e viene usato per completare le condizioni di selezione richieste per la cancellazione.