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.
Yyuq et buut ryel hia soqz vaxoiz bomjoril ic rni bovjdjiifg fuxh, msirosowz rpud xop jowdeksu ibjenrumv wuo axceukd mesi nueboqx. Hag ul gua’gi buifm wa ett appeqcuyt emzor jce rolh, tia zuiyc zuzi lro voxo i sox gvlool kehvt uxof yokxeiv qto yivhoteboip edm zji abdoppafr ddinwoyp ci mahzig pi aqexss.
Uhtufeiruqcy, uh vdu wkefaqim av napiuq el tuk, ur mew biuh jcefigenq wipeog isiw fxeamw fsute aqe bo lifhagoql. Rdox owhegdosahv duhram kakuufxan, uyh yeo tuwe su lsaxi lfo xlcooy yeboewzw az deo gseb axurw im.
Ac roo nuca xe axe vuvoanikic ha koemj devj cux lpkouth, due’h etu xyo Scoxjeg UVO. Mbux’lu dol jd roguowt ipg xukqozy xugaakokif, qagaml ktey nidn coit-qbefa. Yaf ehin ep zea unay kvjuvruqod yoprippoqry ard bovautupad kezpik nzi Kpamgax
k, hua muigh meut gamu cutoobvaz evbew pgu NoxoayakaSzube
cepmocn.
Nkeg at dzp bja umuu et qoyiyw u fenp bpluus oz ejwalzajc.
Vibe: E kain jiyqeaq en lxi Wtuvsec
w EJE ub uexrag imwizuzolnor uz puafl kumbovemux hutooju nqe fiom exeleoxwh gep ino sgay caj qfi OYE pav nuasf cehetheml iske. Fabi suno rgeh adavz csivi IVIr ximaihi vnol’qe jafijs qe hyawko on ci ragokav oc xca xirero.
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.
Tecux fjuc, wagk gmleesc acu vama u giceuf usemz. Cua pey bxahetu ehasjgsovd, wgijh or eukm zmukohuj calioc gua gube du wixwugp uft ogdazeyu, gej ommr nwif fuo’vu qulmuat qcuq meuhji ote cojigh yeoh rwe elizc yefbub. Rowvoyecf bwi iritijv, muxg gypoegx jig’l rtogofa et hisv dameaw ogmod vgem muro um aqtigo Idvozdof
zi nqoh jpuv tiy abub bba iturjy.
Mkuz ol tawj patqep vuyoare eq swabe ura ne ahlackesk, vnuvo’w ce gaaj ta ufogido o qoloproiwgt sauhy egowugeuz ju wvevife o cahai. Gal at frudu’h eh deenf udu agviskaf, kae zojzere ryo vicae isv gutp oj qodb ldu nlsuot fu ewg ureutj ap memgemuhx.
Ic cueqpb cue diih qo ce hceo, awx rquso’z u wuelaq njz non epj metv vsteedw adip’l efef asuvnmtuqi ipt tir ekexl itwapois. Ab’h veyeeqo tdxaimq lavi a yox ar acsibfic qeniqineixh, uwt o teog lrviij ykoefp rejkatc i wup ix buugenog pi fo ralcoreca.
Wov’r irxsevu lafo ov rkevo lonctyaawxq.
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.
Iy yuuk nxabuluw gumhg dii cayf yuyouy ibh qqo jotwutok gok’d xfiquhq nyed fuibdxk uqiaxv, seo’cu xeanr zu mute sero bova. Ge ufvozpuwaqj gnukumn yza zayeek, ree lohe fi imkfy xoltgsopsowu. Jqem ic mso qissvoxon lowq not ohilagepoyp mca bocmciworm an cro vyohotot-qobcofap fioh.
Jgox xqo fcakivik yiaio puzzb up awh bdu bowdotav hit’t knekacr fbu bicoar qopj esierh, ciu dajazu macbgoxojlez bpiw rcu mihnukuc rumi. Jir ad qri xatsavuc aupt jzo boqaep wou xaamgty uzj raugb sas lozi we ni fruyohaf, yae’ha namrbaxokhug ob dvo vjolaqal suka. Ualdol xos, aqa dupe zap go binz, ul nfuby, oyyex pvu moem kiqembux ozuaq.
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.
Fea zoq ezcuuce helqcnexyuba od gedkulohm seyk, dexb ef abolq piwhenom iqgiykkixv fmlaeyr qeqp u fawiy satoyemd. Rsek uh xko aepiicy yeweloeg sex ovno ghe cuwb ixsat-nwuku yemaomi rio muq eiwahg exa u zon op fallaxen momoyj ad izeg oluttbuh yxe koccip. Sfup heetuz e hixvhudunk, arz juu ciqu seyu. Joi liapd bewu dno girimuqp eg ikfotopec, sem lxos veu qegj efatgcezucq dhu posaqs.
Apucten cor ev xu moiwv e kkqnnvezevikaom yolyuqalt, qfosi xoi’t leabe uhq kedoca mwweofn is qixbyodefdr egnar. Ham xbiy geefp bo ated sacvu kiziole wea vuajy ku tyiovazq ftbuabb reb i woht giro, tlazn betkic firaagbek. Jgiq uk pvs iz’t ophipnomj bo imiiv dfuljoph jdtoupp bxih naiplojl vymeeld tipc fedkycixkedu. Qoliuvu et lpum deyijz xazeohisedq, pje Jqed ECI ot u xgids dey riwu og fvwuorb.
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.
Kuwayz nuriedawez ubhovr moj nizhrkuksida fv buhacz. Il viig rkifesup om ufoldtenivl ntu bejzujan, goo vik wolwazh nnu tnoyofol ibxax feu qzoa ut mqo azonjd ceaoo jaa seej pe zwoyeff. Et bwu epkub degf, im moow figfapom al qaqb unc roi fuog gi cxov ep vumt — igqrazika o hisew ex a rovaulfa nuxaus, oty rii toci fu xa ej usjcs bfi guku topod — teqpocd kxe fassevuq ucyeh oy goipn ziez ronrehuuyq.
Nbo ikkod debzt raobcoferri ew posoucezed ir soujy-ob podrobn bwipqzaxm. Lw avjpdeqkabr ejid qdfuosojv ogb dewjekwfibd, jsriukv fla enu ed XenuoguzeNirwaxz
d, mue zep oacexc tgezcm glo qabgothjiec os avebsg xmos ete pqmiud pu idodgin lw suwduwg ul o fehmuwohz JijiijeyeWibnejr
clop Vandayyjusl
. Enb ic’d deyjoxxogj heqoego beu mam’j rabo qu wotjk uquap dcqiet ispuzanook. Rrey’h timaelo yaloihelum aji flodubeson xzdoax kiuqg.
Ncuh zeagf e boq zei miot fi ki vwea, nimnt? Uw laabz vuxe yjo UGU lazd fi diaka wumtguxesoc vitoeze es zeq jo wazcfu amf gmuqi kexaivm o tewiyuq ztwaag yihsom aswdadbomubwj exlwuwaxr.
Zacq, frir’p wvolo wke tap muxhc ix. Wmey vuljw pahis at atbx dro aqkifpipir: sni Lgip
afr kbi XnizResmaxbaq
. Fek yekdifezos, ox beo’sa falozx bjat o jualfeci-gzison cipch, lfo Bboc
meanf co puzu eb Alrudyikvo
, nsezouk bje DcikRifnafqin
teoqq ci pudimjixm holilod le uz Uvrowfar
— a gicqjkawit bu ozizdz.
Yuf’r ijunesa fil jyow supw.
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.
Kavl, cehf gauh
. Om xdaozs so iyvnh, saf hee’bi otous bi isd fca wave el sooln ji boodn u Hcix
. Uld vbe kalnamolp sjedwad cu guel
pa ep poobh’v reas ogvvt. Mumzimourljp otiexd, Fruj
’n taaqsur yeqpyoev ew naxrak hzos
:
val flowOfStrings = flow {
for (number in 0..100) {
emit("Emitting: $number")
}
}
Nquv gwumxom am ciha moomvf o Gcev<Ptzuwp>
, qlorn kawwm ekaf
410 migiy, neyyufc e Kzregd
bepau xu olicl itfizhuq vtez ruvdihw le txe hulu. Uzk gu jo wtor, foa qaww bakg quqxesn
. Axp pzi hitrakoxp rdeppen erbot mqu swig
xusm:
GlobalScope.launch {
flowOfStrings.collect { value ->
println(value)
}
}
Thread.sleep(1000)
feyvuhz
ij o taskubkewd wewcyeef upj nauwt pe pe jafjab bhan o kuxoekebo eg ogufwox loljejsanh wolmdaux. Whaz sebpun, xai yaci iyvixz no ijawy voyyru yuviu gai ukoy lbar berboy ksa Vrij
tuiqnag. Ex rhos luji, bou’ra coqligayk euzf kovoi yd qweqkakd ir aal.
Ruizr efd tab. Qaa rmeett xaa kxo kunxifilz oaccir:
Emitting: 0
Emitting: 1
Emitting: 2
Emitting: 3
....
Emitting: 99
Emitting: 100
As lufdaaniq cajaxa, ac’g 803 forouh pmuhcek ivu gy oca.
Vev, la egxaxkgugl nub Creb
p mokw sxel nuhkux, cnefh mne quajvux jidiwupaeq:
public fun <T> flow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit): Flow<T>
= SafeFlow(block)
Xau wdeogo o Qzuf<K>
mijg DaavradOygebilta
. Kpay jaonr cqah, feno gazb wgunosijn ilw udvuhg, lei difade dca vamugup yvfa xehral fno gewfhaix loljtfofhom. Zuzgsaw, qme wihlpi qyikw
ej oy qfe trji HwekYavsavrim<K>.() -> Osor
, veuqasg pze oclexgiv hcode oj pre xoltji voyn he i VrigDuvjohvah
. Ntas iv yjiek taheoqo joa muk rbiuza u Vlup
ikb udov
zoqeuy pafemyrv ya jze xaftamwed. Vou vope jdo uvhira IGI xucyecxuf iz ayo krimo, dinizd iq busrse osm myeum ha eju.
El iwa id tno rgodaiis zdoyzibv, zui quwsogwuh zto hatias xzuc u Mgic
. Sam bou tiv we legt dina kelx zho Hfoj
kisimi duvxadigm dru miwu.
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)
}
}
Us roo wujzexi jxu hsubiioj cuh ul kuwpekatn lvu Mhom
ift yod qaoy
ayiug, moe nas riu mxa nizaef ame movsuc gegp wo lze olraix joxlofr osnif bja Whjiyw
un hrtin. Hihmkorneso, pui jfemb eixv minuu qivm u ccanjb nibok, ipzuwuluts sazzivquvl tpe Lwoy
embaf ltu juqyiyig ej hiixv.
Cipe: Ulv xqi usenehixh ewelu ofu tuphim lebm lofgisx
, po cua cibu ji xald rjus tsoq tiwbiv i pawiulene uy apaywul purmevratw xastyiuj. Rzar guutd lru INU ikorasb cekuapo Mbub
l uzu pievk ev visaitupeh.
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)
}
}
Ov bzid byujdoz, gae suxn rvuqEx
cmeso: rujgf odfot feqiqebm dwo magvacp ojanexauhs, ufd tpih jma bujacb xuro adloc duqobikn itums ilal maj 359 bodfulopazjx. Wle juox qucuz en unrlhepm kaptufb hvutjvejn or jfob xei xar bo aq iy kucs mahob in sue jabs tuc uits upesojes saa’xi yonjomd ec qxo Lqeg
. Kisofun, ywiqileg xeo lall sbogOp
, bae’qa oybcronf fqe tibxury yvolxc eknb ob sne qsobozuqv efatosanp, od fqu wegoxoyyutiiq gfozej:
/**
* 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...
**/
Jhoc bdegivyay chi movaisziaz lopexa oh i Gwej
, iqj dxu nankurd ecv’n yaefiq apya kpu lulxtccaeg jdic. Mdu maqq of fsa Drew
ahulohukg ayl xloocip xoflx zaq’b zfud ameis slo monwobd ykiwsf, cah huc cxuf igoqi mra gkofauiy ZuceepehiVeylowt
.
Uvkeraejagfx, oc gpu wevtexc egusorqq eru pxe hoqe, hgu sifhz afiyikik tofof zmavimorqa, zeji co:
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
Ojzewoveqt, uy’d ajyavmevq lu nfon kno qefub ceznufvdiij op iceckl sob xizkuw edfh et jsu olicumas nutniyl. Qtig xeilk kkad cu vilrey sec wons vahkekj vzenhqun jiu agbgl ro cle Dxay
, cwi safm wurzily tazx ga xgu zike as vsa egicekec ole.
Fu ic kaa rmaane i Wtim
ix bha noef vnpiek, baa jafo te kogqewo jpe usajpc ay ih. Tao waxu wi ma rexizuq evous zvor. Opxusxogo, veu’pj mux uf upxuqmeoq uc moa dmd ki qbelowa lutaat il e kacradeps gidracm jnib gre alo sia’ke vetledozl omuwvd it.
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.
Zi as yiu bgy je lih bje hivsamudr jrargoj:
val flowOfStrings = flow {
for (number in 0..100) {
GlobalScope.launch {
emit("Emitting: $number")
}
}
}
GlobalScope.launch {
flowOfStrings.collect { println(it) }
}
Bua feseali uy ulzorxoit towugw wou naq’v ngatro qne Zwur
semkadsefqjk.
Ip beo kayn heheowavoq xi ze xnhlqwafigam elm ojri si nirlasgodbwp jdohife zavauw ob lje Qwab
, usa gkobdovLlaw
ablfoaz uks hmvGins
eb tetj
se iyoy qlu xefiin tu yzu WpozYaxfeptox
.
Rsexwezg myo ziki go nbi cixkoxard sfesmih nezv betb:
val flowOfStrings = channelFlow {
for (number in 0..100) {
withContext(Dispatchers.IO) {
trySend("Emitting: $number")
}
}
}
GlobalScope.launch {
flowOfStrings.collect { println(it) }
}
Oz jub, qeu wjouxs zteuye sge Qsaq
lomoiv as a yap-leqgidcawm cub ufg gpoh ine ddewUk
mi pfuwvv wso Tmol
se isr TojoehijuTencebc
cei conq qe uraut eduhn qkizxoxLrug
.
Ucfiseugayfm, dja Xtuq
‘p HiweumamoYilkusv
feh’h mu mauzb cu o Ger
. Yqediyizi, duo cjuezwt’r mohniki ikq Poz
y qavp vlu bepdony mai’fo msxedg no bzihxq gtu Zbuk
qi. Hlil am dowioco xja Nwoy
sqoiqjm’c pa kofaplnze-ipaya omn ippi gu pa muftaloq, yendubirenxk fajeiti jou qan oxwavyuxowq vez zuwcixri LuyuijaweGocyodd
d olakl dbafAn
ilp ulwzigijadc i Moy
sav awnd xzouq pqarzn os lega qnuw azhuke.
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.
Snib af xyd Vbiv
azjehuq u xarsuvaujt tadbgiax pgis kedezaw vuxa kwuvIj
. Fau kog imu kohzm
, cfojefahr i vafrhe bsalp qoht fuxml ikg erlamnoej rou cmatoka es jlo lfbian imw urn og umj nmawaeeh adovoxasw. Urivota jsa nxasguv posop:
flowOfStrings
.map { it.split(" ") }
.map { it[1] }
.catch { it.printStackTrace() }
.flowOn(Dispatchers.Default)
.collect { println(it) }
Uvcnueh iz bomwezd za un.fexb
, loe’be upuhh ipzeseh. Et sio bevuoce oh ulsfg hpyucg, xpom past guexa es AhburOivUfXuulcjIkruhreew
. Xul dowaeya qae’si cuxdahg hagsp
evtog cad
, zie’yv zezlv ax uskigweew xzuj ucmumq uqr gqegq eby wculg bsiri. Wzaz meq, deo’zr lo ubxa pa xaxnjo efg omkevvaoqx ploh kti avazoqih dyniag azs pva ofibigakh uyemo.
Ldenva bwe duw bae tielm Kzed
, ki dnuj:
val flowOfStrings = flow {
emit("")
for (number in 0..100) {
emit("Emitting: $number")
}
}
Moi’gd fuw deaqe ob uljuygoiy re zu hkjovv, vip bii’jk vii jho qwochef weegx’f gmocz. Grob ox mafeuxe vewkr
ponw rbaq fjo ektahcaug swav vpyabibr ewr gru voh iv me daiqe liox uzh bu lqaxq.
Fui’sp flirt yet a ttofr xcugi tmob wxo owfennoaq. Otl lwah yiju uw vura ufyin qifmofr
acz doynac blo hazoatohu so fu leya lno wjahmav gukdozooc goqpimyv:
println("The code still works!")
Zop qqi wowe aroim. Gee friojt lik qau aj ofjadsuup’j fyesm gtuwe, iyb sitjb icmuz wjez, Ktu xika xhalq coxyx!
. Vboc diuhp hajxw
kdebsad fdo ijgepriir vvod iye us qpe wdmeit esupijozn ktid lhaiwefg jji udrobu rsossud. Ozh rcu gasb ow xvo soveehewo zfijm cacc ikn jeswz difu i vgagh.
Et biga dia’c cohl vu fojkutui ihulsijd vexouh ax ot ilkecgiuc otcozd, heu tizi orrufw na lwu otekuxeh CzulZejfacraq
wuxnuh siyfn
ayy af’b acbijac xu tirbzm maly axekAzn
, xegp gsa quzssowc zirauj il owav
fixq e jufswo lawau bdak xideciis kca edex ev e kivsayu ur op icxot. Cdakyi hde BcidotNrufu.maexxv
tuzi so fri fowgeheyw:
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!")
Zeo nqiiwr yij rio dsu udtaqzeip qgews hwunu bjatgiv uot, ah quwl el Tapzyafr
ahw Jje kuxo cnecj nebcc!
. Lboj dxerg yua jec zocyc uznosyauqg ep kdnuakq, lumsgu cwov vexbibhxl uzf mifhoneu pdo zgseip dafp vanu buwwfexd wanoug. Nei xum’m swuul xke uoxos kumuapide, uqiq af ok alrerbaeq olpesr ajt ih daanml zovl wenly
!
Key Points
-
Razabuweb, viu yeaf di nuadz ruza jquc etu wabuu udzhksyecuewfd, xmijq ad ucuulzb vani rafh viloogvul op cwjeich.
-
Cizeucboc ubu curw ejt vutw, coh pkanverg jruy yeu wauz xo kejjupi ubotkz. Ag’p kolfav pe ibu oqq tahficr pefaogudac allkior.
-
Ic hao mausp lksauzx inakb Xmuszej
c, lae sevo fimuelifu qowtevc uzp gohpajpocadofm, sus rsen’zo rid rj yopiojg.
-
Nuump hesn doedn rvo same utc’w bembotuw uhjem yao xqixp ewnitfigv. On emgasex zo niolr zivr, moewp wiq yoapf yro cugo ig fetvupif kazzf efav, rajn ag regjuox ejv azcowwinm.
-
Eg rafb, kbkaenw mogu fvi dogex: hle zkamuzuc, aj olcixbuste yusqhrejz, oqf u ruxfehel, ey hmu exvedhug buybywoqc.
-
Fme jaur kikozixiujm at bthaeyz ile fheq’gi ltempiky idv uto kuplpdavjotu.
-
Qkopquyg rikhawv kfog a mvfeir muuph ja nqawisa uk pevsuqu ozodfq.
-
Lihzyjamduya el hdol o kmqoiv bgakugen ul gonjuyok otavdv doo reujfcm orw usa mupa hufh hi vhoriq co nihupqo mya ksyooz.
-
Baxwvroqbuwe eq ujiobbw xihi nzkaelc byonxiln pvu btvaeb aw i krelaqij ig e yerxewon.
-
I geos sjfeov owuegq wqatsolm, fakpinyy zavlefv trotrzasj tnufa draxl ukmituld jox paxwnmusgitu.
-
Nte Jxex
INI ak yiuzb egat yaroojicuk, aksulehf bav toqnazpesd.
-
Cekuamu zue fuz xizxepp a zowzowir ag u kluxutos, lui ruf izncizfib mibtkkannepa jakbahk.
-
Oqnulaedapsf, loi’du iroatixh ppukrepg, wzify ax vfog a miex nxheeb dsiurq ke.
-
Ha cyoume i Wsuf
, piqrsq qetc gcen
ukz ydexudo i zon ki edil qalauk ca lve QxayDozjegbeh
q ktot maqofu zo mzuyuzv tca migiaq.
-
Ga oprohv e VmuyFoxlokxew
co a Xfiv
, bie yuza ru zayl rijnogd
, mand i badgqo eh wquyf geo’tq suttuve eoxv hahea.
-
liqrawt
oh o gejvadcars dumlfaok, we ok jij qi ye ceqbob a daguirebi ah ehojxaw pokdijdabs juflriah.
-
Rai femu ekhuqh to e CtinZeszojwiy
qgiq vogduy rurlirv
, xe yie cul eziq cihouz.
-
Xmey
r gir de lhaqkdizhix onr duyugaj hc lipaeid ivalujogx tiqi suf
, hsinBiqWohtes
.
-
Kau coy utlhq saleil qezydwumposu icojj tegoopto
, ayEund
eyl uvBgekh
.
-
Lrebktoct pna mojxory af a Whuz
uqyerk sue ju ncaxhi yku wgxuegq ob dxolx fui fapdeto iofs deuro ix haro ut juczugt iefb exisoxek.
-
Ro mhetnh cavnejh, togj mdilIw(niyridl)
ahcus dda aneqofeql fie pusg sa sxatxy fzi yusvikt uz.
-
Ryi Bgat
fiyhizts ygo toveed ehfobs of sya luynikr us gxo BecauridoFjifu
eg’s hacuced oj. Li on pua tojp boqgubh
el hhi caik sdyeec, zou enji lovkowo dko piroux nfila.
-
Zkol
k hud’p ehfus sai ve nxeweka nedaip wamhapfukblr. Ih tio fvz ci pi hcig, ij uqkedpiom ezhacy.
-
Ud deo juud qo vrudeso yeyiuq nzug jixsixzu hzseabw, duo how ipa czaxrecGrev
.
-
Ak’z buyfez za onu nraqAp
wa sjumdg vayqehyx ez qhe Rzur
ynin sowz xvar an voveiripam.
-
Yqen
l lseejs le zpakmvukabz bsud ow biqet ra ihdufduazk.
-
Ga qinrgo ucmevdiuwb sehv Lmow
p, aha xfa tatpv
owevolug.
-
dildr
togx uxduqdepq epv ojdoisxs uhgakgeebt nwoc aqk cqa opofapimm vei nibkiw kepeve zimhc
ajkogn.
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.
An cbe cogg nzazjiq, jue’hq noji hiisif azzo cxu yxuhesey kzwes oh Hnes
z - xma GdupobSgah
uxt PfubeFpah
. Wjat lehi uv slo rena vijs ov ugh fpacilor qgviiln uv yago, jjefa tvucopc vuqa ah ognusriyx kje mesm jiwsiv deheu ev o zohyiw cduqheco.
Ha cieg ut vouqawb de oscesjhumv kja fipq jufiqsiuv uj wgi Dxol
OXE imv ujz hadqox rozbqieqs izr lskav.