Tipicamente, la gestione dei dati di un'app Android si basa su Sqlite, un DBMS relazionale già integrato nel sistema, capace di salvare un intero database relazionale in un unico file. Il suo funzionamento è efficiente ma richiede le consuete conversioni tra modello ad oggetti e relazionale, che possono risultare piuttosto ripetitive. Realm, la libreria di cui tratta questa lezione, offre una gestione dei dati interamente ad oggetti. Non si tratta nè di un O/RM nè di un wrapper per Sqlite, bensì di un meccanismo di persistenza totalmente originale.
Realm offre soluzioni per tutte le principali tecnologie e tale offerta si è ampliata ulteriormente
da quando, nel 2019, il progetto è stato acquisito da MongoDB.
Troveremo infatti SDK Realm per Android, iOS, .NET, Node.js ma anche piattaforme più recenti quali React Native e Flutter (versione Preview).
Realm in un progetto Android Studio
Per utilizzare le funzionalità che Realm SDK per Android offre è necessario che il proprio ambiente di lavoro soddisfi i seguenti prerequisiti:
- Android Studio 1.5.1 o versioni successive;
- JDK di versione maggiore o uguale a 7;
- Android SDK dotato almeno di API di livello 16 ovvero relative alla versione 4.1 del sistema operativo.
L'integrazione di Realm in un progetto Android Studio offre la comoda forma di plugin. Pertanto, procederemo in due passi:
- nel file build.gradle del progetto inseriremo la direttiva
classpath
:buildscript { repositories { google() jcenter() } dependencies { classpath "com.android.tools.build:gradle:3.5.1" classpath "io.realm:realm-gradle-plugin:10.8.0" } }
- nel file build.gradle dell'applicazione richiamiamo il plugin con la seguente direttiva a inizio file:
apply plugin: 'realm-android'
Al termine di tali modifiche sarà necessario sincronizzare nuovamente il progetto con i file di configurazione di Android Studio: l'IDE, comunque, ce lo ricorderà prontamente.
Inizializzazione di Realm
Prima di poter utilizzare Realm è necessario inizializzarlo, invocando il metodo statico init
della classe Realm
. Per evitare di ripetere questa fase (ed altre analoghe) continuamente, è conveniente estendere la classe Application nel seguente modo:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
Realm.init(this);
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().allowWritesOnUiThread(true).build();
Realm.setDefaultConfiguration(realmConfiguration);
}
}
e riportare poi il nome di questa classe nell'attributo name
del nodo <application>
, all'interno del file AndroidManifest.xml:
<application
android:name=".MyApplication"
... >
In fase di inizializzazione, volendo, si può provvedere alla cancellazione del vecchio archivio con:
Realm.deleteRealm(realmConfiguration);
La concentrazione degli elementi di configurazione nella sola classe MyApplication
permette anche di applicare alcuni
aspetti in maniera omogenea. Ad esempio, nell'attivazione del Builder abbiamo invocato anche il metodo allowWritesOnUiThread
che
permette di eseguire scritture sul thread principale dell'applicazione. Sottolineiamo che abbiamo utilizzato questa misura per facilitare
l'avvio all'utilizzo di Realm sebbene, al fine di ottimizzare le performance, tali operazioni possono essere svolte in modalità asincrona
come viene specificato nella documentazione ufficiale.
Prime operazioni con Realm
La classe MyApplication mette a disposizione un'istanza di Realm ovunque serva nell'applicazione. Per utilizzarla nell'Activity, converrà ottenere un riferimento ad un oggetto Realm nel metodo onCreate
(da salvare come membro della classe). L'oggetto verrà racchiuso nel metodo onDestroy
:
private Realm realm;
@Override
protected void onCreate(Bundle savedInstanceState) {
..
..
realm= Realm.getDefaultInstance();
..
..
}
@Override
protected void onDestroy() {
super.onDestroy();
realm.close();
}
I dati verranno salvati in Realm mediante oggetti che diventeranno automaticamente persistenti. estendendo la classe RealmObject
. Creiamo, ad esempio, una classe Libro. Questa operazione corrisponderà alla definizione di una tabella in un database:
public class Libro extends RealmObject{
private String autore;
private String titolo;
private int numeroPagine;
private int annoPubblicazione;
/*
* OMISSIS: getter, setter e altri metodi
*/
}
Per inserire un nuovo oggetto all'interno del database, dovremo semplicemente creare un oggetto di questa classe ed inserirvi i dati:
realm.executeTransaction((Realm realm) -> {
Libro l = realm.createObject(Libro.class);
l.setAutore("Alessandro Manzoni");
l.setTitolo("I Promessi Sposi");
l.setNumeroPagine(600);
l.setAnnoPubblicazione(1827);
});
Si noti che l'oggetto è stato generato con il metodo createObject
e non con il consueto operatore new
. Le operazioni di modifica andranno eseguite all'interno di una Transaction, che può essere sviluppata con
un oggetto di classe anonima come si vede nell'esempio precedente.
Eseguire query
Anche per le query l'approccio è totalmente ad oggetti. Il metodo where
recupererà gli oggetti dalla classe specificata ed il suo risultato potrà essere trattato con filtri tramite altri metodi specifici. Al termine della configurazione della selezione, si invocherà il metodo findAll
per il recupero di tutti i risultati corrispondenti, findAllSorted
per ottenere un risultato ordinato o findFirst
qualora si voglia solo il primo risultato. Ecco alcuni esempi:
- tutti gli oggetti appartenenti alla classe Libro:
RealmResults<Libro> results = realm.where(Libro.class).findAll()
- tutti gli oggetti appartenenti alla classe Libro ordinati in senso decrescente in base al valore del membro
annoPubblicazione
:
RealmResults<Libro> results = realm.where(Libro.class).findAllSorted("annoPubblicazione", Sort.DESCENDING)
- tutte le opere pubblicate tra il 1940 ed il 1960:
RealmResults<Libro> results = realm.where(Libro.class).between("annoPubblicazione",1940, 1960).findAll()
- tutte le opere successive all'anno 2000:
RealmResults<Libro> results = realm.where(Libro.class).greaterThan("annoPubblicazione",2000).findAll()
Oltre ai metodi di filtering appena visti (ed altri ancora consultabili nella documentazione ufficiale) esistono costrutti logici che permettono di formulare interrogazioni più avanzate, come ad esempio beginGroup
e endGroup
per racchiudere delle valutazioni (una sorta di parentesi), not
per negare una condizione o or
per l'omonima operazione logica. Ad esempio, cerchiamo i libri che neghino la
condizione per cui l'anno di pubblicazione sia inferiore al 1970 o il numero di pagine sia superiore a 400:
RealmResults<Libro> results =realm.where(Libro.class)
.not()
.beginGroup()
.lessThan("annoPubblicazione",1970)
.or()
.greaterThan("numeroPagine",400)
.endGroup()
.findAll();
La classe RealmResults costituisce il set di risultati della query ed è una Collection
iterabile, e pertanto accessibile con i consueti metodi offerti dal linguaggio Java: ciclo for
, accesso in base alla posizione e Iterator
.
Cancellazione e modifica
I risultati di una query possono essere oggetto di cancellazione. Ad esempio, ottenuto un oggetto RealmResults
si può procedere alla cancellazione complessiva del contenuto con deleteAllFromRealm
o, in alternativa, del primo o ultimo valore, rispettivamente, con deleteFirstFromRealm
e deleteLastFromRealm
. Per la cancellazione puntuale di un elemento, lo si può selezionare in base alla posizione, ordinandone l'eliminazione:
RealmResults<Libro> results = ....
// selezione e distruzione del quarto elemento
Libro daCancellare=results.get(3);
daCancellare.deleteFromRealm();
Gli oggetti che vengono recuperati possono anche essere modificati all'interno di una transazione. In alternativa, possiamo inserire in un realm (l'equivalente di un database, secondo questo paradigma) un oggetto "non gestito", ossia un oggetto creato in Java con la normale procedura di istanziazione, utilizzando il metodo copyToRealm
. Inoltre, sarà possibile ricevere in automatico informazioni sulle modifiche subite da oggetti referenziati in una collection RealmResults utilizzando un listener di classe RealmChangeListener
:
RealmResults<Libro> results = ....
results.addChangeListener(new RealmChangeListener<RealmResults<Libro>>() {
@Override
public void onChange(RealmResults<Libro> r) {
/*
* r costituisce il set aggiornato di risultati
*/
}
});
Appendice
Quanto visto in questa lezione può essere sufficiente per utilizzare Realm come sistema di persistenza della
propria app, sebbene lo studio può ulteriomente essere approfondito. Elementi interessanti di studio sono, ad esempio,
la libreria di Adapter messa a disposizione per integrare i risultati delle query con AdapterView tradizionali e
RecyclerView, nonchè
la possibilità di svolgere chiamate asincrone per non penalizzare le prestazioni dell'interfaccia utente.