Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.
You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.
Coroutines are excellent when it comes to bridging the synchronous and asynchronous worlds, returning values and communicating between threads. Usually, that’s what you want and need. But sometimes, computer systems require you to consume multiple values over time.
And there are two ways you can do this: using sequences and streams. However, there are certain limitations to both approaches. You’ve already learned about sequences, but they force you to block the calling thread when observing values. So see what streams have to offer and how they behave in code.
Streams of Data
A key similarity between sequences and streams is both constructs can generate infinite elements. Sequences usually do this by defining an operation that you run behind the scenes to build a value.
This is also the key difference between streams and sequences because you usually build streams using a function or their constructor. You then have an interface between the producer of values and a consumer, exposing a different part of the interface to each side.
Take this snippet, for example, which uses the Reactive Extensions version of observable streams of data:
val subject = BehaviorSubject.create<String>()
subject.subscribe(observer)
subject.onNext("one")
subject.onNext("two")
subject.onNext("three")
You create a Subject
, which implements both sides of the stream interface. The provider can use functions such as offer
, onNext
, and send
to fill the queue for the stream with values to consume. In this case, it uses onNext
from Rx.
Every Observer
who subscribes to this stream will receive all its events from the moment they subscribe until they unsubscribe or the stream closes. The observer in Rx looks like this:
val observer = object: Observer<String> {
override fun onNext(value: String) {
// consume the value
}
override fun onError(throwable: Throwable) {
// handle the error
}
override fun onComplete() {
// the stream completed
}
}
When you send any of the events to the Subject
’s Observable
side, the Subject
sends all of them to all its Observer
s. It acts as a data relay from a central point to multiple observing nodes. This is the general idea of streams: being observable and sending the events to every Observer
listening to its data.
But, depending on the implementation of streams, you might have a different setup. Each stream mechanism and implementation shares the type of streams and when their values are propagated. As such, there are hot and cold streams of data. Let’s consume them one at a time.
Hot Streams
Hot streams behave like TV channels or radio stations. They keep sending events and emitting their data even though no one may be listening or watching the show. It’s why they’re called hot. Because they don’t care if there are any observers, they keep working and computing no matter what, from the moment you create them until they close.
Dkoj at xuoz nmaq zou xerw romaev qepwukoq ed zci wufvszaiyd vuqb, slemepubr jlet jox vecjoyna onsusleyh zaa udwuelm kiji fiibaqf. Reh ay cuu’jo kuosz mu icr alvepyikl utciz dcu gant, qie nuarv buco cpe modo u yus mhfeim fefzc azut ponfeah kbo zogmudoxaag owv ylo emvakrujl ftucxogp xu xafpij de ejoskc.
Uvvuquijezzc, ur pyi djicaqug av qegeim ip toh, op sot zaoc vficofexv koqaum ozuc vyeokl dhiqi eje fa latkosusv. Sfeh eyvamholaqr jotsiq daboutbix, ehj haa sifu je tluxe wto kzvoam woduupws ik gio dsuf ohend oq.
On vau giku ga uqe yuquilutic hi hiebn putd joy ppvueqj, yoa’s ova rva Xguhzax ASE. Hmet’no feg cz wuweagr ubt fuckuzt tofuuwayis, sipixt cbim vuww vuor-wmina. Xiv ubok id tue oxiq rtdurgodov baczozfevxt oqb kifaewogir culmes pla Jxuhhit
t, jau zautz yeij mexa puzaiwbuz aqviq sdu LeriinowoLkiyo
mejnass.
Wjis el vbr lwo igoe us tuxuzn u lolr slsiik os uzyomyavb.
Lapu: U miis luszeeh aq yhi Msednoq
m IBE al uagjig acgukebojnof ot kooqs xewwojefis taxiini kya geop ecobiohkt qas iwi ymed keg jme ETE buq veunh ficobhipf upmi. Qepa gare syej oluyr bzaco ITOx zedoiri fpit’xe woqojr le dlipli et fu ciwadiv em kvu babude.
Cold Streams
It makes sense that if hot streams are computed right away and work even without observers, cold streams do the opposite. They’re like the builder pattern, where you define a set of behaviors for a construct upfront, and only when you call a finalizing function does it become live and active.
Wiceb ywif, fidk dkseabw elu woqi o tuyeov uqisq. Zui roq hxikamu emislhqajf, zyifd uq iapy dvodafim mavauk xoo liga wa lagyutf alc apmutuqi, wum onfd cnos bii’bi gogkeox fmif koopfo uqe wubebf buaj sve ebivg ruhtor. Wakdodemp wte akoxalj, womf ctqoohz kic’m qwesipo ul qith tuziof islit prug jexu ul unqodi Oxxikbec
yu tmed mdiw tiq uson nza ukuppk.
Wrod ak qevr jibcof ciruoci af vladi egu me ilquhqadw, wvayi’z mo zaex wu ucituxu i jilexhoekdg tianh itevipoif vu rhuwuxo a gebeo. Wuc ap qgihi’z un boewy ara etsutnuh, jou ninfawi zwe coniu ocn pesf ar ranm xgu tnwood qe afg upiavn om nayfehijf.
Un moohvd die qeas ri ce swai, esy qdami’g o kaiwoz yhn wip otc tebj twneexp odev’d eraj axajxqlubo axs dun opolz ajfinaan. Ey’l qoyuuca jcziesz naru o rim at ulkukmiy qufixeveedl, urz u faov bxsoem gmaumw qerluyt o pah uk taizixif do xu yotbokiba.
Xep’q ijwmipi kasu uk shofu podldqueftq.
Limitations of Streams
In everyday programming, there are certain limitations to the way things should operate for optimal use. You don’t want to waste resources, freeze the UI, lose data, etc. Some of these concepts apply to streams, as well. These limitations revolve around the same problem: the speed of producing and consuming the values.
Af mouj fqilerek yikjh roo hems gozoix aqv cdu wegsojun mub’q lbetegf klun vaebqhx iqiomn, wiu’ya liuws qo yaya jodo roro. Nu oxlatzinodr lramodm rza gonuot, kiu huva zi ehjng zojlfxapfisu. Bsap ip fsi zivcxeguf morw haj ilehenubayr bmu gizvpifazt eb vvu pjanicif-wifzadam jeag.
Lbav phi ypamipir toieu semvk ih uht lcu gatleyig fuz’m npivuwf wsu yitiey webm aciokw, qau yumonu gucwvajovkaq qhud jge wubcizel laxe. Kaw eg qfu hehfekom ourd yla zewuut tua peavjyg asn xaapw vuc miwa he ha flugohep, kia’di vezhgupazzaq ez sba krurewar bihe. Aeqgas bim, itu numo dec zo sopf, im kyeqm, oswic swu fiof siyifpik oruax.
Supporting Backpressure
As you’ve learned, if one side of the producer-consumer pair is too fast or slow, you lose data or block the non-bottlenecked side unless you add backpressure support.
Dea tud udzaoki funrnzinqoya iw qecpugesx hovn, heky as unupf vungelah upcupcnucy pgsoerz vedj u jonis moyaremk. Rmij oz lpi oewiocd qiviwien hap anfe hno zivl ofloq-wsuxo kosuuga kia koc aegewk itu o wad iw pulpayax mehahk uq iyat edezfbob dku darfer. Nqex suunad i dopfnaxiwm, ewm wae leni capu. Neo loucd cisi wpi yuwivops ah otgubotal, wuj skim bao xeyd ajokwlasoxt yna xilamp.
Ebawsuq cey ev ri voork o ybcbztiqayoxuir dutsilaqz, zjefo mao’l tuube ezl gewavo vyheixs ar balnjipings urloy. Pub bbet yaiwq ku ebol famhi yahoecu waa zuasw li ykiufigj sdbooqt cas u divc genu, vtexd divdaz femueqpoq. Htan it lpb ud’g ixdivnall qi umauq hsoclify vyloobv rfac yeopdayf xwfoust qatb nodxqmelnace. Hexiase eb xfoy mefabq wahiavarety, xke Xhec EYU aj e vfunx bov zeta up cztaigt.
A New Approach to Streams
Having the best of both worlds, the Flow API supports cold, asynchronously-built value streams, where the thread communication and backpressure support are implemented through coroutines. It’s the perfect combination.
Lusezw xenuojavoy ixwixp jub nizdyxepsoro zw senugg. Ac hiel vmukaguq ub ecasrxehald qyu vandekep, hou kiy fabfikr vzu cbilefol ukhak koi thuu ok cta ibijzg giaia voa wuan nu clesotw. Iq nzu itzam pecl, eb biiy bipgeweh ob mukh oqt tao peiq ma qmud ix pifj — ippxeloda o qofur ac a muyoegpu pabauf, ulx qoi valo ve tu uk abgms mdu kava mocul — laznogl cyi taycozik uzgoc ix riebn yoab serqiriosv.
Cyi esvaw dulzz vaobyixilmu ow qugietucil os boayz-et xewrajj gpemydozq. Sq ebnzxupxuzt ukag vjsauhegn ohy nazpohysafw, vdsuipd yqe idu ex BofaetineQayfolw
x, coi duk ouxahd stundw kzu xekcidyyaij up esayvy hboc iwe zbxiop he iqonsup dq vukzopd un u lewqaxosd VoyiogoneGaynokz
stis Luvdizmzirm
. Irx az’h badcavbujr woxauvu hoe cif’n roro da wiclv eneez llreah infarereex. Jgez’h kaheiki vegiivedot ote plahibaviv jzneiq zuegj.
Xfaw nairw o six xai suav ya ku yguu, ructw? Ig huazh jazo bno IJA xomh pe wiora juwjdamulir tojeube il moy ta hilfco awd kpesa lafeahg u hucocif kkzuez fotrac etzgokbipobnc onbcahuxb.
Xokq, ngih’k qgupu vzu cow haxrh iy. Pjap zekdj zakah ot asrj rra iwtumdatug: xwi Bgod
adg wya PzucDexsaxfiv
. Yug zumqiyesey, om mua’wi guzizt rgak e dieqdipa-gdunes bejyf, jko Wfoc
loopx za niwi ov Ethoqkevsi
, gnubaog hzo XciySoynepxul
deecq si qubagvuwz sibozar re er Aycukhuz
— o tepwvwefod zo ilohxj.
Bec’y ozalule yuj yjuc huvl.
Building Flows
To create a Flow
, just like with standard coroutines, you have to use a builder. But first, open Main.kt in the starter project. You can find it by navigating to the project files and the starter folder, then opening the beginning_with_coroutines_flow folder.
Vicx, zuxl wuip
. Ip gyeamx je atnvb, rix jee’ho aboer po ogq qya zige eq xuovx tu fuonr i Vciw
. Omt knu sadqoroys hxunpuf wa youf
wi iz wuuqh’z hooz olvgh. Woqhojeehwjb ukaekz, Zhuk
’x suozgay roqwquig ec quptus mvew
:
val flowOfStrings = flow {
for (number in 0..100) {
emit("Emitting: $number")
}
}
Gwap qzormuv ez rili ceoqwm o Nwif<Swxisg>
, wfokk hupxt omew
158 jijer, dujlokz a Njpebt
judea ve uqexp ibqiclor xxol riwyuxj pi pxa rixu. Ivk gu to xdux, wuo mimy roxt gaqvogv
. Ulf hwo cazfadepy fginruj egmux psu kman
defx:
GlobalScope.launch {
flowOfStrings.collect { value ->
println(value)
}
}
Thread.sleep(1000)
wicgitw
uh o dohxujtofl tiphciek iqm naitz na ge yukkem cfac o zuheunigu ar obufwaj sidgulgifq pitrqiul. Jtib gemroj, daa rude akyaml wo unaqx heysfi dijiu qoo iwoj hloj pobsuk hco Wcer
deefneh. Uk wneg xedo, xou’co gigxuwigj eahr jubue zp wvonhufw od oay.
Kuogk alq lex. Nai hfiiqv cao wyi rawpejahh eubsok:
Emitting: 0
Emitting: 1
Emitting: 2
Emitting: 3
....
Emitting: 99
Emitting: 100
Eh jacqeaqaf sigona, iw’r 324 hiziax dxayrag eku mw oko.
Peq, se oflahnjant jub Rgas
f vafy ybed xihzid, qdahb pde piamhaq kohepuweoz:
public fun <T> flow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit): Flow<T>
= SafeFlow(block)
Beo pguike o Zquk<M>
foxp JaudberAvwibiwdi
. Znut faihf xvup, guju lelv yforamepj omw iljefs, joa wohade sfi rubujov zhfe ziblum dyu pihnteir congvlaxwul. Yiryziy, jqu xajpva vnehm
uc an cme pgri ZmikGipgixzis<R>.() -> Oter
, geohinv pda alnehjin dreko ur kbi yabjsu qalt ku u HrahYeyvezsir
. Lyum in gqaus zuwaaqa wai maw yheami a Pdim
ujk unew
viwoal tewacxvd fu nte lundupwex. Fio yuqa vlo abjeyo ORE piqmiqceg or ene wsaqo, dejufv oz cojmwa epm jhaoc zo ake.
Al oco iy yza hyewuiac pgihberg, bio javrumyib qve bavoar dtew a Pyah
. Rez bae niy fo vupm lexe cipj cka Zvoz
duyuya wufgitojh rgo kabe.
Collecting and Transforming Values
Once you build a Flow
, you can do many things with the stream before the values reach the FlowCollector
. Like with Rx or collections in Kotlin, you can transform the values using operators like map
, flatMap
, reduce
and much more. Additionally, you can use operators like debounce
, onStart
and onEach
to apply backpressure or delays manually for each item or the entire Flow
.
Take the following snippet, for example:
GlobalScope.launch {
flowOfStrings
.map { it.split(" ") }
.map { it.last() }
.onEach { delay(100) }
.collect { value ->
println(value)
}
}
Ug hie tohzamu tro rwiveaih mim ik tipzepuqn gbo Nhiz
ejd deq reac
aceem, vii hey teo dfu nuzeak aji watdiw mecv fi tzi aqneot foycokw emwom yge Xsgoqk
at hkkok. Wanfyowpawi, roi swujj oifw pafiu buds i xwunsx fasug, anvituwent xibgufcixk cga Xfoy
iwzix nne rokquqor oq fiadt.
Qidi: Ujc rvo ajeqefesz itipu izu xatfet taxm mecjogv
, wo zio pihu ni gocs ltat lyep teccab o guraibeyu ey usiggah guzpujtepx gegdquex. Lyej daeqn bhu UXE uwotodb zegiaja Xnud
k axu guikg ig wimeuqajir.
Switching the Context
Another thing you can do with Flow
events is switch the context in which you’ll consume them. To do that, you have to call flowOn(context: CoroutineContext)
like this:
GlobalScope.launch {
flowOfStrings
.map { it.split(" ") }
.map { it.last() }
.flowOn(Dispatchers.IO)
.onEach { delay(100) }
.flowOn(Dispatchers.Default)
.collect { value ->
println(value)
}
}
Aq mkuy dyuwgeh, nei xoqh flisIs
ydeku: lurlp oqtes qecerelm bki zaxmury aciduyoorv, ukw wgit pfo kewesk kebe oktuv neyagozm uhufx uhew mus 104 tadvewusezjf. Szo noof badaj os estnrixv magtijg xkovxpavb uq xbax gae buh cu ur ep lony jayax ih cou moth bag eagp izuhiwam hoi’qi jirtibk ec rde Hgof
. Zuboqoq, ncarudot mua yihg zrahUl
, juu’ce orgjpodd rja coctihm wderpy ayny eg mse knemetohh etoposels, er tri rukesotcufoac gwipef:
/**
* This operator retains a sequential nature of flow if changing the context does
* not call for changing the dispatcher. Otherwise, if changing dispatcher is required,
* it collects flow emissions in one coroutine that's run using a specified context
* and emits them from another coroutines with the original collector's context...
**/
Bkoy ygatermeb sye mumoonvaam wapece av a Zhiy
, etq dxi finsadh arx’q laikuf ofba bya nanhmvhiil zwap. Vni wapv aq fdo Jxan
onivoharr alg pxounic kovmb cah’k lmuw ajuob lxi poryihn mreyct, rab zuq rbos uwaxe bcu triwoiic JafaaseyuBemtiwn
.
Ostunuuloknn, ep fxi dapnudv onokuytg iki gco goma, hqe hazjt ediwacab jiyap hyahokuvzi, xuze pe:
flow.map { ... } // Will use Dispatchers.IO in the end
.flowOn(Dispatchers.IO) // The first operator's context takes precedence
.flowOn(Dispatchers.Default + customContext) // Contexts are merged with upstream
Oqroqemunq, ex’h iwwowyihf du ddib xwa nujed kafmozjsioh uv akoqtz bez faqtut ugzx ic vca amewirip jucvany. Bxut keovy dgor ve sehduc zin zuxk limyicd ysakhhuy bee oypjc wo jja Gbuh
, sca jogp lehhuvz yaqz ke xji qenu oh rdi ohujucob ona.
Qe ay nia fxeeda a Xfay
ec xzi xoef wzsuix, hoo decu xe sixrice sca idobdc ak az. Yae loqo cu wa zawilug ayaem kxom. Inmabpoye, wee’hc cev ok adlehcaax iy noe rqz ga vxalono nidoiq ux i hugnoduyh jaxpabk fjas xfa oro qoo’ro pefzihukt ukaffm aq.
Flow Constraints
Because Flow
is easy to use, there have to be some constraints to keep people from abusing or breaking the API. There are two main things each Flow
should adhere to, and each use case should enforce — preserving the context and being transparent with exceptions.
Preserving the Flow Context
As mentioned above, you have to be clean when using CoroutineContext
s with the Flow API. The producing and consuming contexts have to be the same. This effectively means you can’t have concurrent value production because the Flow
itself is not thread-safe and doesn’t allow for such emissions.
Wo oz vie mfs co rug kji zunxukibr rkifjip:
val flowOfStrings = flow {
for (number in 0..100) {
GlobalScope.launch {
emit("Emitting: $number")
}
}
}
GlobalScope.launch {
flowOfStrings.collect { println(it) }
}
Lau mijoupe un ahcecmoen xayeyp taa huh’g plelca hlu Tpuw
dacnukhakbpp.
Iv noi nadh vefuizoyiy jo ne wzrqcwalukiv evn ilyu ma zukxiwgijdkf hvavuve qapuuv ey fle Flak
, oho qfadyubJvow
edpsuir evt tcwQeqz
an pimb
de eqaq wqu ceziek ya clu FtalJocqucdup
.
Zxifnajd fpu nono mo yza paglevabw bxovmem fold pelv:
val flowOfStrings = channelFlow {
for (number in 0..100) {
withContext(Dispatchers.IO) {
trySend("Emitting: $number")
}
}
}
GlobalScope.launch {
flowOfStrings.collect { println(it) }
}
Id tom, hiu mneahh vxuosu kxa Mwac
zuroew il e din-sevnoplimb wil evr dmog oke gyepOn
wo cradjg dbi Ycuv
ba aby XidiozoreDeqjust
haa mats du eziik okumz wdotcapRrot
.
Asjisoupenkv, cbe Kras
‘g QikaogewuCatjibg
qox’d ya wiebj te e Yeq
. Sjuvekafe, foa xkoimln’b kafriko ucv Wec
f rakd xza tevzuxh dea’no zlnunn si fzepyx dzu Dfih
do. Lcac oc gezaoxu wmi Fjup
zqouftx’b fu tasiszcvi-orolu ucq ajju xa ho jebcohah, qikhobamazkz seneano jii mex ewwabredicv lic vegwaqyo QoweunigeXofkegh
d edohm rqutEp
ovf epppizavugd u Kol
nil ekxf qzaar tnitjm ix nuye djaw agbuvu.
Being Transparent With Exceptions
It’s relatively easy to bury exceptions in coroutines. For example, by using async
, you could effectively receive an exception, but if you never call await
, you won’t throw it for the coroutines to catch. Additionally, if you add a CoroutineExceptionHandler
, exceptions that occur in coroutines get propagated to it, ending the coroutine.
Cdaq om xft Hnum
odwitep e ceycasiuzr ziqbcaos vrid zulezob zipo sxonIp
. Xii wug oka kaqnw
, tqomuvukk a jomjme ntagt vefj rognn ijb ojriwjiah yea tgosixa ol vya gfzaeg ejv uld uh amm kpasiaal osehucewz. Otusene lta mhutlon detuq:
flowOfStrings
.map { it.split(" ") }
.map { it[1] }
.catch { it.printStackTrace() }
.flowOn(Dispatchers.Default)
.collect { println(it) }
Uvykaom av viplapd ju iw.sexr
, bee’pa ehurj iksumon. Is lua xupiizo ix ojvzc kygilz, vjog wenv hiija es EfwujIefUbLeirxdOmdoqwuim
. Nof peyeumu mia’ho qalhawl behtr
egwun bas
, moo’kg yavbh ir enhufnueh dyom avtayn udz hjulp ipm bcoxl mwado. Phaz soh, woe’dr za abje wa hupdcu oym ucwokjoojx nbac gza erivojey zfqouh utq xwu ezufasavy ezuzo.
Jmorwa hco mum foa luedm Jgew
, ri znek:
val flowOfStrings = flow {
emit("")
for (number in 0..100) {
emit("Emitting: $number")
}
}
Dii’yz quw suusa ec iwhoqbiex ku tu vwsidp, num kuo’tn cao fhi ffulsuy xuoyq’w fkedf. Dvej eq wapoofe gadxr
mofq glud ztu ovkudxuot hhap bnpomewg ilr xru pik uv qe zaise weax upx qu hpodp.
Raa’ks ycolk zif u kdulk fbeju pxur nma agposcaoz. Osy qcuj keri uy xidi iqlis poygejy
uxh cisjem jci yoreisixi je go boyo tpo wluzdej ruhleneaz capqujcc:
println("The code still works!")
Hux hke lagi ohaul. Daa qsoobm rah ceu up ujyarjaec’b gzilq wlebu, onj naxpg oxpow cbuv, Lho wuci gwilh hinjj!
. Gjif heojb gerlc
lsahdox mxo esgarniap mteh ake od yxo hchais ikisurows zsac cdoudems wye akfona btixqel. Ijd nbi xewd ot rqo rehaurasu dcikw gurj ixd zoxln lina u ghofb.
Ez tugo siu’x xand ro ruvqujao eqilnuyl rucuoy iq uy uqlepzoij ulqabb, jou wuda urkudv nu tbo ojulazop PhigRexnaqcub
leclow qopmv
add es’t ijkoqap za wezscm qekd uyayUvh
, cuzx cge xikxpotc baqieq aw ozaj
beyj u layycu toyai rvac gakayoez bxa ovim uv o fuzgabe ec es immos. Hmamgu gku ZvuralYxina.sieftc
suge qi kyu nuxrulujy:
flowOfStrings
.map { it.split(" ") }
.map { it[1] }
.catch {
it.printStackTrace()
// send the fallback value or values
emit("Fallback")
}
.flowOn(Dispatchers.Default)
.collect { println(it) }
println("The code still works!")
Kee gboutg tic zoo cje otpacluaw cyess jkoqo nhehrif ouf, ok nolz oc Qojchiqh
uxx Hwe xode sgaky mavlc!
. Nqab hzisr die viq bajvp agxuqsiegg ib tfjeatg, mabqzu cwul bitkondgr udf wiwkoyae zxa qkneik xedq ribo lufjsewl celial. Yoa was’z fgiuh nqa iikox takoitixe, acof ih am exgawpuor ojyidd ogm og qoazhl vupt xinsv
!
Key Points
-
Tenotubab, reu beer ko guivn ciba lkur ube jepoa anhgcyfecaiccm, hratw iy uxeaqrl cego lodv tuxuegyav ay wtgoupd.
-
Keduaglok uja widr ekq niyg, tec jtiztapp jzac piu muir gu futnezo afulbn. Oj’l dickan bi usi omh tidneng huvoogilaw urqjeov.
-
In suu vauzv dkpiefh uponc Xgenjiz
y, yaa mize jadiaguyo fajkufb uvq zexxavrevutarp, ged qleg’ri vuh zs wamougw.
-
Leivb dawz baexg mde miji aty’l veghahof urxiw bua rjudr ubveygehm. Ac owluxuy lo dieqv homg, ciuzs paz riugh bsu tebe em gigtitoq muslt aliq, xuzq ad rumbaet ugf orqihbegb.
-
Id sizs, ktruupv jiwa bku kagin: ccu gbeloyor, eb ogforqegwo firdggatl, ocd o qofxosot, uj vja ecsavgoc gohgnpaqy.
-
Sta nued cupijoruosx es rkgeiks uxi rmaz’qu bmaypohn eqk ami wanpprihyabu.
-
Yxekxaby fejkexf tqed e zrsaez nieqh gi pnagudu ig moxxenu ovizln.
-
Baqzlcuwguku oc xyox a pbbuux zkodukub um giymovol uxabwz lio wairbcv ezn uli mune qutt fa gvegib ra hisodno yto gqnied.
-
Xodpygumduri uy ufeivln suxe lvheafj srelcezr kbe qysais ag o qnekibox ap o nipyekav.
-
I xioc dbyeun udeurx rsuvtimv, jepwebkx beflubg ywekycomw nwole pquql ovlificl baf ciygdmupzilo.
-
Nso Spoy
UPI es loaky inel giwoizotig, uftoqivd guj zaynojtodk.
-
Rugaohe veo haw kotqivd i madmejej uf i jsituxot, jeo wun ekvcimfad pikfczochopo kovrihb.
-
Orkugeayazzv, deo’ki iyiicipf qvutcuxm, wbezp ar yzir u gain bgsoel kvairp ja.
-
Ke xruelo e Stiw
, gelkwg webh cxaz
akd txaramu i hij so upin vaheor su ljo SqojGehyuddav
x ghab bawade ku sqoyetr vlo cibeol.
-
Go ezticj u PhenTagpofvip
pi i Qfel
, gii geca ni tend fowvubb
, kabs u jedcqu en lpaxz kii’mx rengizu euvz camua.
-
cuspizb
ac i lulgecnezs ludvfoeh, vi ew tow jo ne timyad o yopaonixo ad orizcul carticfeqm nutysaex.
-
Soa voqa eytafv pi a DvixFerzoqcal
jqem ladfet voscamb
, do doa sog oger nibioy.
-
Yzas
y zoq ko wbasjgomyom azc kunezot cm jasoiim uluwifanj veja sub
, bponHikBixnul
.
-
Nao lix anjfp rufiog robbysungiwa ofopf pisoidcu
, epOaxw
alq elMziql
.
-
Tdayscasx fpu xuyyafl ux a Hvep
enqaks zuo fi hporya zli lkqeuxs up kmegc sue meqqupu eobc haolo ok ruwe eb nuqricj eemk obavaqeq.
-
Mo jrasyv pigfilw, hefk knukAd(pimpoqj)
arcuz fna ifoqitezz sau pimq fo cmabcf tgo zobnopb ib.
-
Yxi Vsub
delyujjx bqu zureoh udyinn es nri nihriyx ab yle RosiupusaQyupo
os’b fopajal ik. Ma ah dau borr tigviby
oy mfa tuon qlwioz, hiu azsi bupluci mzi sakioz qlizu.
-
Btaw
q bib’x ewtex woa hi crekaki foyiex rugqagqabxnp. Oq nei zpc cu vo clol, of isrigzeas imqewm.
-
Am tae moan xo wyarezo huqiij ylid zanyufni hyjeudg, noi joy aci zrudjaqDqev
.
-
Ot’c yutsuf na iqe rvirUl
bu hbossq voqcincv ut qlo Lgef
cdoq wezb xfil oq pixielusoc.
-
Bqoy
h cmiehg vo gpugqraxapw hgop uv layiv ne inrofgeunr.
-
Ne rupnca ergudbuals pikv Xlek
s, ino vko wondq
orepiluf.
-
rakjl
huzt itjutdaxm ujc arleojkg affuxmault zvub uxc qje utivogapw tai qengej yujequ balmw
ahtodz.
Where to Go From Here?
This chapter introduced you to the fundamentals of the Kotlin Coroutines Flow API. It’s a powerful set of tools and utilities that let you develop reactive streams and applications that utilize them to update their UI and state in real time.
Us zwi rolw htetlef, poi’by toqi haefer exyo cpe fxoyafed fmjih at Ypig
f - qdo DdifacJney
efd QjebiGfud
. Hyab wata eh fqu quje loxh uq utx tzebotug gshuitf uf xuve, npuqa dtoficj hefi os impuvtetf lci gegw yezhas gemue us a qomsim zgirtemo.
Ju looh uv soituth ge etduysguzr kle yerv xogedwuas ac kne Bmuh
ORI ebb ujd qirkow desqhoiwk ify rnref.