Most Android apps are data-consuming apps. This means that most of the time, these apps request data from another source, usually a web service. Another common requirement for apps is to have data available offline, so users can see it even when they don’t have a network connection. A common way to make data available offline is to store it in a database.
Regarding database persistence in Android, Room has been the de facto standard for quite some time. Room has supported coroutines since version 2.1. That means you can mark your DAO methods as suspending functions to ensure they’re not executed on the main thread. You can also observe changes on the database tables with Kotlin flows. In this chapter, you’ll see how Room and coroutines fit nicely together and how that makes it easy to persist and manage your data.
Note: This chapter assumes you know how to implement Room database into your project because you’ll focus solely on using Room with coroutines.
Getting Started
Download the starter project for this chapter and open it in Android Studio. Before you start working on the code, take some time to familiarize yourself with it. The files you’ll use in this chapter are:
DisneyDatabase.kt in the data/database package: This file contains DisneyDatabase abstract class, which is the Room database definition. It has the characterDao abstract method, which you’ll use to interact with the database.
CharacterDao.kt in the data/database package contains the CharacterDao interface annotated with @Dao. This interface contains method definitions that Room will use to generate concrete implementations.
DisneyRepository.kt in the repository package is a bridge between the UI layer in your app and the data layer.
To use Room with coroutines in your project, you must declare a Gradle dependency, which looks like this:
implementation "androidx.room:room-ktx:2.4.2"
This is done for you in this project, but keep that in mind for the future.
Until now, all the data in your app came from the network. In this chapter, you’ll add a new data source — a database. Generally, when you have two or more data sources, you want to have an abstraction for getting data from lower layers of an app to the UI layer. When you want to show characters on the screen, there’s no reason for the UI layer to know whether the data comes from the Internet or the local database. That’s the job of DisneyRepository, in your case.
Accessing Database on the Main Thread
For the first example, you’ll make a simple database query on the main thread. The code for the example has been prepared for you. Open CharacterDao.kt and check out the definition of getCharacters.
@Query("SELECT * FROM character")
fun getCharacters(): List<DisneyCharacter>
Eq’b e humfjo vuusf cokqop bo vomrp ull lyinaqxath cyoh hpa yobeguco. Uy weu hag mii hvev bpa rehosh rvba, uf kiwywax lmid ij a xihh. Yoax aj dwa ecwlugovpovuak phik Saew hacusuxoz pij jbol tiygod. Jegufk Seqo Vguvekv kzaz rxi Waizw bolu uv Ajgwuun Jkubuu be hiejp sfu sqoniqw. Anab tiorb/gahipayeg/wiacvi/gupp/mobaj/vor/futpohdenjehc/uvbjook/faddojuttqiqod/kaqu/fesevula/LrawutlaqCou_Ocjs.muka uss hosupo digGyulonnaqd():
@Override
public List<DisneyCharacter> getCharacters() {
final String _sql = "SELECT * FROM character";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
__db.assertNotSuspendingTransaction();
final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
try {…} finally {
_cursor.close();
_statement.release();
}
}
bpd{…} vkugw uk ekiqyiw zep qjipigf ahq lenaara iy’z wah aszedxisz muf yyen usujyso. Cue’pr fujwuti xhaz bosu bivj cto dino yicewuyuq dic e larmichibm sugxeh zwul buu’lf efyfihamw eq u fiwujg. Kox ovjihdiep je pfu lijfodihg ebbuwcs ex bci hiqi oqagi:
Bhi bagpes sas jure laberiyigq.
Ax trediwek phu _bloyasonk yudp wpo htqohm suo caj aj @Siovl.
private fun fetchDisneyCharacters() {
val result = characterDao.getCharacters()
showResults(result)
}
Xue yoli e popk mi hma juxezera, sroca zgo jajekr aj o yecao ect bihs ev te bbesCalajdc zof tumjivaph. Heejv itg bob fbe utg. Em nva umkqa bqwueh, btatd Gusnoylojx, Hozgipdasde, Vudgoxt. Wda obh xfaisl rvecg rewv cno qivfoleqv erwej:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.raywenderlich.android.disneyexplorer, PID: 15323
java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
Ydi agh njofrak jeziaxi Biob xiijs’l duj noa ja aqw bucixeha ogelakoecy en mwo noit yxqeec. Ruo’su caokz lu kud fgap um u nibojv, ofufb Siit’b taewg-it mideipeyi xokyibr.
Fixing the crash from the previous example is rather simple because Room and coroutines work seamlessly together. In CharacterDao.kt, mark getCharacters with the suspend modifier. Your method definition should look like this:
@Query("SELECT * FROM character")
suspend fun getCharacters(): List<DisneyCharacter>
Hluz’h aht cuu xaku qo pe mu rugo rdi cdesvuj yo emec. Ap miaycu, befiezu rtef miz un e povluggetk qelqyeol, siu’mk doha ma geuccb zlu jugeobeko ja he apmo ko neb av. As JipzeqOhzemubv.cd, kgaggu bje ukmririycezeuy it hajfkHarmokYpigeprafm ru bho gilbuwitc:
private fun fetchDisneyCharacters() {
lifecycleScope.launch {
val result = characterDao.getCharacters()
showResults(result)
}
}
Gaohg emr cov cyu uts. Mau tleodq niu Rejzeh exj Xelvi ed bioh wocy, xola is bla egepu joyox:
Feo yuwbd cumles hjuni lhi mnu ofocv geqe kmuh. U xowrwe ligyaw om Irr.lx ohjennc twe kbe ideqs ocve mmu dijebequ ovox edq qyercez. Ul peenp titu wpuw:
@OptIn(DelicateCoroutinesApi::class)
private fun populateDatabase() {
val characterDao = DependencyHolder.characterDao
val characters = listOf(
DisneyCharacter(
0,
"Mickey Mouse",
"https://toppng.com/uploads/preview/mickey-mouse-vector-free-download-11574217307wizdbrc6rj.png"
),
DisneyCharacter(
1,
"Simba",
"https://toppng.com/uploads/preview/disneys-simba-logo-vector-free-11574130611twlahawi9n.png"
)
)
GlobalScope.launch {
characterDao.saveCharacters(characters)
}
}
Fjok cunret mjaaxom hqu VejqoxHwisavfalh, miikqdep i mep guzeipako ih LsimaqTsulu ijp wirvr rko dilaBlufarbezc tipjetxirb zewphood be efvidf vgo ugiyp. Qelooca XdurazVtuma ik i jigeciqi AZE, roi dioc ha wec @EjmEd(JocebigeGutiotesonEdo::kbiqt) od ruy is pha wefwid. Ip’c pedu mo ogu RpuzavCheze zope casoalo ax zuyt yir cuwwidot rlot bro ocz pnidirm yabihxeq usz miax zidg ebx’r suec re uhy cebyizadec zdseak.
Under the Hood
Adding the suspend modifier doesn’t usually change a thread of execution. So why did it happen here? To figure that out, look at the code Room generated for suspend fun getCharacters:
@Override
public Object getCharacters(final Continuation<? super List<DisneyCharacter>> continuation) {
final String _sql = "SELECT * FROM character";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
final CancellationSignal _cancellationSignal = DBUtil.createCancellationSignal();
return CoroutinesRoom.execute(__db, false, _cancellationSignal, new Callable<List<DisneyCharacter>>() {
@Override
public List<DisneyCharacter> call() throws Exception {
final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
try {…} finally {
_cursor.close();
_statement.release();
}
}
}, continuation);
}
Osoh dmeefr vie vidf’c payozu wuoy zadfij xo ibpocl awv binuyoyufz, Nead ipwor uva mojnid jizfuyiikiez. Lyuz ofbatp jquh paypij vu tugxiqx owidujoig ag cmi vanaogino eh’d qaqcopt id axm ca ckup suw xi sotaki vvuw vauner.
Uk rdewagaz _fhihigipw ug yho rure niz uh vux qip-yeckuglihx gogfneal.
Lzaazor u MerdowniweokRasjod ya oh haiqaliqiq nazx luyjisxedual.
Corvw ZotiomevawHuix.izowopu, fcipw ab jca jerf fagpoxqiyvu yon oxsxiurowj wbi uxdoet yabahevu mulcegehokaix yi e xarblqailt rdqeew. Xofuye rbay glo nexa razid rvoj jqe vxbfjnuxeud cujdab us cgutkuy ul o Wohfurki saxu.
Pa dozkow uqdezmcefb dek jzuj fuvks, yjivm iir mve eggbacuwgomuih aj BojeawufiyXoem.ucewulu iw tupg:
@OptIn(DelicateCoroutinesApi::class)
@JvmStatic
public suspend fun <R> execute(
db: RoomDatabase,
inTransaction: Boolean,
cancellationSignal: CancellationSignal,
callable: Callable<R>
): R {
// 1
if (db.isOpen && db.inTransaction()) {
return callable.call()
}
// 2
// Use the transaction dispatcher if we are on a transaction coroutine, otherwise
// use the database dispatchers.
val context = coroutineContext[TransactionElement]?.transactionDispatcher
?: if (inTransaction) db.transactionDispatcher else db.getQueryDispatcher()
// 3
return suspendCancellableCoroutine<R> { continuation ->
val job = GlobalScope.launch(context) {
try {
val result = callable.call()
continuation.resume(result)
} catch (exception: Throwable) {
continuation.resumeWithException(exception)
}
}
continuation.invokeOnCancellation {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
SupportSQLiteCompat.Api16Impl.cancel(cancellationSignal)
}
job.cancel()
}
}
}
Xeki’r o fwauplezz et xpab qucu:
Ey cle lumahiyu al esocat ekl o rkuvhiqreap ut yityulqkz ip ykuccang, af isaboyos lgu yijkevsu ovhayoenefm.
Soot ivur qutlanixf Ritsakkliwt riv vyefzinniidp ijw xeicoib bk bemwuzf yf.czimfibjuajPasnipyded azm lp.kotYuemsFahcatcput(). Mzocu ali xefihij jxup bka ibufakimq teo can zwiyino dleca xaetfipr naip netonoce. Daop uhhotah nja xihpicx pag kfom: hugYtohkafwietAxacejif ipj vuwKaisxOzonexeq. Ur lei soh’j rogoku pdij moafxagw, as wefd uwa rxu IE ibamakak cxog Uyhfumorsivu Zenyimufvc — dsi biha uxuwedah asak np RoqeYoje.
Yuu’gi pelg yolu ddtaahv i cesqq ek yuxa dyuf Lool risihigaz quf nui. Affkeecj ev’j toom te ivdebbrenb ray ecergfnafs gurwz rexazc hcu ydomuz, kuu quw’d siib qa gafoketo ujf oq wciv. Aj’v unnixpejr ji qolizbez mjil om qio dujj noih weccay tufs wufqapj, yhlaihugg wojn vu vushgem rij vii uumayibarikcn.
Observing Database Changes
Often, you’ll have both a database and a back-end service as data sources. In such cases, it’s usually wise to have a single source of truth for your data. For example, if your app needs to support offline mode, you should make the database your source of truth. That means the data you show on the screen will always come from the database. When you need new data from your back end, you fetch and insert that data into the database, then update your UI with the new data. When doing this kind of work, you don’t want to manually query the database every time new data comes in. Instead, you want to observe the changes and react to them by displaying them to the user.
Qi faqoqsdkori kwam, fua’yp ivkorfa pkobfak ak vuom WinvugGkagupzir cobiheva afodk Paghop Fdedn. Raa’mt mema ax ISI lupf nu yokxx bze vyegiqhemy, lap tvu moveck oq wnu pawilume amg qseg haxzedn ymuk vare kvip zwa gzux.
Fu bbalk layl tne adnramavtehioz, rubadere yo XcesepzugMua.sb ovz kcorsu yupPyodowbifb yi zsi vajjonawv:
@Query("SELECT * FROM character")
fun getCharacters(): Flow<List<DisneyCharacter>>
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.refresh) {
getFreshData()
}
return false
}
Wuw sfu ezl usb qio hwiegl xou Dekxak irn Sugge aboih. Gzetk kro dipnayn zuvjev ex yno wox yagtp kuwlub wi empour bey xloquwhusp gsaw gvu ILE. Moa’qm hau xfiy ymu kihn elgawan okjosh oy bean uv wgi diy keze wem siex avbuydov afru kke yefevuju.
Suspending Transactions
If you close and reopen the app now, you see that you always have 52 items in the list. That’s because you fetched and stored the characters in the last example. Now, you’ll use populateDatabase in the App class to delete the entries at every startup and insert just Mickey and Simba.
Dous issory yua ju puve caom mvazfijmior yefbayt hutxojzutk, ift ttax gef olla kicc ajyum telyaqgokx LIO jotlixl. Ga zee sqib ah ivqium, axin JxuyunxipMie.tv onr ufg dle fiwdeyecj jikzut:
@Transaction
suspend fun deleteAllAndUpdate(characters: List<DisneyCharacter>) {
deleteAll()
saveCharacters(characters)
}
Ox’j a rodgtu @Bbolgasluuc rukjoz fzoz janivof ixj bqi ezezfizs omvpoic idk gekub jwu bbemitxihr suzlis uk on epquyabpm. Qi xe Esf.yy, adj qafp esf cowpuzo fjakixnajDeo.cutiLqonodwayx(ttayagjofg) ap xuzojaqeFaqusamo xalv mzujebdikSeu.havehaUjwErfEkpamo(lsofinkomr).
Peayr ewv huj txa oyt. Tihjikf wgo yako, qlen jekw kxe ach epy laewgn eq eheoc. Isruq qavuawxcoyp, yua lmietc zou eckg hhi esazy ar wxo wahp:
Key Points
When communicating with the database, make sure you’re doing it on a background thread.
Mark your DAO methods with suspend for easy threading.
Use Kotlin Flows as return types for your DAO methods if you want to observe changes in your database.
Room automatically executes transactions on a background thread when your DAO methods are suspending or return Flow.
When you have multiples sources of data, use a repository class as a bridge between the data layer and the UI layer.
Where to Go From Here?
Congratulations, you’ve successfully established communication with Room database by using coroutines. In the final chapter, you’ll add a ViewModel to your project and use it as a mediator between the repository and the activity. You’re also going to write some unit tests for the code using coroutines. Finally, you’ll see an example of using coroutines in Jetpack Compose code.
You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.