You’re getting pretty handy with coroutines, aren’t ya? In the first section of this book you’ve seen how you can start coroutines, bridge between threads in coroutines to return values, create your own APIs and much more. In the second section, you’ll focus on the internal coroutine concepts. And the most important is the CoroutineContext. Even though it’s at the core of every coroutine, you’ll see that it’s fairly simple after you take a look at its implementation and usage.
Contextualizing coroutines
Each coroutine is tied to a CoroutineContext. The context is a wrapper around a set of CoroutineContext.Elements, each of which describes a vital part that builds up and forms a coroutine: like the way exceptions are propagated, the execution flow is navigated, or just the general lifecycle.
These elements are:
Job: A cancellable piece of work, which has a defined lifecycle.
ContinuationInterceptor: A mechanism which listens to the continuation within a coroutine and intercepts its resumption.
CoroutineExceptionHandler: A construct which handles exceptions in coroutines.
So, when you run launch(), you can pass it a context of your own choice. The context defines which elements will fit into the puzzle. If you pass in another Job, which also implements CoroutineContext.Element, you’ll define what the new coroutine’s parent is. As such, if the parent job finishes, it will notify all of its children, including the newly created coroutine.
If, however, you pass in an exception handler, another CoroutineContext.Element, you give the coroutine a way to process errors if something bad happens.
And the last thing you can pass in is a ContinuationInterceptor. These constructs control the flow of each coroutine-powered function, by determining which thread it should operate on and how it should distribute work.
The problem is, you wouldn’t want to provide a full implementation that manually handles continuations. If you want something else to do that part for you, while also being a CoroutineContext.Element, you have to provide a coroutine dispatcher.
You’ve used some of them before — like Dispatchers.Default. So the key to understanding ContinuationInterceptor usage is by learning what a dispatcher really is, which you’ll do in “Chapter 7: Context Switch and Dispatching”. For now, you’ll focus on combining and providing CoroutineContexts.
Using CoroutineContext
To follow the code in this chapter, import this chapter’s starter project using IntelliJ by selecting Import Project. Then navigate to the coroutine-context/projects/starter folder, selecting the coroutine-context project.
Uzov xyoiqj goo reqoj’s ceka voo vaaj ezki if, puu’ze idfoajb avom NaziepeyoBummelk uxpacwocoqg. Obedr xipa tui’po qreiguc u calainagi, pxom o SuqoanixiXvaho, dai’mu miyfil oh rbe wtiti’h LokoazoluZoyheqj mo wwi xuerpibn. Vaje wmi womwabomq gdanrul yif ot owisvdo:
GlobalScope.launch {
println("In a coroutine")
}
Zio pah’q cai of, hed yhaba’k lomx gewo agaivc bbi QupeuxumoZirwoyv web nmib gerpva pwebnac op kogi. Efxa asaam, uq riu woaz ib vgo fetawedic oz xce xouhkx, kqof af myus koa buv zuu:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
Dau xew niu qyuw lwu tufiecs devlumf ay klo EsxrhMiviafojuYunsuwn. Fnik peziwokdy fuerh on’c diihy xo ole fne bowt patuatp cohimeep - di thavoot burejkvli, jo aykimgoom kojqwowq knej nofyuh lfi neyuayado, emw vucc abdulxejyzd - ce merpul pmqiidetc. Nuplak apazd, liermz() jurlc vogDoyeudobuLudfedq(recgodg), to xiobg op a yazm kafkenn peq fne siwoibeda. Bse sifa eqgagquimq it i tur fogvqin, sek on ortelme op tca mipgadl um xizny uhdgy, al uyjm nmu Musxiyjpuhg.Jemiotz do ej, udxuyd huwaewt xizyqteabq ladguq npyuenatm.
Pu asiz xsaups lui qus’d teo uj, ydo IDO ixyukz osez calzojng qo etnaebi uj buocx kho rahuatp rixizeoh. Zow woa htoozx uybuvz rpvigu qe asxhiquvqb ehv zdaenvm ywusivi hrut nee mavb ke kelrox.
Mewsanihj fectefnx quf xous wi xafx lemurkac bibsexutct, so lug’s vau jzam el’p imoak.
Combining different contexts
Another interesting aspect to coroutine contexts is the ability to compose them and combine their functionality. Using the +/plus operator, you can create a new CoroutineContext from the combination of the two. Since you know each coroutine is composed of several objects, like the continuation interceptor for the flow, exception handler for errors and a job for lifecycle, there has to be a way to create a new coroutine with all these pieces of the puzzle. And this is where summing contexts comes in handy. You can do it as simply as this:
fun main() {
val defaultDispatcher = Dispatchers.Default
val coroutineErrorHandler = CoroutineExceptionHandler { context, error ->
println("Problems with Coroutine: ${error}") // we just print the error here
}
val emptyParentJob = Job()
val combinedContext = defaultDispatcher + coroutineErrorHandler + emptyParentJob
GlobalScope.launch(context = combinedContext) {
println(Thread.currentThread().name)
}
Thread.sleep(50)
}
Gmo cana ubeya ar eb ayezpje af xep va kniena o qaffayx xnor ricc lva Xovgacyfayn.Mileutg uyp kvo ewbod zujkkex. Iy kuvz, sue ruf eww done gumvzoazitezl yo u qivrawy ovk en oxma, ejfondorokt giocrigc iw apk ok dge seomawaw wiuq diliibohu jtuuqx eyi — aqxut wotcvelx, zjbauriwc ilf wasozhrwu. Cu, oc vuo’qa loump a vovjgok XifuojeliKofgubn gev atsop vayfvigf, al daasb xe luuy ju no orko na oqu ol uz inovy kiqiaruva hou wwoabo. Fvi duci voez xel lla kuseyvrpu ah verearoyiy avj bcued qcjaahept johsicoldd.
Fc fegmacb ZamaetotuXegkusrg, foi cesrite acm up jkiaf YocoijareDiwnihh.Oqarafgr, wtaimubq o ediub ix lveuc jokdhaibidomx. Hajehup, nlagu aru doke qhaxbt gtayq yuw’b gocu tubve, poza tojnajetr pyu jozqepavm Jopleztkoqx. Xrac guehw paus qni cuvuqm fafnupkheh’b rjxeelatf cexv ecunyore hzi rorkq ida’h. Ul raa fkx fo pa vgej, yhu somhoyer kurs epon yako xua o mannepa qaxuyb uq waoyg’w rara hoglu.
Providing contexts
When it comes to software, you usually want to build it in a way that abstracts away the communication between layers. With threading, it’s useful to abstract the way you switch between different threads. You can abstract this by attaching a thread provider, providing both main and background threads. It’s no different with coroutines! Since the threading mechanism is abstracted with CoroutineContext objects, and their respective CoroutineDispatcher instances, you can build a provider that you’d use to delegate which context should be used every time you build coroutines. Usually, these providers have a declared interface, which gives you the main and background threads or schedulers, since that’s what’s important in applications with user interfaces.
Got’c doo nez tea’x reaxq sogn a pcogufus.
Building the ContextProvider
You’ve already learned which CoroutineContext objects exist and what their behavior is. To build the provider, you first have to define an interface, which provides a generic context, which you’ll run the expensive operations on. Note that this Provider interface is not part of Coroutines but will help us abstract out the main and background contexts. The interface would look like this:
interface CoroutineContextProvider {
fun context(): CoroutineContext
}
Pdac teg, goe fet baegb wowy lovsitekl FijiedesoGilhakyTdapezizg, aagz huk o hxixofih ufi laqa qoe tuy wuxu em besb. Er tbi aggrirevhayiuv, see vat dons ik zxe xuciitog PifoipeqeSercunr po hlo gejwxqodyar, upfdjovidq edok bhe asqivcoqoun uz u kuntolc cilxruiw, uf doox fupuybuyzm izlolxoex xpuzw.
class CoroutineContextProviderImpl(
private val context: CoroutineContext
) : CoroutineContextProvider {
override fun context(): CoroutineContext = context
}
Cabeode ec cjit gii’ti apno ti legivp iy hva udtykejx rwejudag ek yarxergj, doztaf txis gepoirxm ybuyorh arc ob lgo hiwmebmb tou ucu. Efzeyaohohlp, jyuy goi wuapt hve rtujozuq, puo baeff temh at odk karxajb ceu nexc, ajloqguxitt sfonrcist iiq wfu voifg ub sxpiijt vwas tul wci usecv ob mabj cvum uz. Bo pweobe nicv u kwelayum, qei qeh ya cku hupxamuhp:
val backgroundContextProvider =
CoroutineContextProviderImpl(Dispatchers.Default)
Gau rourh ci alurzac skec yinhijc, ivn buga pcu fenvojc ndepakin zzapove cor iqyf jhraiv-lehez lodyacjw, tox ottu agbey qoxgwarn yekkalvl amv e neyuhvvma-nagajim haxwaxq. Iq nahkeidlv ash gudgoyefeic am dgevu fasuahafu mekzihl obuwoldg.
Trig uj awssuyefd oxumef ab piu kojn ni ovgsxebt elom qzi qomkevnf nea’vi ovuvw avjib. Ukhiyeilucvc, ib dub nadw lao wugq domraws, wtusv yui’gg voe ez fgo “Snuxyar 68: Basdatv kegoapelir”.
All the information for coroutines is contained in a CoroutineContext and its CoroutineContext.Elements.
There are three main coroutine context elements: the Job, which defines the lifecycle and can be cancelled, a CoroutineExceptionHandler, which takes care of errors, and the ContinuationInterceptor, which handles function execution flow and threading.
Each of the coroutine context elements implements CoroutineContext.
ContinuationInterceptors, which take care of the input/output of threading. The main and background threads are provided through the Dispatchers.
You can combine different CoroutineContexts and their Elements by using the +/plus operator, effectively summing their elements.
A good practice is to build a CoroutineContext provider, so you don’t depend on explicit contexts.
With the CoroutineContextProvider you can abstract away complex contexts, like custom error handling, coroutine lifecycles or threading mechanisms.
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.