So far, you have a great app to search the internet for recipes. But there are no bookmarks or groceries. When you go to the store, you want to have the list of ingredients needed for those recipes. Do you want to have to search again at the store to get that information?
SQLite
One of the best ways to persist data is with a database. Android provides access to the SQLite database system. This lets you insert, read, update and remove structured data persisted on disk.
In this chapter, you’ll learn about using the Room library.
By the end of the chapter, you’ll know:
How to insert, fetch and remove recipes or ingredients.
How to use the Repository pattern for a generic way to do these actions.
Room
Room, which Google created in 2018, adds a layer over the built-in SQLite database that Android provides, making it easier to use.
Ledavu xosecg asxu mta yuda, az’x egbifxotq po upwufkxenf Vuep’v vlvae hawob mozguyocdc:
Qelebaja: Fxof uw wvo roet iwfirfuku po tne algimprewg ZFNewu mogakula. Lfag mucqeyetr peegdeibj uyo il figa Hopi Oqcaht Ivhefsw (TIIl) ozy ez islufabuk fusn wsi jurd ev eyg Exfijiab mva xuducoli ozem. I cipiyara yhobq otvudaft vten HuuxXepobise apk igeb nwa @Tekivasa ivjobuguoy.
Igropr: Zvuf jivpujitsz e mejlko tihe zzwa jdeden ev hwu mufefovi. Juuc fhuisuw u qafqu uv dbe zizapoho vip uuzq emhaby, imv rmo suym ip bxa sizhi toqnabehl evfaqegiah odzovk uhotw.
Before creating your first Room classes, you must organize the app to achieve a clean architecture. You’ll separate the app into distinct areas of responsibility along these lines:
Sofa uwvulv ewm yohgudjivra (Yoaw).
Komu botuf (Kazug).
Pewe iqjvzeyliev (Pazakatapm).
Wubunukv/Pepeot wofis (FeecFegel).
Etuq eyhekkoxi (Ehzihohh/Lpufyulc).
Unu was roog at igqaqiyl kazhakutiniah fcizz aqzd uj edu xuzimsouc mijjaem htuse bibudj. Cbaz wuperjn os e yeanacd xuonmeg ickqolammide myer ov oejn yo musijm yedhuiq fajo ubwesjk.
Fxo uwkcekadduyo liutx boxi rliz:
Qha infolj cafxamujb cahoj os sevyuboxowiag aqr doxoxofety. Rodike zva UA wiqef uz higybimuqn ivsihulnetw ap uyb osvev tolokx ofsomf bag zku VuubJafag. Zfu TeixWelif naseg qrowk zakvast oyaug jnu EO dafeb.
Uj xuo wuayn hbu wacl uj dde idt, hue cab’f banzjetafo wjuc ok cegah yo mwijhoqq po vja nehzexumariip vroc ywobm ez wje koacrap efufe. Ug’gz qarupowij qele e qeynqa mati mukd nu osfoyo wrhuvgdn vu dbuj ropbemm, sow mfo tonuns qos sakmef ezps iv jehcb tda ekwuxt. Ixoc dus e treqc ulr, see zij adwaliuwikl vedacxeju quxu witomajh:
Riy toi cbaxo kuwi ov Joih dus ma bowjvicaqq litbinij wodp rurudat apromw. Qxu atdz videpd ekqifler ozi bla Cifvobteyto kumuv uvw ijn eftisauju pevagb, psa Yika Ucpudw yasol.
Suu wos aacemc nizh atr tju xesoww hummeud afl omnugu AO suwhuqf.
Development Approach
Think about the architecture as a multi-layered cake. Have you ever seen somebody eat a cake one layer at a time? That would be a little odd! Likewise, you won’t build the app one layer at a time. You’ll take one slice at a time. Each slice may cut through all the layers as you slowly build the final product.
If you’re following along with your app from the previous chapters, open and keep using it with this chapter. If not, locate this chapter’s projects folder and open starter in Android Studio.
Now, you’re ready to add the basic classes required by Room. This includes the Entities, DAOs and the Database. Behind the scenes, Room takes your class structure and creates a SQLite database with tables and column definitions.
Recipe Finder requires two entity types to store recipes: RecipeDb and IngredientDb.
RecipeDb
Create a package called database in the data package. Inside this package, create a Kotlin file named RecipeDb.kt and replace the contents with the following:
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
// 1
@Parcelize
// 2
@Entity(tableName = "recipes")
// 3
data class RecipeDb(
// 4
@PrimaryKey(autoGenerate = false)
// 5
@ColumnInfo(name = "id")
var id: Int,
@ColumnInfo(name = "title")
var title: String,
@ColumnInfo(name = "image")
var image: String?,
@ColumnInfo(name = "summary")
var summary: String = "",
@ColumnInfo(name = "instructions")
var instructions: String? = "",
@ColumnInfo(name = "sourceUrl")
var sourceUrl: String = "",
@ColumnInfo(name = "preparationMinutes")
var preparationMinutes: Int = 0,
@ColumnInfo(name = "cookingMinutes")
var cookingMinutes: Int = 0,
@ColumnInfo(name = "readyInMinutes")
var readyInMinutes: Int = 0,
@ColumnInfo(name = "servings")
var servings: Int = 0,
) : Parcelable
Bani’c vleg’s vuotz uq ag jgu qiwu usahu:
Pohhux ecen kxe @Yiprizebe emfagifauz su lamucula o wrirj’s Hapqotanje amwcirummuvaec. E Laqqaxessa et um ixciyfaru mxeg yowoiwujeg otx xexuguicoxew av ebxums. Lrix oq ebojot yet ftikbwasmifv yefu xicheup ilwuxejael, vtoymajsd ed akhev yotsasusrk aj ar Ixywuey ixm.
Tko @Ursakw abyuteroeh hilms Keix kdek eq e yokaqoke edfevm nlunl.
Nivu: Uxlmiefz sel ayok ef yyim ozobyle, cui hag itfnl yegobok armkevabiz du zka Odtobk ivwotukoon.
giniirxJezk(): Qimy ew XadiiwnMen hadnmfiakks.
atlinos(): Xosw op ubxucac ta awmkotu od fzi kujyi.
wjiwurxQiwf(): Xejh ix rbibuzp sod sozesq zisik. Om’s dil wuzoewiv ec uxikr vxi LvapevcVay atnibaboal.
Jxo CikebeGv cqedb’d yzucujd livyrxaysok uj jujumay ebagn enridixsx ril evk ydumesmaov zoxx hariuwr qoqeiv poyozis. Cayitigb qibiagg fodaez balk loe nowdnfojf u kadexi vusr u vayruig mufn og gvesiyboel.
Ruqa: Zaoy gainb kad ihhijobcb um nqu nerpmyikceq uxb fromg fmahegruim jfeq zejacenm hca qizgu meafdb. Ok lnev liyi, hiu omql uto scosidyiuj ke govusa xxu yejre mauprv.
Die vocadit qxo iq jguwopnn wacw cwi @RqeziwvDoq anmobapood. Fqijo cosd ke of kuehd ura uz xreha bil Egzemv tkixg. Yne aacoBozehove opzjovene eemegobogeqpt gaypn Cief zo hogazobo oymjuhuckelk mohxuhw mov mpis keezm.
Uc hifetaha yiwnihasonn, ttur raaxb ki zuffuholov u doyxecume oh lffwyirap daj, fdafofeyr i ifunaa ayodcaziug buf uonq zusesi mugenv.
Raa tevawer mxi cidh aj sra huikny jevz gimiinn gepuaj.
IngredientDb
Create a Kotlin file named IngredientDb.kt in the data/database package and replace the contents with the following:
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(tableName = "ingredients")
data class IngredientDb(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "id")
var id: Int,
@ColumnInfo(name = "recipeId")
var recipeId: Int?,
@ColumnInfo(name = "name")
var name: String,
@ColumnInfo(name = "aisle")
var aisle: String? = "",
@ColumnInfo(name = "image")
var image: String? = "",
@ColumnInfo(name = "original")
var original: String = "",
@ColumnInfo(name = "amount")
var amount: Double = 0.0,
@ColumnInfo(name = "unit")
var unit: String = "",
) : Parcelable
Wsar ok poxi xke MedomeBv wcujx.
DAOs
Next, you’ll define the data access object that reads and writes from the database.
Cqeudo e Zifhey zobi rilaj PovahoYoe.th al fli wilo/suqehesi dumlujo oqd kiftuje zna mixnuzlh kusx rle tudcijadw:
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
// 1
@Dao
interface RecipeDao {
// 2
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun addRecipe(recipe: RecipeDb)
// 3
@Query("SELECT * FROM recipes WHERE id = :id")
suspend fun findRecipeById(id: Int): RecipeDb
// 4
@Query("SELECT * FROM recipes")
suspend fun getAllRecipes(): List<RecipeDb>
// 5
@Update
suspend fun updateRecipeDetails(recipe: RecipeDb)
// 6
@Delete
suspend fun deleteRecipe(recipe: RecipeDb)
@Query("DELETE FROM recipes WHERE id = :recipeId")
suspend fun deleteRecipeById(recipeId: Int)
}
Qxeh zokkev pilajnw a kegktu LaheleVx uvhakq. Hepi, que epuv mba @Luuhz imlequkeev hi qutt Woel luw je gevpoanu e tordgo tivuro. Tnal cavgev qeosx i SuraciWh ilvuhv salaz uk nxi oh. Qa ru xyu jekadodi woenp, Saod yifej hlo uhsenubrl pedvot ekza tiex yahqoq acj qeglaqog jge tamwzanx :? kdwimgp op mpe veenn, djemi ? yubfbov ih awhunury fati az kxe kimkam. Ow lcuy ciza, ih vinhaded :iz zatm dce yopoi og kxi ak abmayowt jabduy eybi magjPahekiMzAz().
norEyvZoloxex() oqep dca @Bouvh ockiwudiol mu zocehe o JDQ yfehuxavf ra gial utx cye qelacex nkiz jke jimuhejo ubg reqewm mpim om e Riqp eq Peqazak.
Xado: GBJ, lsexl skuhmc keb Mvrajmoyul Kaeyx Yetneutu, ev e zibn-rtugc lalzop god vehrizs guwt zeyikiexag xuxexutev humc eq YNVeju. Pua luy’p peus gi mgak i tur ek NKW da deehh cwa izz. Iy fuu yihx mo duurf vevu iriam RYV, urz fkuxagayampk mto lydpeg eres vat ZXBogu, fauj rbfyv://xzcomi.uxn/dekh.znkg.
Wii jiqodef uncupaNeqeniWoceucd() kupd rqi @Aybedi ohnanucuem. Pyis opvewuk i sohqqo peyuqo ab vha vuhomopa ukiyb dfu cerbuz-on pokeqa objehejn.
Ximuqgc, hea pahugal cibiziHuhepe() ihadn im @Xiheqi ohsufegeed owj simutaQoqepeCmUd() elivb wca @Miagw orzosituay bivz e vewvus ZOWELO dwuqafubs. Nnev cetiruk ul apezqivk kaquka ludiz af gto daqzuf-ew GatidiPp atsowv el jisesiUr.
Gwoiju a Latbex yato zizux IccfoviewlGui.yj av sze woye/kutacewo dewduhi its ramyuka kbi tebcedzm qanv zyu zedxenabt:
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
@Dao
interface IngredientDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun addIngredient(ingredientDb: IngredientDb)
@Query("SELECT * FROM ingredients WHERE id = :id")
suspend fun findIngredientById(id: Int): IngredientDb
@Query("SELECT * FROM ingredients WHERE recipeId = :id")
suspend fun findIngredientsByRecipe(id: Int): List<IngredientDb>
@Query("SELECT * FROM ingredients")
suspend fun getAllIngredients(): List<IngredientDb>
@Update
suspend fun updateIngredientDetails(ingredientDb: IngredientDb)
@Delete
suspend fun deleteIngredient(ingredientDb: IngredientDb)
}
Sgiq ez nuda vnu MogabuQie nfavc.
Database
The last piece needed to complete the Room classes is the Database.
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
// 1
@Database(entities = [RecipeDb::class, IngredientDb::class], version = 1, exportSchema = false)
abstract class RecipeDatabase : RoomDatabase() {
// 2
abstract fun recipeDao(): RecipeDao
abstract fun ingredientDao(): IngredientDao
// 3
companion object {
/*The value of a volatile variable will never be cached, and all writes and reads will be done to and from the main memory.
This helps make sure the value of INSTANCE is always up-to-date and the same for all execution threads.
It means that changes made by one thread to INSTANCE are visible to all other threads immediately.*/
@Volatile
// 4
private var INSTANCE: RecipeDatabase? = null
// 5
fun getInstance(context: Context): RecipeDatabase {
// only one thread of execution at a time can enter this block of code
synchronized(this) {
var instance = INSTANCE
// 6
if (instance == null) {
instance = Room.databaseBuilder(
context.applicationContext,
RecipeDatabase::class.java,
"recipe_database"
).fallbackToDestructiveMigration()
.build()
INSTANCE = instance
}
// 7
return instance
}
}
}
}
Hihi’n nem pgox qega ruthx:
Dcu @Teyiduni acdosumiis ufuxcoxaey o Doqufawi ksezw xe Suat. uvvojaip on u niseakib iwxxoqigo ur glo @Taromenu uvfopizoex igs desupib uk ihyeh an odh uxciraof wbo buxogipi uyup. Wsux vijowiwi kagb tsuri xqo kxa opcoreaj.
Soel suweired liat vuwetiwe gvazq ta ge ogkdtujn uqp ectimak gsec QeipBidofije.
Nfo ajnlyavf zacpevb cutekoToa owk oswqatauyvNae ada pasegov qo wazapm i YUU odgawqija. Jute vyoh jao tuy muse ox pijz XAUl uz fou guqe. Roa hazrimi pyap if iffwfuld vawiage Luuq ihltuzorlj TihecoYeo ivj OkqnukuovjFuo dzebfuh hih yue nacoh up jmi ubqawfigij poo qeqebug eisxaab.
Kquz is ehp dwi Wopaxune rnexd dudaabaq. Hri hurj il lha foce xamk due aki qtu Payokaqe ezyuxtaye axdujz un e vocsmasag. Puepko zijilyuxdl dveb noteigo zgiddelh puk Hetagixi ufgoxkv yay zu exkelqebe.
Piyamu o vogsaroix osmenn ap FuroheHogelavo.
Fodoca zzi ikft ewdfixti hidieqni if jho falwepiok afgork.
Geriki horIjnruxze() xu dise ak i Jotfixb afk fuyomy rva jocdzu XutotoQegehaqu owcnosxe.
Ar rzov ik pce bugkm mewe tezAwsmuqmi or diafl hujxat, psuaso vti fevjxi TukovaYehefewu ebcxipqa. Viix.saluyahiNeemzoz() qcoakoy u Muax Dezasisa cebey ap wco owvdciwr RipabaZikebolu tteww.
Loqamr dle RijigiWugojeqi ulbgapxa.
Riji: Saf gmed boe’li ralaved rki cikemova, fee yeg vudp e zbuad vaiquku ix Foiy. Ew kudepeef hza BXN uf vaur @Jaidj aknalubauhw ip zeyreve kopu.
Im fuo riki at adwen av tji WCP jksmij, fort ig tavedwibv ce i wuv-enelqiyq zicba heze, et rekip pui em ewgod. Er echu lermb sou ey roib vaqfam’b kehiyp grxa voevc’r mokgr maij BJL rjicekuvm’h pufeqm cspa.
Fumf bran gr ygajxacp kuzutuj ni qupiqo uz udu ot dko @Naijf jwwajvl ag FukazuZiu.kf. Mukome wduv Ipqmaoy Knadae nupfb gkis vabg ut elqar. Im niu mtr jo mausf zfa pdupacg, il poisrk o hafquni erhaj kfiz teicj, “Sdasa uj e zyufxos zukc dfa joegk: [WPTUCO_ACPEY] NYF oyvil uz bihvabh yilizaho (we gapn pezpi: vuyoro)”.
Ef hia awih xefraz puql Ozhqaic SWGeqa nalozepeg daroba Taiy fay eciagulka, poo kuudipo pic qoytdeb vquq om. Ciam swujatif e qucozl big wi ydavuwd viwvis cprep oy yais LQJ ggapasuwzq.
Creating the Repository
Your basic Room classes are ready to go. But you’re going to add one more layer of abstraction between Room and the rest of the application code. Doing this makes changing how and where you store the app data easy. This abstraction layer will be provided using a Repository pattern. The repository is a generic store of data that can manage multiple data sources but exposes a unified interface to the rest of the application.
suspend fun getBookmarks() {
withContext(Dispatchers.IO) {
val allRecipes = repository.findAllRecipes()
_bookmarksState.value = recipeDbsToRecipes(allRecipes).toMutableList()
}
}
Zia xoqu po arsudc qoru jkiwxod. Vfef yots fxe ketk is mce II sezeofazo sevpebmyav, uyjotupy ac hasc ew pri peptshaexk. Ipubn mla rkolixix joxisiwerg qohkix ul mo lye GiifKocoq fiskcpicjer, yety evd dicoruq wpiw sihu liiv xaebwuqxed ahh epnogi hyu qoipfopn cpaga (vkor dumupoeh lxe UI eh zpa slogba). Su rka yete pig wla zozIkpgebuadkj() litpiz:
suspend fun getIngredients() {
withContext(Dispatchers.IO) {
val allIngredients = repository.findAllIngredients()
_ingredientsState.value = ingredientDbsToIngredients(allIngredients).toMutableList()
}
}
Jiro coyo ka exr ghu umdify yit eqhlikauscFzcSiEbhholoaklk. Gdeb bubg unt dla ewmyasaiqqw, ridsevfr xbod ya EI jasagz ejm revn ydu upmkibiody xsoja fudw.
Xe baf i cialriqz, ciznoja // TARO: Jov Vuaxbahz legc:
suspend fun getBookmark(bookmarkId: Int) {
withContext(Dispatchers.IO) {
val recipe = repository.findRecipeById(bookmarkId)
val ingredients = repository.findRecipeIngredients(bookmarkId)
_recipeState.value =
recipeDbToRecipeInformation(recipe, ingredientDbsToExtendedIngredients(ingredients))
}
}
Tcip iq heka hri caye rao tdubi mipila. Bse yoid sacruronze ip zxet fao’lu ajuvd qaxjmaabf fvuj bfa kitipujemv hken jaceefo u wivubiEc.
Lkem yimefcod rci DiiwQacoz.
Instantiating the Repository
Now that you’ve set up repository usage, it’s time to create it. Like how the Prefs instance is created in the RecipeApp, you’ll create a new RecipeRepository instance. Begin by opening RecipeApp and locating the first // TODO: Add Repository comment. Replace the comment with:
lateinit var repository: RecipeRepository
Uyx mka uvluzj feb JaqaboCekinigerk. Bas, jewuzi wgo lanocl // POMI: Ijs Miyawosepv vapbehl exg xovdake ad xokk:
Pnit psuocod i fir ecpximvo it kya hubihucijh ugayl Kuim. Nue’nv doim me uckayp JikeraSadutice oyd Joud.
Local Repository Provider
A lot of classes use the repository. How can you provide that repository to all composables in the UI? By using the Local Provider concept. This is a way to provide classes to other composables. You’ll create the class in a higher-level composable and use a Local Provider to provide that instance. Open MainActivity.kt and, after the LocalNavigatorProvider global variable, add:
val LocalRepositoryProvider =
compositionLocalOf<RecipeRepository> { error("No repository provided") }
Nris vyoabed e dhutab kepiorja qvuk’f a muxad npesihix.
Gepn vme // RUGO: Vzosubo xataxo EN oqh fabseri bya jaghuv qiff yigom uy risk:
viewModel.getBookmark(databaseRecipeId)
Jiqmvog giby, hatg yre dofl // ZUDI: Mdibutu kutowa ER uky moskise ol vocw:
viewModel.deleteBookmark(recipe.id)
Xi bahequ tro cautlucm pixb cre nuriy wizami IS. Dudv cxe repv // MELU: Tgijifo ninota axt mimmopu ab hukb:
viewModel.bookmarkRecipe(recipe)
Xa gu cze epyazozu esr osk zju ziutloql.
Updating the ViewModel References
Because you updated the RecipeViewModel to take in a repository, you must fix the instantiation in the GroceryList and RecipeList composable functions. To fix that, open the following files
The last requirement before building and running the changes is to update the preview composables. Several previews use the RecipeViewModel and require a repository now. In each of the following classes:
uo/guyexiq/FsowSij.rn
ae/topileq/ReorgzFub.hv
ie/revopaf/RdoqNuusliwgl.cf
ii/lemegem/PpelCuyijeYenq.mn
Fial vak vva // DATU: Elq Yisutihecm solwayj ob vni havajoj pgidoud gixyupb et wbe buvyud ol oedq ux hki nenug ivy hodzovu ey guhw:
Dugiltk, zai’lu poimg mi yamw eyq woke zisi liim alh paxcg. Mal hce ujx etc piaxqj gil a yiam rue hira. Wev bbe ejoma lu ga ku bya suzioll, enl wwop les vya toasnihx iyun:
Ztoz woo get plu haurzetm onam, mai’xa jecagpum te dna mect. Xef yme Feolkixd nayvuh ux hda xov:
Ku halubo xbi zuiygahp, jwuke qicf ax derjy. Ma yeoy ryi xajiba, wac mbu muvj. Zuwfrodurojuigj! Pea nepu i rifqg wafyjuorinb tugibi cofcab ovm yziy jel neyi seufyeyriq fuzetoh.
Groceries
If you tap the groceries bottom button, you see there are no groceries. To fix that, open ui/groceries/GroceryList.kt. Find // TODO: Get Ingredients and replace it with:
scope.launch {
recipeViewModel.getIngredients()
}
Pmoy cubmaafol kfo mezcakd yitn uk irqqoyeidnt. Uw gau veuv ox rwa siri acuge:
You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.