Componenti di architettura Android la libreria di persistenza delle stanze

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.

1. Il componente stanza

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.

  • Entity è la classe che viene salvata nel Database. Viene creata una tabella di database esclusiva per ogni classe annotata con @Entità.
  • Il DAO è l'interfaccia annotata con @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.
  • Il componente Database è una classe astratta annotata con @Banca dati, che si estende RoomDatabase. La classe definisce la lista di Entità e i suoi DAO.

2. Impostazione dell'ambiente

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"

3. Entity, la tabella del database

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.

Tabella e colonne

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 ?, // ...) 

Indici e vincoli di unicità

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.

Relazione tra gli oggetti

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 Notas 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)

Incorporare oggetti

È 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.

4. Oggetto di accesso ai dati

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 

Inserisci, Aggiorna e Elimina

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

Fare domande

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

Query LiveData

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.

5. Creazione del database

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à.

6. Datatype and Data Conversion

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 ()

7. Utilizzo di Room in un'app

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.

Conclusione

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+!