Componente de arhitectură Android Biblioteca pentru persistența camerei

În acest articol final al seriei Android Architecture Components, vom explora biblioteca Persistență cameră, o resursă excelentă care face mult mai ușor să lucreze cu bazele de date din Android. Acesta oferă un strat de abstractizare peste SQLite, interogări SQL verificate în timp util, precum și interogări asincrone și observabile. Camera efectuează operații baze de date pe Android la un alt nivel.

Deoarece aceasta este a patra parte a seriei, presupun că sunteți familiarizat cu conceptele și componentele pachetului de arhitectură, cum ar fi LiveData și LiveModel. Cu toate acestea, dacă nu ați citit nici unul din ultimele trei articole, veți putea totuși să le urmați. Totuși, dacă nu știți prea multe despre aceste componente, luați ceva timp pentru a citi seria - vă puteți bucura.

1. Componenta camerei

Așa cum am menționat, Camera nu este un nou sistem de baze de date. Este un strat abstract care împachetează baza de date standard SQLite adoptată de Android. Cu toate acestea, Room adaugă atât de multe caracteristici SQLite că este aproape imposibil de recunoscut. Camera simplifică toate operațiile bazate pe baze de date și, de asemenea, le face mult mai puternice, deoarece permite posibilitatea returnării observabilelor și interogărilor SQL verificate în timpul compilării.

Camera este compusă din trei componente principale: Bază de date, DAO (Obiecte de acces de date) și Entitate. Fiecare componentă are responsabilitatea sa și toate acestea trebuie implementate pentru ca sistemul să funcționeze. Din fericire, o astfel de implementare este destul de simplă. Datorită adnotărilor furnizate și a claselor abstracte, boilerplate-ul pentru implementarea camerei este menținut la minimum.

  • Entitatea este clasa care este salvată în baza de date. Pentru fiecare clasă adnotată este creată o tabelă exclusivă de baze de date @Entitate.
  • DAO este interfața adnotată cu @Dao care mediază accesul la obiecte din baza de date și din tabelele sale. Există patru adnotări specifice pentru operațiile DAO de bază: @Introduce, @Actualizați, @Șterge, și @Query.
  • Componenta bazei de date este o clasă abstractă adnotată cu @Bază de date, care se extinde RoomDatabase. Clasa definește lista Entităților și a DAO-urilor.

2. Configurarea mediului

Pentru a utiliza Room, adăugați următoarele dependențe la modulul app din Gradle:

compilați "android.arch.persistence.room:runtime:1.0.0" annotationProcessor "șiroid.arch.persistence.room:compiler:1.0.0"

Dacă utilizați Kotlin, trebuie să aplicați kapt plugin și adăugați o altă dependență.

aplicați pluginul: "klin-kapt" // ... dependencies // ... kapt "android.arch.persistence.room:compiler:1.0.0"

3. Entitatea, tabela de baze de date

Un Entitate reprezintă obiectul care este salvat în baza de date. Fiecare Entitate clasa creează un nou tabel de bază de date, fiecare câmp reprezentând o coloană. Adnotările sunt folosite pentru a configura entități, iar procesul lor de creare este foarte simplu. Observați cât de simplu este să configurați un Entitate utilizând clasele de date Kotlin.

@Entity class data Notă (@PrimaryKey (autoGenerate = true) var id: Long ?, var text: String ?, var data: Long?)

Odată ce o clasă este adnotată cu @Entitate, biblioteca de cameră va crea automat o tabelă utilizând câmpurile de clasă ca coloane. Dacă trebuie să ignorați un câmp, trebuie doar să îl adnotați @Ignora. Fiecare Entitate trebuie să definească și a @Cheia principala.

Tabel și coloane

Camera va folosi clasa și numele câmpului pentru a crea automat un tabel; cu toate acestea, puteți personaliza tabelul care este generat. Pentru a defini un nume pentru tabel, utilizați tableName opțiune pe @Entitate adnotare, și pentru a edita numele coloanelor, adăugați a @ColumnInfo adnotare cu opțiunea de nume pe câmp. Este important să rețineți că numele tabelei și coloanelor sunt sensibile la minuscule.

@Normal (tableName = "tb_notes") clasa de date Notă (@PrimaryKey (autoGenerate = true) @ColumnInfo (nume = "_id") var id: Long ?, // ...) 

Indici și constrângeri de unicitate

Există câteva constrângeri SQLite utile pe care Camera ne permite să le implementăm cu ușurință pe entitățile noastre. Pentru a accelera interogările de căutare, puteți crea SQLite indicii la domeniile care sunt mai relevante pentru astfel de interogări. Indicii vor face interogările de căutare mult mai rapide; cu toate acestea, acestea vor face inserarea, ștergerea și actualizarea interogărilor mai lent, deci trebuie să le folosiți cu atenție. Uitați-vă la documentația SQLite pentru a le înțelege mai bine.

Există două moduri diferite de a crea indicii în cameră. Puteți seta pur și simplu ColumnInfo proprietate, index, la Adevărat, lăsând Room să-ți stabilească indicii.

@ColumnInfo (nume = "data", index = true) var data: Long

Sau, dacă aveți nevoie de mai mult control, utilizați indicii proprietate a @Entitate adnotare, care enumeră numele câmpurilor care trebuie să compună indicele în valoare proprietate. Observați că ordinea articolelor din valoare este importantă deoarece definește sortarea tabelei index.

@Entity (tableName = "tb_notes", indices = arrayOf (index = value = arrayOf ("date", "title" 

O altă constrângere SQLite utilă este unic, care interzice campului marcat să aibă valori duplicate. Din păcate, în versiunea 1.0.0, Camera nu oferă această proprietate așa cum ar trebui, direct pe domeniul entității. Dar puteți crea un index și îl puteți face unic, obținând un rezultat similar.

@Entity (tableName = "tb_users", indices = arrayOf (index (value = "username", nume = "idx_username", unic = 

Alte constrângeri cum ar fi NU NUL, MOD IMPLICIT, și VERIFICA nu sunt prezente în Cameră (cel puțin până acum, în versiunea 1.0.0), dar puteți crea propria logică asupra entității pentru a obține rezultate similare. Pentru a evita valorile null în entitățile Kotlin, trebuie doar să eliminați ? la sfârșitul tipului de variabilă sau, în Java, adăugați @NonNull adnotare.

Relația dintre obiecte

Spre deosebire de cele mai multe biblioteci de mapare obiect-relațională, Camera nu permite unei entități să facă referire directă la o altă entitate. Aceasta înseamnă că dacă aveți o entitate chemată Bloc Notes și unul a sunat Notă, nu puteți crea un Colectie de Notăs în interiorul Bloc Notes așa cum ați face cu multe biblioteci similare. La început, această limitare poate părea enervantă, însă a fost o decizie de proiectare pentru a adapta biblioteca camerei la limitările arhitecturii Android. Pentru a înțelege mai bine această decizie, aruncăm o privire la explicația Android pentru abordarea lor.

Chiar dacă relația obiectului Room este limitată, ea încă mai există. Utilizând chei străine, este posibil să trimiteți obiecte părinte și copil și să le modificați în mod automat. Observați că este, de asemenea, recomandat să creați un index pe obiectul copil pentru a evita scanarea completă a mesei atunci când părintele este modificat.

@Intitul (tableName = "tb_notes", indices = arrayOf (index = value = arrayOf ("note_date", "note_title"), name = "idx_date_title" "idx_pad_note")), foreignKeys = arrayOf (ForeignKey (entită = NotePad :: clasă, parentColumns = arrayOf ("pad_id"), childColumns = arrayOf ("note_pad_id"), onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE)) ) clase de date Notă (@PrimaryKey (autoGenerate = true) @ColumnInfo (nume = "note_id") var id: Long, @ColumnInfo (name = "note_title") var titlu: String ?, @ColumnInfo var text: String, @ColumnInfo (nume = "notă_date") var dată: Long, @ColumnInfo (nume = "note_pad_id") var padId: Long)

Încărcarea obiectelor

Este posibil să încorporați obiecte în interiorul entităților folosind @Încorporat adnotare. Odată ce un obiect este încorporat, toate câmpurile acestuia vor fi adăugate ca coloane în tabelul entității, utilizând numele câmpurilor obiectului încorporat ca nume de coloane. Luați în considerare următorul cod.

clasa de date Locul de amplasare (var lat: float, var lon: Float) @Entity (tableName = "tb_notes") clasa de date Notă (@PrimaryKey (autoGenerate = true) @ColumnInfo (nume = "note_id") var id: Long, @Embedded (prefix = "note_location_") var locație: Locație?)

În codul de mai sus, Locație clasa este încorporată în Notă entitate. Tabelul entității va avea două coloane suplimentare, corespunzătoare câmpurilor obiectului încorporat. Deoarece folosim proprietatea prefixului pe @Încorporat adnotări, numele coloanelor vor fi "note_location_lat' și 'note_location_lon"și va fi posibilă trimiterea acestor coloane în interogări.

4. Obiect de acces la date

Pentru a accesa bazele de date ale camerei, este necesar un obiect DAO. DAO poate fi definită fie ca interfață, fie ca clasă abstractă. Pentru ao implementa, adnotați clasa sau interfața cu @Dao și sunteți bine să accesați datele. Chiar dacă este posibil să se acceseze mai mult de un tabel dintr-un DAO, este recomandat, în numele unei arhitecturi bune, să se mențină principiul separării preocupărilor și să se creeze un DAO responsabil pentru accesarea fiecărei entități.

Interfața @Dao NoteDAO 

Introduceți, Actualizați și Ștergeți

Camera oferă o serie de adnotări convenabile pentru operațiile CRUD din DAO: @Introduce, @Actualizați, @Șterge, și @Query. @Introduce operațiunea poate primi o singură entitate, o mulțime, sau a Listă de entități ca parametri. Pentru entitățile unice, se poate întoarce a lung, reprezentând rândul inserției. Pentru mai multe entități ca parametri, se poate întoarce a lung[] sau a Listă in schimb.

@Insert (onConflict = OnConflictStrategy.REPLACE) distracție insertNote (notă: notă): Long @Insert (onConflict = OnConflictStrategy.ABORT) distracție insertNotes (note: Lista): Listă

După cum puteți vedea, există o altă proprietate pentru a vorbi despre: onConflict. Aceasta definește strategia de urmat în cazul utilizării de conflicte OnConflictStrategy constante. Opțiunile sunt destul de explicative, cu ABORT, FAIL, și A INLOCUI fiind posibilitățile mai importante.

Pentru a actualiza entitățile, utilizați @Actualizați adnotare. Se aplică același principiu ca și @Introduce, primind entități unice sau mai multe entități ca argumente. Cameră va utiliza entitatea primitoare pentru a actualiza valorile sale, utilizând entitatea Cheia principala ca referinta. Însă @Actualizați poate returna doar un int reprezentând totalul rândurilor de tabel actualizate.

@ Update () fun updateNote (notă: Notă): Int

Din nou, urmând același principiu, @Șterge adnotarea poate primi entități unice sau multiple și să returneze o int cu actualizarea totală a rândurilor de tabelă. De asemenea, utilizează entitatea entității Cheia principala pentru a găsi și elimina registrul în tabelul bazei de date.

@Delete fun deleteNote (notă: notă): Int

Efectuarea de interogări

În cele din urmă, @Query adnotarea face consultări în baza de date. Interogările sunt construite în mod similar cu interogările SQLite, cea mai mare diferență fiind posibilitatea de a primi argumente direct din metode. Dar cea mai importantă caracteristică este că interogările sunt verificate la momentul compilării, ceea ce înseamnă că compilatorul va găsi o eroare imediat ce construiți proiectul.

Pentru a crea o interogare, adnotați o metodă cu @Query și scrieți o interogare SQLite ca valoare. Nu vom acorda prea multă atenție modului de scriere a interogărilor, deoarece utilizează standardul SQLite. În general, veți utiliza interogări pentru a prelua date din baza de date folosind SELECTAȚI comanda. Selecțiile pot întoarce valori unice sau de colectare.

@Query ("SELECT * FROM tb_notes") distracție findAllNotes (): Listă

Este foarte simplu să transmiteți parametrii la interogări. Cameră va deduce numele parametrului, folosind numele argumentului metodei. Pentru ao accesa, utilizați :, urmată de numele.

@Query ("SELECT * FROM tb_notes WHERE note_id =: id") distracție findNoteById (id: Long): Notă @Query (devreme, : Data): Listă

Cereri LiveData

Camera a fost proiectată să lucreze cu grație LiveData. Pentru o @Query pentru a returna a LiveData, doar încheiați întoarcerea standard cu LiveData și ești bine să pleci.

@Query ("SELECT * FROM tb_notes WHERE note_id =: id") distracție findNoteById (id: Long): LiveData

După aceasta, va fi posibil să observați rezultatul interogării și să obțineți rezultate asincrone destul de ușor. Dacă nu cunoașteți puterea LiveData, faceți ceva timp pentru a citi instrucțiunile despre componente.

5. Crearea bazei de date

Baza de date este creată de o clasă abstractă, adnotată cu @Bază de date și extinderea RoomDatabase clasă. De asemenea, entitățile care vor fi gestionate de baza de date trebuie să fie transmise într-o matrice din entități proprietate în @Bază de date adnotare.

@Database (entities = arrayOf (NotePad :: clasă, Notă :: clasă)) clasă abstractă Baza de date: RoomDatabase () abstract fun padDAO ()

După implementarea clasei bazei de date, este timpul să construim. Este important să subliniem că instanța bazei de date ar trebui construită în mod ideal o singură dată pe sesiune, iar cel mai bun mod de a realiza acest lucru ar fi utilizarea unui sistem de injecție de dependență, cum ar fi Dagger. Cu toate acestea, nu ne vom scufunda în DI acum, deoarece este în afara scopului acestui tutorial.

distracție oferăAppDatabase (): Baza de date return Room.databaseBuilder (context, Database :: class.java, "database") .build ()

În mod normal, operațiile din baza de date a camerei nu pot fi realizate din fila UI, deoarece acestea blochează și probabil vor crea probleme pentru sistem. Cu toate acestea, dacă doriți să forțați execuția pe fila UI, adăugați allowMainThreadQueries la opțiunile de construire. De fapt, există multe opțiuni interesante pentru cum să construiți baza de date și vă sfătuiesc să citiți RoomDatabase.Builder pentru a înțelege posibilitățile.

6. Tipul de date și conversia datelor

O coloană Datatype este definită automat de Room. Sistemul va deduce din tipul câmpului tipul de tip SQLite Datatype mai adecvat. Rețineți că cea mai mare parte a POJO-ului Java va fi transformată din cutie; cu toate acestea, este necesar să creați convertoare de date pentru a gestiona obiecte mai complexe, care nu sunt recunoscute de cameră în mod automat, cum ar fi Data și enum.

Pentru ca Room să înțeleagă conversiile de date, este necesar să le oferiți TypeConverters și înregistrați acei convertori în cameră. Este posibil ca această înregistrare să țină cont de contextul specific - de exemplu, dacă înregistrați TypeConverter în Bază de date, toate entitățile din baza de date vor folosi convertorul. Dacă vă înregistrați într-o entitate, numai proprietățile acelei entități o pot folosi și așa mai departe.

Pentru a converti a Data obiecteaza direct la a Lung în timpul operațiunilor de salvare ale camerei și apoi convertiți a Lung la a Data la consultarea bazei de date, declarați mai întâi a TypeConverter.

Class DataConverters @TypeConverter distracție de la Timestamp (mori: Long?): Data? return if (mills == null) null otherwise Data (mori) @TypeConverter fun fromDate (data: Data?): Long? = data? .time

Apoi, înregistrați TypeConverter în Bază de date, sau într-un context mai specific, dacă doriți.

@Database (entități = arrayOf (NotePad :: clasă, Notă :: clasă), version = 1) @TypeConverters (DataConverters :: class) clasă abstractă Baza de date: RoomDatabase

7. Utilizarea camerei într-o aplicație

Aplicația pe care am dezvoltat-o ​​în această serie a fost utilizată SharedPreferences pentru a memora datele meteo. Acum, când știm cum să folosim Room, o vom folosi pentru a crea o memorie cache mai sofisticată care ne va permite să obținem datele memorate în cache de către oraș și să luăm în considerare, de asemenea, data de vreme în timpul preluării datelor.

Mai întâi, să creăm entitatea noastră. Vom salva toate datele folosind doar WeatherMain clasă. Trebuie doar să adăugăm câteva adnotări la clasă și am terminat.

@ColumnInfo (nume = "oraș") var nume: String ?, @ColumnInfo (name = "temp_min") "var tempMin: Dublu ?, @ColumnInfo (nume =" temp_max ") var tempMax: Dublu ?, @ColumnInfo (nume =" principal " String ?, @ColumnInfo (nume = "icon") var icon: String?) @ColumnInfo (nume = "id") @PrimaryKey (autoGenerate = true) var id: Long = 0 // ... 

Avem de asemenea nevoie de un DAO. WeatherDAO va gestiona operațiunile CRUD în entitatea noastră. Observați că toate interogările se întorc LiveData.

@Dao interfață WeatherDAO @Insert (onConflict = OnConflictStrategy.REPLACE) inserați distracție (w: WeatherMain) @Delete fun remove (w: WeatherMain) @Query ("SELECT * FROM weather" + "ORDER BY ID DESC LIMIT 1" findLast (): LiveData @Query ("SELECT * FROM weather" + "WHERE oraș asemănător: oraș" + "ORDER BY date DESC LIMIT 1") distracție findByCity (oraș: String) @Query ("SELECT * FROM weather" + "WHERE data < :date " + "ORDER BY date ASC LIMIT 1" ) fun findByDate( date: Long ): List 

În cele din urmă, este timpul să creați Bază de date.

@Database (entities = arrayOf (WeatherMain :: class), version = 2) clasă abstractă Baza de date: RoomDatabase () abstract weather weatherDAO (): WeatherDAO

Ok, acum avem configurația bazei noastre de date Room. Tot ce trebuie lăsat să faceți este să vă conectați Pumnal și începeți să-l utilizați. În DataModule, să oferim Bază de date si WeatherDAO.

@Module class DataModule (contextul val: Context) // ... @Provides @Singleton distracție furnizeazăAppDatabase (): Baza de date return Room.databaseBuilder (context, Database :: class.java, "database") .allowMainThreadQueries () .fallbackToDestructiveMigration ) .build () @Provide @Singleton distracție oferăWeatherDAO (bază de date: Bază de date): WeatherDAO return database.weatherDAO ()

După cum trebuie să vă amintiți, avem un depozit responsabil pentru manipularea tuturor operațiunilor de date. Să continuăm să folosim această clasă pentru solicitarea de date a camerei Apps. Dar, mai întâi, trebuie să editați providesMainRepository metodă a DataModule, pentru a include WeatherDAO în timpul construcției de clasă.

@Module class DataModule (context val: Context) // ... @Provides @Singleton distracție furnizeazăMainRepository (openWeatherService: OpenWeatherService, prefsDAO: PrefsDAO, weatherDAO: WeatherDAO, locationLiveData: LocationLiveData): MainRepository returnWeatherService, prefsDAO, weatherDAO, locationLiveData ) / ...

Majoritatea metodelor pe care le vom adăuga la MainRepository sunt destul de simple. Merită să te uiți mai atent la clearOldData (), deşi. Aceasta șterge toate datele mai vechi decât o zi, păstrând doar date meteorologice relevante salvate în baza de date.

class MainRepository @Inject constructor (privat val openWeatherService: OpenWeatherService, val prefsDAO privat: PrefsDAO, val propriu weatherDAO: WeatherDAO, localizare privată: LocationLiveData): AnkoLogger fun getWeatherByCity (oraș: String)> info ("getWeatherByCity: $ city") întoarce openWeatherService.getWeatherByCity (oraș) distracție saveOnDb (vremeaMain: WeatherMain) weatherOur.info (weatherMain) fun getRecentWeather LiveData info ("getRecentWeather") întoarcere weatherDAO.findLast () distracție getRecentWeatherForLocation (locație: String): LiveData info ("getWeatherByDateAndLocation") retur weatherDAO.findByCity (locație) distracție clearOldData () (info ("clearOldData") val c = Calendar.getInstance () c.add (Calendar.DATE, -1) de la 2 zile în urmă val oldData = weatherDAO.findByDate (c.timeInMillis) oldData.forEach w -> info ("Înlăturarea datelor pentru '$ w.name': $ w.dt") weatherDAO.remove ) // ...

MainViewModel este responsabil pentru a face consultări la depozitul nostru. Să adăugăm o logică pentru a aborda operațiunile noastre în baza de date a camerei. Mai întâi, adăugăm a MutableLiveData, weatherDB, care este responsabilă pentru consultarea MainRepository. Apoi, eliminăm referințele SharedPreferences, făcând cache-ul nostru bazându-se numai pe baza de date a camerei.

class MainViewModel @Inject constructor (depozit privat val: MainRepository): ViewModel (), AnkoLogger // ... // Vremea salvată pe baza de date private var weatherDB: LiveData = MutableLiveData () // ... // Eliminăm consultarea la SharedPreferences // făcând cache-ul exclusiv pentru distracția privată a camerei getWeatherCached () info ("getWeatherCached") weatherDB = repository.getRecentWeather () weather.addSource (weatherDB, w -> info ("vremeDB: DB: \ n $ w") weather.postValue (ApiResponse (data = w)) weather.removeSursă (vremeDBSaved)

Pentru a face ca cache-ul nostru să fie relevant, vom șterge datele vechi de fiecare dată când se face o nouă consultare meteorologică.

 private var weatherByLocationResponse: LiveData> = Transformations.switchMap (locație, l -> info ("WeatherByLocation: \ nlocație: $ l") doAsync repository.clearOldData () return @ switchMap repository.getWeatherByLocation (l) private var weatherByCityResponse:> = Transformations.switchMap (orașName, oraș -> info ("weatherByCityResponse: oraș: $ city") doAsync repository.clearOldData () return @ switchMap repository.getWeatherByCity

În cele din urmă, vom salva datele în baza de date Room de fiecare dată când se primește o nouă vreme.

// primeste raspuns actualizat la vreme, // trimite-l la UI si salveaza si distractia privata updateWeather (w: WeatherResponse) info ("updateWeather") // obtinerea vremii de astazi val weatherMain = WeatherMain.factory (w) // save pe preferințe partajate repository.saveWeatherMainOnPrefs (weatherMain) // salvați pe db repository.saveOnDb (weatherMain) // actualizați valoarea vremii weather.postValue (ApiResponse (data = weatherMain))

Puteți vedea codul complet în repo GitHub pentru această postare.

Concluzie

În sfârșit, suntem la încheierea seriei Android Architecture Components. Aceste instrumente vor fi companioni excelenți în călătoria dvs. de dezvoltare Android. Vă sfătuiesc să continuați să explorați componentele. Încercați să faceți ceva timp pentru a citi documentația. 

Și verificați câteva dintre celelalte postări ale dezvoltării aplicațiilor Android aici pe Envato Tuts+!

Cod