La libreria Room, introdotta con Android Architecture Components,
rappresenta una soluzione "ufficiale"
dedicata alla persistenza su Sqlite, caratterizzata da un approccio O/RM. Tecnicamente, si tratta di un
abstraction layer, uno strato software che permette di sfruttare tutte le potenzialità
del database senza la preoccupazione di affrontare ogni aspetto nei dettagli.
Room può essere utilizzato da solo in un'applicazione o integrato con ViewModel
e LiveData per supportare l'interfaccia utente nella massima efficienza. In questa lezione, ne esploriamo
i principi fondamentali mettendo infine tutto in pratica con un esempio.
Integrare Room in un progetto
Per avere a disposizione Room, è necessario agire sulla configurazione del progetto Android,
accertandosi che:
-
- il file build.gradle del progetto includa i repository di Google, mediante il metodo
google()
:
allprojects { repositories { jcenter() google() } }
- nel file build.gradle siano incluse, a livello di modulo, le dipendenze necessarie:
- il file build.gradle del progetto includa i repository di Google, mediante il metodo
dependencies{
...
...
implementation "android.arch.persistence.room:runtime:1.0.0"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0"
}
Gerarchia delle componenti
In un'applicazione Room-based, le componenti da implementare possono essere classificate in tre tipologie, come illustrato dall'immagine seguente:
In particolare:
- Room Database: rappresenta l'astrazione del database su cui vogliamo lavorare.
Verrà creata una classe di questo tipo che costituirà il centro di
accesso ai dati: qualsiasi operazione da svolgere passerà di qui; - Entity: ogni classe di questo tipo rappresenta una tabella del
database. All'interno delle classiEntity
predisporremo tante variabili d'istanza quanti
sono i campi previsti dallo schema della tabella, più altri eventuali membri che si renderanno
necessari; - DAO (Data Access Object): un DAO viene utilizzato per incamerare
il codice che agirà sui dati, e conterrà i veri e propri comandi per le
operazioni CRUD. Tipicamente esistono più DAO in un'applicazione, ma non è necessario
ve ne siano tanti quante le Entity. In genere, ogni DAO raccoglie tutte le interazioni che
riguardano un sottosistema del software: se stiamo modellando, ad esempio, l'accesso
ai dati di una biblioteca, potremo avere un DAO per gestire i dati utente, uno per il catalogo
libri, uno per i prestiti e così via.
Come vedremo, la configurazione che attribuiremo ai suddetti elementi sarà molto
semplice e dovremo specificare solo ciò che Room non è in grado di "intuire" da solo (gli
aspetti personalizzati, per intenderci). Anche di SQL ne useremo ben poco. Infatti, i metodi
disponibili nei DAO lavorano direttamente su oggetti Java e riescono a capire l'operazione
da svolgere interpretando le annotazioni applicate: query e comandi espliciti
vengono scritti solo quando si renda necessario personalizzare il lavoro.
L'esempio
L'esempio che realizziamo utilizza Room per gestire un'applicazione in cui potremo
salvare delle note testuali. Ogni nota sarà composta dal semplice testo e da un numero intero
che svolgerà il ruolo di chiave primaria. Tale valore sarà autogenerato di volta in volta
dal sistema. Ogni oggetto inserito potrà essere modificato e cancellato e, come azioni generali,
supporteremo la possibilità di scegliere di caricare le note inserite (da mostrare in una ListView) o
cancellarle tutte.
Il database
La classe che rappresenta il database prende il nome di DbManager:
@Database(entities = {Note.class}, version = 1)
public abstract class DbManager extends RoomDatabase {
private static DbManager INSTANCE;
public abstract NoteDao noteModel();
public static DbManager getInMemoryDatabase(Context context) {
if (INSTANCE == null) {
INSTANCE =
Room.inMemoryDatabaseBuilder(context.getApplicationContext(), DbManager.class)
.allowMainThreadQueries()
.build();
}
return INSTANCE;
}
public static DbManager getDatabase(Context context) {
if (INSTANCE == null) {
INSTANCE =
Room.databaseBuilder(context.getApplicationContext(), DbManager.class, "note_db")
.allowMainThreadQueries()
.build();
}
return INSTANCE;
}
public static void destroyInstance() {
INSTANCE = null;
}
}
Di seguito elenchiamo i tratti salienti che contradistinguono la classe appena vista (così come tutte le altre classi analoghe delle applicazioni basate su Room):
- è annotata con
@Database,
che introduce l'elenco delle Entity che
dovrà gestire (in questo caso solo Note) e la versione del database; - è astratta ed estende
RoomDatabase
; - contiene metodi astratti senza argomenti ognuno dei quali restituisce un DAO. In questo caso,
ve ne è solo uno,noteModel()
. Non sarà necessario fornire un'implementazione; - per facilitarne la gestione, la classe contiene un riferimento statico a sè stessa, denominato
INSTANCE
. Il database può essere gestito in maniera persistente (con salvataggio
su disco) o in memory (ideale per realizzare cache e strutture dati di appoggio). Si sceglie
l'una o l'altra a seconda che venga inizializzata con unDatabaseBuilder
o unInMemoryDatabaseBuilder
:
noi, a scopo di esercizio, abbiamo predisposto metodi per entrambi i casi.
Ogni azione CRUD inizierà sempre richiedendo a questa classe un riferimento ad un DAO.
Classe Entity
L'unica Entity di cui abbiamo bisogno coincide con la classe Note, la rappresentazione
Java di una tabella a due colonne:
@Entity
public class Note {
@PrimaryKey(autoGenerate = true)
@NonNull
public long id;
public String text;
@Override
public String toString() {
return String.format("%s (id = %d)", text, id);
}
}
Ciò che rende questa classe un'Entity è l'annotazione @Entity
. L'annotazione @PrimaryKey
segnala che la
variabile d'istanza id
è la chiave primaria ed è autogenerata (autoGenerate = true
).
Il DAO
Con gli elementi definiti finora, abbiamo creato il livello di accesso al database
e gli oggetti che simboleggiano i singoli record da inserire in una tabella. Ciò
che manca è lo strato intermedio, il DAO. Creiamo a tale scopo l'interfaccia NoteDAO:
@Dao
public interface NoteDao {
@Insert(onConflict = REPLACE)
void insertNote(Note note);
@Query("SELECT * FROM Note")
List<Note> loadAllNotes();
@Query("DELETE FROM Note")
void deleteAll();
@Delete
void deleteNote(Note n);
}
In linea di massima, per creare un DAO con Room è necessario:
- applicare l'annotazione
@Dao
; - creare una
interface
che definisca tutti i metodi di accesso ai dati. Sarà poi
Room a fornire loro un'implementazione.
Per connotare i metodi, sono state applicate altrettante annotazioni. Alcune di
esse contengono codice SQL, altre sono auto-esaustive: un caso o l'altro si renderà
necessario a seconda di quanto dovremo personalizzare il comando.
Con @Insert
, @Update
e @Delete
potremo, rispettivamente, inserire, modificare o
cancellare un oggetto Note: questo sarà direttamente tradotto nel relativo comando sul
database. Per effettuare interrogazioni o eseguire comandi di modifica personalizzati
passiamo direttamente il comando SQL. All'annotazione @Insert
, è stata specificata la proprietà onConflict = REPLACE
, che impone la sostituzione dei dati nel caso in cui la nota da inserire abbia una chiave primaria già presente nella tabella: in questo modo utilizziamo @Insert
per effettuare sia inserimento che
modifica.
Implementiamo l'esempio
Per seguire una via semplificata, tutta la logica dell'esempio sarà inserita nell'Activity.
Ecco i punti cruciali dell'integrazione con Room.
Tra le variabili d'istanza, ne fissiamo due per nostra comodità: una conserva il riferimento al database
attuale (lo richiameremo tramite il metodo getDatabaseManager()
) ed un'altra il riferimento
all'eventuale nota che stiamo modificando (in tutti gli altri casi, rimarrà null
):
private DbManager db;
private Note currentNote;
private DbManager getDatabaseManager()
{
if (db==null)
db=DbManager.getInMemoryDatabase(this);
return db;
}
A scopo di esempio, abbiamo creato un database in memoria, ma si consideri che al termine dell'applicazione
non resterà traccia dei dati inseriti. Nel metodo onCreate()
recuperiamo riferimenti ad elementi dell'interfaccia
utente ed impostiamo ArrayAdapter e ListView. Nell'OnItemClickListener
a quest'ultima legato,
recuperiamo il riferimento all'oggetto Note selezionato e lo usiamo per compilare l'EditText:
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Note n = adapter.getItem(position);
MainActivity.this.txt_note.setText(n.text);
currentNote = n;
}
Si noti che abbiamo impostato la variabile currentNote
per sancire l'inizio della fase
di modifica: se al click del pulsante Salva questa verrà trovata nulla, si procederà ad un nuovo
inserimento. Segue il metodo save()
:
public void save(View view) {
if (txt_note.getText().length()==0) {
Toast.makeText(this, "Inserire del testo", Toast.LENGTH_SHORT).show();
// eventuale fase di modifica viene annullata
abortUpdate();
return;
}
if (currentNote==null)
currentNote = new Note();
currentNote.text = txt_note.getText().toString();
// inserimento o modifica? Dipende se la nota esiste già nella tabella
getDatabaseManager().noteModel().insertNote(currentNote);
abortUpdate();
// aggiorna ListView
updateList();
}
Il metodo abortUpdate()
è stato predisposto al fine di avere un luogo unico
dove concentrare le operazioni per interrompere eventuali modifiche in corso (ad esempio, se iniziamo a modificare una nota e nel frattempo attiviamo un pulsante di
cancellazione):
private void abortUpdate()
{
currentNote = null;
txt_note.getText().clear();
}
Anche per la cancellazione recupereremo il riferimento alla nota da eliminare e ne
ordineremo la rimozione mediante il metodo deleteNote()
offerto dal model:
public void deleteNote(View view) {
// annulliamo eventuali modifiche in corso
abortUpdate();
// cerchiamo la nota relativa alla riga selezionata
int position = list.getPositionForView(view);
Note n = adapter.getItem(position);
// cancelliamo la nota
getDatabaseManager().noteModel().deleteNote(n);
updateList();
}
Per correttezza, sarà infine importante invocare la chiusura del database in fase di
distruzione dell'Activity:
@Override
protected void onDestroy() {
DbManager.destroyInstance();
super.onDestroy();
}
Conclusioni
Con questa lezione, possiamo dire di aver appreso la gestione dei dati con Room. Ci sono però altre interessanti tematiche
da affrontare relativamente a questa libreria, quali la possibilità di gestire operazioni asincrone, l'uso di Converter per la conversione di dati e soprattutto
l'integrazione con ViewModel e LiveData per realizzare applicazioni moderne, efficienti ma al contempo robuste.