In questo ultimo articolo della serie Android Architecture Components, esploreremo la libreria di persistenza Room, un'eccellente nuova risorsa che rende molto più semplice lavorare con i database in Android. Fornisce un livello di astrazione su SQLite, query SQL controllate in fase di compilazione e anche query asincrone e osservabili. Room porta le operazioni del database su Android a un altro livello.
Poiché questa è la quarta parte della serie, presumo che tu abbia familiarità con i concetti e le componenti del pacchetto Architecture, come LiveData e LiveModel. Tuttavia, se non hai letto nessuno degli ultimi tre articoli, sarai comunque in grado di seguirlo. Tuttavia, se non sai molto di questi componenti, prenditi un po 'di tempo per leggere la serie: potresti divertirti.
Come accennato, Room non è un nuovo sistema di database. È un livello astratto che avvolge il database SQLite standard adottato da Android. Tuttavia, Room aggiunge così tante funzionalità a SQLite che è quasi impossibile da riconoscere. Room semplifica tutte le operazioni legate al database e le rende molto più potenti poiché consente la possibilità di restituire osservazioni osservabili e query SQL controllate in fase di compilazione.
La stanza è composta da tre componenti principali: il Banca dati, il DAO (Oggetti di accesso ai dati) e Entità. Ogni componente ha la sua responsabilità e tutti devono essere implementati affinché il sistema funzioni. Fortunatamente, tale implementazione è abbastanza semplice. Grazie alle annotazioni fornite e alle classi astratte, il boilerplate per implementare Room è ridotto al minimo.
@Entità
.@Dao
che media l'accesso agli oggetti nel database e nelle sue tabelle. Esistono quattro annotazioni specifiche per le operazioni DAO di base: @Inserire
, @Aggiornare
, @Elimina
, e @query
.@Banca dati
, che si estende RoomDatabase
. La classe definisce la lista di Entità e i suoi DAO.Per usare Room, aggiungi le seguenti dipendenze al modulo dell'app in Gradle:
compila "android.arch.persistence.room:runtime:1.0.0" annotationProcessor "android.arch.persistence.room:compiler:1.0.0"
Se stai usando Kotlin, devi applicare il Kapt
plugin e aggiungi un'altra dipendenza.
applica il plugin: "kotlin-kapt" // ... dependencies // ... kapt "android.arch.persistence.room:compiler:1.0.0"
Un Entità rappresenta l'oggetto che viene salvato nel database. Ogni Entità
class crea una nuova tabella di database, con ogni campo che rappresenta una colonna. Le annotazioni vengono utilizzate per configurare le entità e il loro processo di creazione è molto semplice. Nota quanto è semplice impostare un Entità
usando le classi di dati di Kotlin.
@Entity data class Nota (@PrimaryKey (autoGenerate = true) var id: Long ?, var text: String ?, var data: Long?)
Una volta che una classe è annotata con @Entità
, la libreria Room creerà automaticamente una tabella utilizzando i campi della classe come colonne. Se devi ignorare un campo, annota semplicemente con @Ignorare
. Ogni Entità
inoltre deve definire a @Chiave primaria
.
Room userà la classe e i suoi nomi di campo per creare automaticamente una tabella; tuttavia, è possibile personalizzare la tabella generata. Per definire un nome per la tabella, utilizzare il tableName
opzione sul @Entità
annotazione e per modificare il nome delle colonne, aggiungere a @ColumnInfo
annotazione con l'opzione nome sul campo. È importante ricordare che i nomi delle tabelle e delle colonne sono case sensitive.
@Entity (tableName = "tb_notes") classe dati Nota (@PrimaryKey (autoGenerate = true) @ColumnInfo (name = "_id") var id: Long ?, // ...)
Ci sono alcuni vincoli SQLite utili che Room ci consente di implementare facilmente sulle nostre entità. Per velocizzare le query di ricerca, è possibile creare SQLite indici nei campi che sono più rilevanti per tali query. Gli indici renderanno le query di ricerca molto più veloci; tuttavia, renderanno anche le operazioni di inserimento, eliminazione e aggiornamento più lente, quindi è necessario utilizzarle con attenzione. Dai un'occhiata alla documentazione di SQLite per capirli meglio.
Esistono due modi diversi per creare indici in Room. Puoi semplicemente impostare il ColumnInfo
proprietà, indice
, a vero
, lasciare Room imposta gli indici per te.
@ColumnInfo (name = "date", index = true) var data: Long
Oppure, se hai bisogno di più controllo, usa il indici
proprietà del @Entità
annotazione, elencando i nomi dei campi che devono comporre l'indice nel valore
proprietà. Si noti che l'ordine degli articoli in valore
è importante poiché definisce l'ordinamento della tabella degli indici.
@Entity (tableName = "tb_notes", indices = arrayOf (Index (value = * arrayOf ("date", "title"), name = "idx_date_title")))
Un altro vincolo SQLite utile è unico
, che impedisce al campo contrassegnato di avere valori duplicati. Sfortunatamente, nella versione 1.0.0, Room non fornisce questa proprietà come dovrebbe, direttamente nel campo entità. Ma puoi creare un indice e renderlo unico, ottenendo un risultato simile.
@Entity (tableName = "tb_users", indices = arrayOf (Index (value = "username", name = "idx_username", unique = true))))
Altri vincoli come NON NULLO
, PREDEFINITO
, e DAI UN'OCCHIATA
non sono presenti in sala (almeno fino ad ora, nella versione 1.0.0), ma è possibile creare la propria logica sull'entità per ottenere risultati simili. Per evitare valori nulli su entità Kotlin, basta rimuovere il ?
alla fine del tipo di variabile o, in Java, aggiungi il @NonNull
annotazione.
A differenza della maggior parte delle librerie di mapping relazionali agli oggetti, Room non consente a un'entità di fare direttamente riferimento ad un'altra. Ciò significa che se hai un'entità chiamata Bloc notes
e uno chiamato Nota
, non puoi creare un Collezione
di Nota
s dentro il Bloc notes
come faresti con molte librerie simili. Inizialmente, questa limitazione può sembrare fastidiosa, ma è stata una decisione progettuale di adattare la libreria Room ai limiti di architettura di Android. Per comprendere meglio questa decisione, dai un'occhiata alla spiegazione di Android per il loro approccio.
Anche se la relazione oggettuale di Room è limitata, esiste ancora. Usando chiavi esterne, è possibile fare riferimento a oggetti genitore e figlio e mettere in cascata le loro modifiche. Si noti che è anche consigliabile creare un indice sull'oggetto figlio per evitare scansioni complete della tabella quando il genitore viene modificato.
@Entity (tableName = "tb_notes", indices = arrayOf (Index (value = * arrayOf ("note_date", "note_title"), name = "idx_date_title"), Index (value = * arrayOf ("note_pad_id"), name = "idx_pad_note")), foreignKeys = arrayOf (ForeignKey (entity = NotePad :: class, parentColumns = arrayOf ("pad_id"), childColumns = arrayOf ("note_pad_id"), onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE)) ) classe dati Nota (@PrimaryKey (autoGenerate = true) @ColumnInfo (name = "note_id") var id: Long, @ColumnInfo (name = "note_title") var title: String ?, @ColumnInfo (name = "testo_note") var text: String, @ColumnInfo (name = "note_date") var data: Long, @ColumnInfo (name = "note_pad_id") var padId: Long)
È possibile incorporare oggetti all'interno di entità usando il @Inserito
annotazione. Una volta incorporato un oggetto, tutti i relativi campi verranno aggiunti come colonne nella tabella dell'entità, utilizzando i nomi dei campi dell'oggetto incorporato come nomi di colonne. Considera il seguente codice.
classe data Location (var lat: Float, var lon: Float) @Entity (tableName = "tb_notes") classe dati Nota (@PrimaryKey (autoGenerate = true) @ColumnInfo (name = "note_id") var id: Long, @Embedded (prefix = "note_location_") var posizione: posizione?)
Nel codice sopra, il Posizione
la classe è incorporata nel Nota
entità. La tabella dell'entità avrà due colonne aggiuntive, corrispondenti ai campi dell'oggetto incorporato. Dal momento che stiamo usando la proprietà prefisso sul @Inserito
annotazione, i nomi delle colonne saranno 'note_location_lat
' e 'note_location_lon
', e sarà possibile fare riferimento a quelle colonne nelle query.
Per accedere ai database della stanza, è necessario un oggetto DAO. Il DAO può essere definito come un'interfaccia o una classe astratta. Per implementarlo, annota la classe o l'interfaccia con @Dao
e sei bravo ad accedere ai dati. Anche se è possibile accedere a più di una tabella da un DAO, è consigliabile, in nome di una buona architettura, mantenere il principio di Separazione dei dubbi e creare un DAO responsabile dell'accesso a ciascuna entità.
@Dao NoteDAO
Room offre una serie di annotazioni utili per le operazioni CRUD nel DAO: @Inserire
, @Aggiornare
, @Elimina
, e @query
. Il @Inserire
operazione può ricevere una singola entità, a schieramento
, o a Elenco
di entità come parametri. Per singole entità, può restituire a lungo
, che rappresenta la riga dell'inserzione. Per più entità come parametri, può restituire a lungo[]
o a Elenco
anziché.
@Insert (onConflict = OnConflictStrategy.REPLACE) fun insertNote (nota: Nota): Long @Insert (onConflict = OnConflictStrategy.ABORT) fun insertNotes (note: Elenco): Elenco
Come puoi vedere, c'è un'altra proprietà di cui parlare: onConflict
. Questo definisce la strategia da seguire in caso di conflitti usando OnConflictStrategy
costanti. Le opzioni sono praticamente auto-esplicative, con ABORT
, FALLIRE
, e SOSTITUIRE
essendo le possibilità più significative.
Per aggiornare le entità, utilizzare il @Aggiornare
annotazione. Segue lo stesso principio di @Inserire
, ricevere singole entità o più entità come argomenti. Room userà l'entità ricevente per aggiornare i suoi valori, usando l'entità Chiave primaria
come riferimento. comunque, il @Aggiornare
può solo restituire un int
che rappresenta il totale delle righe della tabella aggiornate.
@Update () funNote update (nota: Nota): Int
Di nuovo, seguendo lo stesso principio, il @Elimina
l'annotazione può ricevere entità singole o multiple e restituire un int
con il totale delle righe della tabella aggiornate. Utilizza anche l'entità Chiave primaria
per trovare e rimuovere il registro nella tabella del database.
@Delete fun deleteNote (nota: Nota): Int
Finalmente, il @query
annotazione rende consultazioni nel database. Le query sono costruite in modo simile alle query SQLite, con la più grande differenza nella possibilità di ricevere argomenti direttamente dai metodi. Ma la caratteristica più importante è che le query vengono verificate in fase di compilazione, il che significa che il compilatore troverà un errore non appena si costruisce il progetto.
Per creare una query, annotare un metodo con @query
e scrivere una query SQLite come valore. Non presteremo troppa attenzione a come scrivere le query poiché utilizzano lo standard SQLite. Ma generalmente, userete le query per recuperare i dati dal database usando il SELEZIONARE
comando. Le selezioni possono restituire valori singoli o di raccolta.
@Query ("SELECT * FROM tb_notes") fun findAllNotes (): List
È davvero semplice passare i parametri alle query. Room dedurrà il nome del parametro, usando il nome dell'argomento del metodo. Per accedervi, utilizzare :
, seguito dal nome.
@Query ("SELECT * FROM tb_notes WHERE note_id =: id") fun findNoteById (id: Long): Note @Query ("SELECT * FROM tb_noted WHERE note_date BETWEEN: early AND: late") fun findNoteByDate (early: Data, late : Data): elenco
La stanza è stata progettata per funzionare con grazia LiveData
. Per un @query
restituire a LiveData
, semplicemente concludere il rendimento standard con LiveData
>
e sei a posto.
@Query ("SELECT * FROM tb_notes WHERE note_id =: id") fun findNoteById (id: Long): LiveData
Dopodiché, sarà possibile osservare il risultato della query e ottenere risultati asincroni abbastanza facilmente. Se non conosci la potenza di LiveData, prenditi un po 'di tempo per leggere il nostro tutorial sul componente.
Il database è creato da una classe astratta, annotata con @Banca dati
ed estendendo il RoomDatabase
classe. Inoltre, le entità che saranno gestite dal database devono essere passate in un array nel entità
proprietà nel @Banca dati
annotazione.
@Database (entities = arrayOf (NotePad :: class, Note :: class)) abstract class Database: RoomDatabase () abstract fun padDAO (): PadDAO abstract fun noteDAO (): NoteDAO
Una volta implementata la classe del database, è tempo di costruire. È importante sottolineare che l'istanza del database dovrebbe idealmente essere costruita solo una volta per sessione, e il modo migliore per ottenere ciò sarebbe utilizzare un sistema di iniezione delle dipendenze, come Dagger. Tuttavia, non ci tufferemo in DI ora, dal momento che è al di fuori dello scopo di questo tutorial.
divertimento fornisceAppDatabase (): Database return Room.databaseBuilder (context, Database :: class.java, "database") .build ()
Normalmente, le operazioni su un database Room non possono essere create dal thread UI, poiché stanno bloccando e probabilmente creeranno problemi per il sistema. Tuttavia, se si desidera forzare l'esecuzione sul thread dell'interfaccia utente, aggiungere allowMainThreadQueries
alle opzioni di costruzione. In effetti, ci sono molte opzioni interessanti su come costruire il database, e ti consiglio di leggere il RoomDatabase.Builder
documentazione per capire le possibilità.
Una colonna Datatype viene automaticamente definita da Room. Il sistema dedurrà dal tipo di campo quale tipo di SQLite Datatype è più adeguato. Tieni presente che la maggior parte del POJO di Java verrà convertito immediatamente; tuttavia, è necessario creare convertitori di dati per gestire oggetti più complessi non riconosciuti automaticamente da Room, come ad esempio Data
e enum
.
Per Room capire le conversioni dei dati, è necessario fornire TypeConverters
e registra quei convertitori in Room. È possibile effettuare questa registrazione tenendo conto del contesto specifico, ad esempio, se si registra il TypeConverter
nel Banca dati
, tutte le entità del database utilizzeranno il convertitore. Se ti registri su un'entità, solo le proprietà di tale entità possono utilizzarla e così via.
Per convertire un Data
oggetto direttamente su a Lungo
durante le operazioni di salvataggio di Room e quindi convertire a Lungo
a a Data
consultando il database, dichiarare prima a TypeConverter
.
class DataConverters @TypeConverter fun fromTimestamp (mills: Long?): Date? return if (mills == null) null else Date (mills) @TypeConverter fun fromDate (date: Date?): Long? = data? .time
Quindi, registrare il TypeConverter
nel Banca dati
, o in un contesto più specifico, se vuoi.
@Database (entities = arrayOf (NotePad :: class, Note :: class), version = 1) @TypeConverters (DataConverters :: class) classe astratta Database: RoomDatabase ()
L'applicazione che abbiamo sviluppato durante questa serie è stata utilizzata SharedPreferences
per memorizzare i dati meteo. Ora che sappiamo come usare Room, lo useremo per creare una cache più sofisticata che ci consentirà di ottenere dati memorizzati in cache per città, e prendere in considerazione anche la data meteorologica durante il recupero dei dati.
Per prima cosa, creiamo la nostra entità. Salveremo tutti i nostri dati usando solo il WeatherMain
classe. Abbiamo solo bisogno di aggiungere alcune annotazioni alla classe, e abbiamo finito.
@Entity (tableName = "weather") classe dati WeatherMain (@ColumnInfo (name = "date") var dt: Long ?, @ColumnInfo (name = "city") var nome: String ?, @ColumnInfo (name = "temp_min ") var tempMin: Double ?, @ColumnInfo (name =" temp_max ") var tempMax: Double ?, @ColumnInfo (name =" main ") var main: String ?, @ColumnInfo (name =" description ") var descrizione: String ?, @ColumnInfo (name = "icon") icona var: String?) @ColumnInfo (name = "id") @PrimaryKey (autoGenerate = true) var id: Long = 0 // ...
Abbiamo anche bisogno di un DAO. Il WeatherDAO
gestirà le operazioni CRUD nella nostra entità. Si noti che tutte le query stanno tornando LiveData
.
@Dao WeatherDAO @Insert (onConflict = OnConflictStrategy.REPLACE) fun insert (w: WeatherMain) @Delete fun remove (w: WeatherMain) @Query ("SELECT * FROM weather" + "ORDINE BY id DESC LIMIT 1") divertimento findLast (): LiveData@Query ("SELECT * FROM weather" + "WHERE city LIKE: city" + "ORDER BY date DESC LIMIT 1") fun findByCity (città: String): LiveData @Query ("SELECT * FROM weather" + "WHERE date < :date " + "ORDER BY date ASC LIMIT 1" ) fun findByDate( date: Long ): List
Infine, è il momento di creare il Banca dati
.
@Database (entities = arrayOf (WeatherMain :: class), version = 2) abstract class Database: RoomDatabase () abstract fun weatherDAO (): WeatherDAO
Ok, ora abbiamo configurato il nostro database Room. Tutto ciò che resta da fare è collegarlo Pugnale
e inizia a usarlo. Nel DataModule
, forniamo il Banca dati
e il WeatherDAO
.
@Module class DataModule (val context: Context) // ... @Provides @Singleton fun fornisceAppDatabase (): Database return Room.databaseBuilder (context, Database :: class.java, "database") .allowMainThreadQueries () .fallbackToDestructiveMigration ( ) .build () @Provides @Singleton fun fornisceWeatherDAO (database: Database): WeatherDAO return database.weatherDAO ()
Come dovresti ricordare, abbiamo un repository responsabile della gestione di tutte le operazioni sui dati. Continuiamo a utilizzare questa classe per la richiesta di dati Room della app. Ma prima, dobbiamo modificare il providesMainRepository
metodo del DataModule
, includere il WeatherDAO
durante la costruzione della classe.
@Module class DataModule (val context: Context) // ... @Provides @Singleton fun providesMainRepository (openWeatherService: OpenWeatherService, prefsDAO: PrefsDAO, weatherDAO: WeatherDAO, locationLiveData: LocationLiveData): MainRepository return MainRepository (openWeatherService, prefsDAO, weatherDAO, locationLiveData ) / ...
La maggior parte dei metodi che aggiungeremo al MainRepository
sono piuttosto semplici. Vale la pena guardare più da vicino clearOldData ()
, anche se. Questo cancella tutti i dati più vecchi di un giorno, mantenendo solo i dati meteorologici rilevanti salvati nel database.
class MainRepository @Inject constructor (private val openWeatherService: OpenWeatherService, private val prefsDAO: PrefsDAO, private val weatherDAO: WeatherDAO, location val privata: LocationLiveData): AnkoLogger fun getWeatherByCity (città: String): LiveData> info ("getWeatherByCity: $ city") restituisce openWeatherService.getWeatherByCity (città) fun saveOnDb (weatherMain: WeatherMain) info ("saveOnDb: \ n $ weatherMain") weatherDAO.insert (weatherMain) fun getRecentWeather (): LiveData info ("getRecentWeather") return weatherDAO.findLast () fun getRecentWeatherForLocation (location: String): LiveData info ("getWeatherByDateAndLocation") restituisce weatherDAO.findByCity (posizione) fun clearOldData () info ("clearOldData") val c = Calendar.getInstance () c.add (Calendar.DATE, -1) // ottieni dati meteo da 2 giorni fa val oldData = weatherDAO.findByDate (c.timeInMillis) oldData.forEach w -> informazioni ("Rimozione dati per '$ w.name': $ w.dt") weatherDAO.remove (w ) // ...
Il MainViewModel
è responsabile per fare consultazioni al nostro deposito. Aggiungiamo un po 'di logica per indirizzare le nostre operazioni al database Room. Innanzitutto, aggiungiamo a MutableLiveData
, il weatherDB
, che è responsabile per la consultazione del MainRepository
. Quindi rimuoviamo i riferimenti a SharedPreferences
, fare in modo che la nostra cache si basi solo sul database Room.
class MainViewModel @Inject constructor (private val repository: MainRepository): ViewModel (), AnkoLogger // ... // Meteo salvato sul database private var weatherDB: LiveData= MutableLiveData () // ... // Rimuoviamo la consultazione a SharedPreferences // rendendo la cache esclusiva per il divertimento privato di Room getWeatherCached () info ("getWeatherCached") weatherDB = repository.getRecentWeather () weather.addSource (weatherDB, w -> info ("weatherDB: DB: \ n $ w") weather.postValue (ApiResponse (data = w)) weather.removeSource (weatherDBSaved))
Per rendere la cache pertinente, cancelleremo i vecchi dati ogni volta che viene effettuata una nuova consultazione meteorologica.
private var weatherByLocationResponse: LiveData> = Transformations.switchMap (posizione, l -> informazioni ("weatherByLocation: \ nlocation: $ l") doAsync repository.clearOldData () return @ switchMap repository.getWeatherByLocation (l)) private var weatherByCityResponse: LiveData > = Transformations.switchMap (cityName, city -> info ("weatherByCityResponse: city: $ city") doAsync repository.clearOldData () return @ switchMap repository.getWeatherByCity (city))
Infine, salveremo i dati nel database Room ogni volta che si riceve un nuovo meteo.
// Riceve risposta meteo aggiornata, // invia all'interfaccia utente e salva anche l'aggiornamento fun privatoWeather (w: WeatherResponse) info ("updateWeather") // ottenere il tempo da oggi val weatherMain = WeatherMain.factory (w) // save sulle preferenze condivise repository.saveWeatherMainOnPrefs (weatherMain) // salva su db repository.saveOnDb (weatherMain) // aggiorna il valore meteo weather.postValue (ApiResponse (data = weatherMain))
Puoi vedere il codice completo nel repository GitHub per questo post.
Infine, siamo alla conclusione della serie di componenti di architettura Android. Questi strumenti saranno eccellenti compagni nel tuo percorso di sviluppo Android. Ti consiglio di continuare ad esplorare i componenti. Prova a prendere del tempo per leggere la documentazione.
E controlla alcuni dei nostri altri post sullo sviluppo di app Android qui su Envato Tuts+!