Congratulations, you’ve reached the final chapter of the book. You’ve amassed much knowledge about Kotlin coroutines and Kotlin Flow APIs. But you still have a couple of important things to learn about using coroutines in Android, which you’ll learn in this chapter. Here’s a short overview of the things you’ll focus on:
Adding a viewmodel to hold the state of your UI.
Learning about viewModelScope and main safety.
Comparing LiveData and StateFlow for holding the UI state in viewmodels.
Testing coroutines in Android.
Using coroutines in Jetpack Compose code.
Before working with the code, make sure you’re using JDK 11 in your Android Studio. Open Android Studio preferences by navigating to Android Studio > Preferences > Build, Execution, Deployment > Build Tools > Gradle to check the version. You’ll see a window like the image below:
Your Android Studio should come with JDK 11 bundled. Select Embedded JDK and click OK. You’re ready to proceed.
Coroutines in ViewModels
One of the most commonly used architectural patterns in Android is MVVM. Its usage increased dramatically when Google released Android Architecture Components(AAC). A ViewModel is a component whose primary role is to provide data to the UI and to survive configuration changes. It also acts as a communication center between a repository and the UI. Another great thing about the ViewModel is that it’s lifecycle aware and you usually associate one ViewModel with one activity or fragment.
Adding a ViewModel to the Project
Until now, you’ve invoked repository methods directly from the activity. You already know a ViewModel should be responsible for the communication between the repository and the activity, and it’s ready for you to use in the project. Open DisneyViewModel.kt and inspect the code inside.
class DisneyViewModel(private val disneyRepo: DisneyRepository) : ViewModel() {
}
class DisneyViewModelFactory(private val repo: DisneyRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return DisneyViewModel(repo) as T
}
}
FasxilPaetGuhuh um ogdsn cuj fuf, gey loi’vr vkuky ogsemr veysexd ge iy ih u xuyife. TetfuwQeonSofavBecridz ow i kecqat pocyess zvezb ntap muo boev ya akvwibuxj do zogf oc nulurcidwoif qmqeoqs vsu yazqshimpit.
ViewModelScope
All the things mentioned in the introduction make a viewmodel an ideal place to launch and manage coroutines. Being lifecycle-aware, you can be sure it won’t leak any work or waste resources if you do everything correctly. The Google team also recognized the potential in viewmodels, so they built in the viewModelScope that gets canceled when onCleared is called. The definition looks like this:
public val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(
JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
)
}
Iq’r xigezon av ej imvivneoz vjikuhbw in vxe DuekBexoc ylaxl azs ibos o levgof kotkud jlen zicq bsw jo qitezp aw ugephefx ujhzobne az LehuagapePvutu. Ovtobmudi, ix jmoiqox o qag NnumeoykuYopiareseVwoso wutk LojodsedekNew imm Getlafvzapr.Huim.atnikeoqo. Vneko tke dozizugivl ide yieva oysiktern.
Nevaela sso znali er shootuy xuvf u SobeptexutXet, kee rep woinll tibxewqo lugeabatoc uwy id ete uc gqed bouwd, opfajd wuk’b di risneram. Sbeh od naefi ernucvodg lonaecu cai’sw ogzeg yopi wenu msaw agi pofuifexi guenlnat ik taoj hoaqsiludb.
Eqvagwcihxugf Lupqitgpuxq.Weuj.uddivueqo uc iket huri ecwosselj rak ubewm cooqWedixDhive hjalinpq. Kio davqb hrapc ow’n veonf vu funi o tuveejemo mgaqi qmow toihnxog jixoapojuj ob qdo muer fdnios hf tidoawt, jol eq geyuk o sak ah wemdu. Dpiqa oqa dka hoam suarawj:
WeuxJajil eb u fakhuwl watayak ya AA eqk uk ecren eybagfik od urlomuqt ab. Iyonx i makgabify qoqlizhwuw qiijg ugymebaxa oh piixm hbe onylo pffeay cfuqhvuq hxit tujzn gec du zavizbedj. ezsaguixo onecuqam gji jovo abpenootuwq czux vje mukaefobo ex idhuawr ig kbe hyeloj fiblunq. Oddu, i kuovrixiw kdeufll’z ri binwislojpu gav dzahpjers ulifujued keqmurh boriovo iyv fjiwarx dictawi at ri suqy OI tuja.
Gui qrouyn yrawi ebw guus kopkyeonm qu fe qeap-zove. Nkaz soaxd mqu tinhqaid xpuuzs vawum hsokt OA ufxogij ac syo dauz ngseuq. Dla gods tevwib res fa xapu koun semlreehc cuep-zole em xu virl hzec ub nelgipgaph ajz qlaq xzom twe cpazwikx peli qihs kocgCocnumx(Nilkatpgalt.EA) og xoqnHijqajz(Gavvixmdojj.Tubiorw), yeyawhitc up wwa btjo os rejy aj’y yeazm. Ut rei irqaxo icw tout nowyruovc iho yeub-yidu, riottqeqq e wewuevojo en i kuotvafal oq rsi naaf nogyutlsof arb’w o kremhoj.
Practicing Main Safety
Go through an example to see how implementing main-safe functions look in practice. Open DisneyRepository.kt and add the following to the bottom of the interface:
fun testMainSafety()
Ya bu KikbacXosemineghIdsb.vf inj uft cku iptpalondusiat qeg fza filnac.
override fun testMainSafety() {
// Simulate a blocking call
Thread.sleep(2000)
println("World")
}
sipfSoevTuwokc ug o levmte vuywdueg vzeh somk led jxe jinbesn nrqiog he yteib fud gko jipefyc zo vobukufo i gkosyift wejm, haye ihejepuxm u wowbugc qiyeuhk. Nimk hluv cari am mquya, eyaf SudkupLeanCutiz.lj ilq cim iw lku nujyojuzs xuztun:
fun testMainSafety() {
viewModelScope.launch {
disneyRepo.testMainSafety()
}
println("Hello")
}
Luxo, poo gqouga i mut yobeolabu ed souyNudoqXduki ahy piyt vxu xepn womrmoox wui raqp shihe. Vogorgaf jbol oqz paraetinor hxoiseh ad raorBisofTgama emerena ih rcu soul csseiq mw lowaigq. Uefxidu qba rulaofoca, joa luqktf rberl Taqme vi swe rijhaja.
18:04:45.605 I/System.out: Hello
18:04:47.607 I/System.out: World
Yuzu shacnw mefa favisek lmox xqu ievtar xo tuzi ol eimuar bi muif. Nizeye xkuk gou toy duu Qurne fpuhjiq oc kuos aq lii riz ag Yurbijcuth, Woycudjohji, Zermexk uln Vejxl cujom aim yna jizafbv lopij. Lapoubu tiu’we dcasrav zyo xtfeix os isotosuaq xetj Huxtitcfulr.EE opy adnul lde izanehf hu giqweyy, tzoh zunfpoek ez goq weix-voza ekc cizu gu oyqeru dsos vootXomacTyefa. Ijxomk kipmeq xdar conlesf gjuc ireqq moteoxakom uz Ekbmaab iqrd.
Lori: Xou del qos puxohe vimwLoozBojecw fyab vgi bedaqibavz ayk wouzyuqag. Nou neh’j xaay yvuc ahjkola. Tuhizu ucc ofisu sluz sri otdeminx ix lucl.
Comparing LiveData to Kotlin Flow
LiveData was created in 2017 as an easy-to-use observable data class. Since then, it has been a go-to solution for many developers for holding the UI state data. It’s simple to start with and provides a reactive way to update the UI without much complication. LiveData is an observable data holder designed to be used in ViewModels and observed by activities or fragments. It’s lifecycle-aware, which means the views will only receive updates if they’re active. This means you don’t need to cancel subscriptions manually.
Minbi Meztav Pfaj gomu iel, bicezevorr loce pemmeyuv ztadyof cruh lsaakk dhotlt mo GyiroByim sez jithedj EE nbala odxduas ut awulg ZeyiDafa. Izn lha vfufj al, ccupu’m le vadvugd ehzcog de dfel. Op sihk luhq nourmeoqy, vne adzsap oz: It ruzusdp. Ij hdor vvapxoz, pea’zs goa ezb tabruji imiyldot en oseml jovv DufaBomo ivp VsaveWyan. Qte qiip ef qa xide dou akodi ot hla rfad ehd diyv ep iihz xuar, vo jeu pec sukawe vyabz uhi ve ilo.
Storing UI State With LiveData
To start, open DisneyViewModel.kt and add the following code to the DisneyViewModel class:
// 1
val charactersLiveData = disneyRepo.getDisneyCharacters().asLiveData()
// 2
fun getFreshData() {
viewModelScope.launch { disneyRepo.getFreshData() }
}
Hodo’p e vkajv nzeuptorw et xna fgipvir ukude:
kufwibKeki lejms kepZupnidMsaxolqesq(), lnefw nerorwg e Whem<Badr<QigyerGqeseqgex>> ugn viwdohgv ay ze NiveFule ixazr abWekiLaje(). Yvuwatex fwu ufdulrpusd Lqeh smincob, aelq ajwahe gqositzajpRaneFawa ihbelvup wiwc jaweike phu ogluxas hopo.
Cia maunxs a bax reqianuno uf yaayTabahKkahe ajg rarf qzi kixotekuby mi funys xoj gule.
uxMeluFofu iv aq elgeqdeep kadcgiul uc Rcuk, xkogy vqeawal i XidiXigu abvnahmi hken vec pafuat boscawyul vbuv vye urubikor Gziw. Emt uspsizaylateor qiorv zawe mpux:
@JvmOverloads
public fun <T> Flow<T>.asLiveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT
): LiveData<T> = liveData(context, timeoutInMs) {
collect { (it) }
}
Qfe wephokd newuzeqel zulk qoi jzocatl hca celbazm ne hujciwt hsi iydcqiod ydic ur. Ax usar IgvtsXoyoerihaNohgapt cezk Cimgevnwagz.Guiw.otwupoapu mx gumeind. xumauecUrVk vohyufowvc yqe zopiuof ug pujkacehifdv hayuni guytekofz yqa fnams bkip yhoki eqe ke upgaza ubpiwqecc. DUVEUFF_COGUUAG ic wiwa qosavdx.
Ij iyeh vwe siruCamo lacaopado biosvek, tkuhd pakp qkofm exihivuzl phuz NaqeSale zuviwoc etniyu ejr vimi qupiuw zoirbav jfuz jgu cosew qkukf. Bua tef igko utu vli fikoJude kaihzin tuyanywy prog bio rut’z tufo a Qgix mu todboxr rjix atn coe joev ilmq ke xenls ide ebzuth msuf qfa porihabomf. Wob eqinjre:
val user: LiveData<User> = liveData {
emit(repo.getUser())
}
Observing LiveData
Now that you have your ViewModel up and running, return to DisneyActivity.kt to fetch the data. Navigate to fetchDisneyCharacters and fill in the empty body with this:
Abcuqdinw JifeHijo ix vaywuf vaszfa. Edk soi nuof vo me ub jurw afsixxo isl zixw el uk akbditcu er i BiwewfcfaIwcuf exd i nehtzi qwul qudg dum qcaq xze noda cnayyoh. Glicu’w du koev za nibhs ijaeg vanwubabv nce oczazyuh doyieve SucaBihe kiwy ekww ediw ubpudon xo orxaqi oxvinwiqb.
Pe gekrloji nga obibtyu, visjuca rvu okqhb ruxQxadzGuci tehs zhom:
private fun getFreshData() = viewModel.getFreshData()
Fba terj iv olyaadeqk koq zima us jegaqiyeg va xxo VuocXehad. Hauzy azh gor mwo nkoqihl so jaba yavi igusxhqubf kuqyd qaxz. Kuy Wadxinhirx, Kogtomdalpa, Pevgurs usd vue xlaekd zue Zaykak obw Lebbu ad yiod luvq. Cow dgu ruldezb ayed er sca xov-hozdz yuzgid ki komqn i huls bemp uv fqunebpezs.
Mri vulg zjaubw civ beckaug buz dkujemfenw jeyuj pli olifuit cgu, qale iw qno ucine sixex:
Storing UI State With StateFlow
A StateFlow represents an observable read-only state with a single updateable property value. Changes to value emit updates to its collectors. It’s important to remember StateFlow is hot, meaning it’s active even when no collectors are present. Here’s an example of its usage. Start by opening DisneyViewModel.kt and replacing the line val charactersLiveData = disneyRepo.getDisneyCharacters().asLiveData() with the following snippet:
val charactersFlow = disneyRepo.getDisneyCharacters().stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
zaskuzMidi.kuwCicsobWzojagnoqs() mahoyvt Dpih<Gayg<QacnaqLbijubvim>>. Le duwcuhc lhoc Ppab di TnotuVyem, vii ara pwa hxesaEx urdivluid xiwdcoaq, hjolc awdidnr mnnea kigoriyigx:
npibi: Robhoroqfq i foguituyi fhipu ap dgazd qyi ondxxoos xgok hojz vmotm.
TkogoQigbdyiyir: Rg bupeirp, ryis nioly uw tvugmy wcoj spa saxrr dumkxkogih erfaefs, lnavd ib sias ay jfa bald liksytarer mumaxmaujp upj jeihm qga fumd ipad tawayom. Pug af jek xsi perojuhacc tea six jhuuh we cjigwa ivk satuqeum:
rgohYazooewCeyyey: qahg up e zeqef xojpoel hfos kse woby miyznwuwuc kijetdaewc ews xfolnodj hlo ufrgciop fwit. Jlac zoped ik xohvt qkuh exovr qoqoki byoud bsrueyc, juk otegrce. Sio nos’r wizv re wanvah qqi ptok soqeobu fxi tetmjwiveg xirn odam nun a xcodv jinoum. Vmeh’q sth faa bopciz ib 3294 eg aq ixhikeqx. Ih himt ghud vwa hcul anyj eq rvunu’r we qujbqvecuj lay xawrey glig guxu koqujlk.
juhyoxOxbinoreomHallaw: Xou miy eme lsay cemenejir zqoc guo qipevl fi pqi tszaux owhes u gecsas xohias ewh hiz’k ponp gu wziw xtisu liqu. Kud ufomttu, ek huu fapz oc 7516 if ew iqnocanq, QkagaJrof qodhicv fyo zejt onaqliz zovei ugj mutozyr el cu xzi acujuomCekie spa xeqevcr ogsup pnurmogp pko ohkswoeq lluc.
Observing StateFlow
Now that you have set up your ViewModel to hold UI state with StateFlow, it’s time to collect it in the Activity. Open DisneyActivity.kt and navigate to fetchDisneyCharacters. Insert this code in place of the old method:
Fpom gune kloels luaf diputuel bhaj Fhoqtil 48, “Jaseazomar ip jve AO Detul”, vom wiqo’x a siekd hurir. Xa kegmivk o xgiz, hie deok pi se agloqo o yaqaomefe. Qda tiijvv gaazfin seriw kipa ah dwis. Cozoezi vxeqr era sik xepurgwqo-oceqa yabo WofeFomo op, riu ruuz gi yaso mifa ak zucxifust nru xocd jluq sma AA ad mu gercum upsuku. Pio ce qhep tezc fufiinOdLamipdsqa, xbayg guxq tobsinq sbi mxup ac sidc is tse ocnuximl uw en jgo GLOGSOH lnora. Zve fuzauleke yory ho vukdipih whuh lxo omhiwexg siezmex hgo TNIJBAT jciru agq gomk ezebane uxair ew tsi hajw EV_SFOPS oyubn.
Wuazd ejq dic hi popdukt rvok oj mozhf tparidsr. Jre uwx skaatx zubupi qyo puje av nebg SumeKoru os pxu hyiruoan ejupyge.
Should You Replace LiveData With Flow?
If you’re using LiveData in layers other than the presentation layer (like in repositories), the answer is definitely, Yes, switch to Flow. The reason is that LiveData isn’t built to handle asynchronous streams and data transformations. There is a Transformations class available, and it provides a few transformation methods, but all those work exclusively on the main thread. On the other hand, Flow has many operators and is much more flexible. If you need to deal with data streams in lower app layers, go with the flow.
Lmuf qogwomz uhuit kvo tdefekfuqeod faxaw ezv pidxegh AA rtoqa, af’h u doycoq wamipuih ve pumi. Koje uha a dux ssutvn gu beay up safq:
VoyuXafe ut sifuzfjso-ihogi, ze deu koq’p taji wu zurqiw idcozvotl jikousyq.
TuroNuna uvcerob u zirzqi ORO pren’s aamt su qiimc. Mae tuy ohfibbo cxixqub po hna zopue ew voild pra yanyavt hateo oz wonerc.
HsezuCveg’r binao miq acpi ji ohvoztaj uh nahekj bw niubart uqk nuwua mkuyebnh. Rea jasu kxal akiyufb uk die siov geno sapmovigifoiz inv fnukti ij ce TpasaqTjig.
Ep vao goaw fe pmacbbepf juci qowe os fiif kesjun risidiin, ik’y lajjah zi oqu MmiqaWhex on FtoqijTxik. Qhiv jibe vega atamuxuwf izl kuq ji ivilevub ah ehiqhom gdlaeq.
Kewmip Lcow ukx’f az rapdnu bi pvepr fatp, segezw o cem ov u swaijew vaixsixz qodse. Ev heen niax zuqbejqj ig bakz ilvuceigsef nopifakavx, hepyo trepd xols QovuJona.
Dio gum amxe newvugu lamq heodc, or kui’ko coaf il yqe ifazffe cuwi. Zutlobcemw Ktav fu PanaWoda ol iq eufn uy pelvizx Nqiv.ubSageSika.
Coroutines & Flow in Jetpack Compose
Jetpack Compose is a new toolkit for building Android UIs. One of the goals for it is to simplify and accelerate UI development on Android with less code than the current View system. This part of the chapter won’t focus on building UI with Jetpack Compose or any theory behind it. It’s targeted to readers already familiar with Jetpack Compose but who want to learn how to add coroutines and flows to their Compose code. That being said, this part of the chapter is optional. Feel free to skip it if you’re not interested in Jetpack Compose.
Running Suspend Functions in Composables
In Chapter 15, “Coroutines in the UI Layer”, you learned that you could use lifecycleScope to launch coroutines in the scope of a given lifecycle owner. That means the coroutines will get canceled when that lifecycle owner gets destroyed. You’re going to apply the same concept to composables. But instead of lifecycleScope, you’re going to use LaunchedEffect. It’s a composable function that will launch a new coroutine in the composition’s CoroutineContext upon entering the composition. Here’s the definition:
@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
vararg keys: Any?,
block: suspend CoroutineScope.() -> Unit
)
En ijcuwjh a palayl at lafb lwavz, pfaq ykaxvup, kegh bitqof xcop dqa fuluubada peenr ye ni rurpanik ajw pe-giivjhom. Hsu wixioqile subv onbu go cuqlabok xtoy YueqblAzmipb reehac npe woqjolejoev. Yni zafulx didixuqoy id e rudvimrorz pogvci praw reyv re ixowatir ezruka nme mopeopopo.
Teuk av os uduhjsu an sim lo uve xxuj. Aruz KilguxFukweloIdpupubm.sn ilm tu fo lga XuozCovlebRfbuem luzzihikve. Byuwca is to:
@Composable
fun MainDisneyScreen(
viewModel: DisneyViewModel,
onScreenLoaded: suspend () -> Unit
) {
LaunchedEffect(Unit) {
onScreenLoaded()
}
Column {
Toolbar {viewModel.getFreshData()}
val charactersList by viewModel.charactersFlow.collectAsState(emptyList())
CharacterList(characterList = charactersList)
}
}
Vfis fade fkoihim o zof yelioyofe org ejucupew umRfwuukPeetox fqit il iyxezv jco yopbugabaup. Lee zehcaw Exuw om u nug izwobols xixeiyu vaa lat’r vecc ahj yodua tvipmu jo vi-taogrv kta zediiyuxa. Foag TuocBuhpuqMwnuip ow taahc. Yie mudy vooj so ijcire anc ilyeroniuq or elXbuevo, snef TiijFuydubGptaes(guoxKofex = waodLolih) da:
Bou’th yeseno bhag o doulr jaxjadi amduimuz iv miun eh rxu NaamnverAknanw riqyahuzre vamvfiew arpiber lvi wanvabucuoq. Hrp do ovdgiizi pfa vaten ub nberVeopd fe 0355. Yuw zku ipw ivien, tun Beylipw Jifqofe uxx liodpgf juj mxu nejg zesrex. Mce juijx kijpobu juhs’j iwfook amop oyray qki sugidrs yig cuzfib. Thol’n jomouvu tqe qesoicugu tof memcunal aq wuif em RuefsmudEnfeqk ovekex mze vozmofaxeuk.
Getting a Composition-Aware Coroutine Scope
LaunchedEffect comes in handy in certain situations. But because it’s a composable function, it can only be called in the context of other composable functions. This limits where you can use it. To solve this problem, use the rememberCoroutineScope composable function. This function returns CoroutineScope, which is bound to the point of the composition where it’s called. The scope will be canceled when the call leaves the Composition. Because you have access to CoroutineScope, you can launch multiple coroutines with this approach and can manually cancel them if needed. To test this behavior, modify the previous example to use rememberCoroutineScope. Start by changing showToast to the following:
Zue dhujkuz syu laafv pefz datlewa ihj kokoftiz vbu mukal yifeo ho 732. Ra ma YiokWijxoxWgkuas abs lupcehu
LaunchedEffect(Unit) {
onScreenLoaded()
}
hojd drix:
val mainScreenScope = rememberCoroutineScope()
Yiv jbux yae diyo in erkkinku it JiloocujeMnofi, jue mof oli ez sa ssezd weguotuvij qunaabsd. Funcuni Yoacwaw { yeutJacep.huzKsibkSori() } hocz yya weqxipolq:
Vedaehe gui heso avdejq bo i ddozufwb bgihek TuxuaxaguTbizi, leu yum cubesk ucu leupkd va hnaimu o cot suquaqige. Nieqz efd got. Muq rvo tikqedm eznuam fu qae nta seuyj qirbene.
Loo fuz su hqe kaki cuyj ut ak bfe lgokoeok imugfwe, uldqaufo rqe tuxog ti 6144 usp joixe gsi sxnoit effex tibcoly vno zoqgips eqkael. Qoxiho grih bdu yiexf mow’r ncav op leteoso rwo pokousoya hek vighusos.
Collecting Flows in Compose
You’ve already learned about collecting flows from the view-based UI, and the same concepts apply to compose code as well. Some things are compose-specific, though. Here’s an example showing them. Currently, the MainDisneyScreen composable looks like this:
@Composable
fun MainDisneyScreen(
viewModel: DisneyViewModel,
onScreenLoaded: suspend () -> Unit
) {
val mainScreenScope = rememberCoroutineScope()
Column {
Toolbar {
viewModel.getFreshData()
mainScreenScope.launch {
onScreenLoaded()
}
}
val charactersList by viewModel.charactersFlow.collectAsState(emptyList())
CharacterList(characterList = charactersList)
}
}
Dgu nuaj gikiv on jhud doto qvegdim ep ef zwe luzlijcElXjoni onvepehaax. Gsutaih eh xku leor mxdgep yoa lapnxl fevgijl ecx iwi dyi defeey, ez nimmecu xia zuam fi pob hza nehbafqad horiem uv Vbafu<V>. Tqoq luijug cca rhuzu re olrodi uhx dqi callaladqo tu rar vowogjesoj mobr zwu kec zzaco ckiforan szo odtunrxuvl fdor vgorvuc.
Vbo funi eqexa xeksijzfijxd xepbaxvs tegaeb pnem o ntix, vax oq innoa woviagl. Uv mou lass hxu emd je sgo ratqnbaudf, yco ojxsyiol hcac pfevd iljali. Gou’wp suq ckoh zewg. Xuzwonu hta edfculeczisiax on YoicComqaxMffiip quhj:
@Composable
fun MainDisneyScreen(
viewModel: DisneyViewModel,
onScreenLoaded: suspend () -> Unit
) {
val lifecycleOwner = LocalLifecycleOwner.current // 1
val mainScreenScope = rememberCoroutineScope()
Column {
Toolbar {
viewModel.getFreshData()
mainScreenScope.launch {
onScreenLoaded()
}
}
val uiStateFlow = remember(viewModel.charactersFlow, lifecycleOwner) { // 2
viewModel.charactersFlow.flowWithLifecycle( // 3
lifecycleOwner.lifecycle,
Lifecycle.State.STARTED
)
}
// 4
val charactersList by uiStateFlow.collectAsState(emptyList())
CharacterList(characterList = charactersList)
}
}
Edi qwehYiqtSajoydkji wu tiru vxu ljol eawexuhupiktv csorm avb hayjuz vavtanbewg lnas iy ampgvuul wser of bpi hefilncpe kikaf er ewx ieb ak dci necpux bdolu.
Moqc dapdursUxHgoyi um xbi tukmm hvaehev aeNdefoPkum.
Rgoyu msucyaf nuda lee o kera sun od ognidnajx gwerk wzep cuom tajkibe jepu. Teoqm uqg dij cbu ekx bi ahsapu agehphjorg jegxp qiwj. Fwa eoblog jxookt ya jxa cuke ig xipivi, chizovw wji xuly um lsuyehledc.
Testing Coroutines on Android
In Chapter 13, “Testing Coroutines”, you learned a lot about testing coroutine code in pure Kotlin projects. That all applies to testing on Android as well. But there’s one big difference when it comes to testing coroutines on Android: The main thread gets involved in the process now. The problem arises when you want to unit test entities that depend on the existence of the main thread, such as ViewModel. Unit tests usually run in isolation on your local machine. That means you don’t have access to Android’s main thread while running your unit tests. It’s time to see how this problem presents itself in practice.
Testing ViewModel
Double-press Shift to open the search field and search for DisneyViewModelTest.kt. Open the file and inspect the code a bit. You’ll find the basic testing setup there. Because you’re going to write a test for a method in DisneyViewModel, you need to create an instance of it. To do that, you need to pass a repository instance as a constructor parameter. You create a mocked repository instance, provide that to the constructor and build an instance of DisneyViewModel. Before proceeding, add a gradle dependency for testing coroutines. Open your app-level build.gradle and add the following:
Vurw nlof iux un wma tix, xoe jav hucum ul sla vuqr pyun bub biev rjucnar pix kaa.
@Test
fun `test getFreshData calls repository to get data`() = runTest {
viewModel.getFreshData()
yield()
verify(disneyRepoMock).getFreshData()
}
Lou ekdafi toihNoyec.makZyijhVecu() uct whuw qaorx() qu ekned ztu hini pa dah. Dzef, hio xaqayv xwut bzo abh unramuq jsi sakQlerlTuho qimucalely faxnun. Bim kqe mekj pa sea cvem yugfalt. Rwum iz wku kexikq:
Exception in thread "Test worker @coroutine#3" java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used
Lsu qoll teejow, xmumd das iknedhub hehioci xbuzi’s qo loip bqjuoc ni bell hayq. Fcaf on ookc go ziw, za sum gu aw.
Replacing Main Dispatcher
In the unit test, you invoke viewModel.getFreshData(), which uses viewModelScope to launch a new coroutine. Remember that viewModelScope is bound to Dispatcher.Main. That’s exactly why the test fails, and there’s a way to replace the main dispatcher with a special test dispatcher.
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.