To understand flows better we need to revisit suspend functions and coroutines.
What is a suspend function?
A suspending function is simply a function that can be paused and resumed at a later time. How does it do that? The compiler is doing the magic here. The compiler is just taking code that looks procedural and turning it into callbacks under the hood. Continuation is the object which tracks where to pause and where to resume.
Let’s assume we have the below code which has 2 suspension points.
val a = a() val y = foo(a).await() // suspension point #1 b() val z = bar(a, y).await() // suspension point #2 c(z)
This is what the compiler generates internally. There are three states for this block of code:
- initial (before any suspension point)
- after the first suspension point
- after the second suspension point
class <anonymous_for_state_machine> extends SuspendLambda<...> { // The current state of the state machine int label = 0 // local variables of the coroutine A a = null Y y = null void resumeWith(Object result) { if (label == 0) goto L0 if (label == 1) goto L1 if (label == 2) goto L2 else throw IllegalStateException() L0: // result is expected to be `null` at this invocation a = a() label = 1 result = foo(a).await(this) // 'this' is passed as a continuation if (result == COROUTINE_SUSPENDED) return // return if await had suspended execution L1: // external code has resumed this coroutine passing the result of .await() y = (Y) result b() label = 2 result = bar(a, y).await(this) // 'this' is passed as a continuation if (result == COROUTINE_SUSPENDED) return // return if await had suspended execution L2: // external code has resumed this coroutine passing the result of .await() Z z = (Z) result c(z) label = -1 // No more steps are allowed return } }
Continuation object tracks the state of the suspend function. It updates the label as we move from one suspension point to another. In other words, compiler generates the code which tracks the state of suspend function across different suspension points
What is coroutine context and scope?
The Coroutine context determines on which thread the coroutines will run. There are four options:
Dispatchers.Default – for CPU-intense work (e.g. sorting a big list)
Dispatchers.Main – this will depend on what you’ve added to your programs runtime dependencies (e.g.
kotlinx-coroutines-android, for the UI thread in Android)
Dispatchers.Unconfined – runs coroutines unconfined on no specific thread
Dispatchers.IO – for heavy IO work (e.g. long-running database queries)
Coroutine Scope is a way to keep track of all coroutines that run in it. Every coroutine must run in a scope. Structured concurrency in Kotlin Coroutines requires developers to always launch coroutines in the context of CoroutineScope or to specify a scope explicitly.
A coroutine is typically launched using launch coroutine builder:
fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, // … ): Job
It is defined as an extension function on CoroutineScope
and takes a CoroutineContext
as a parameter, so it actually takes two coroutine contexts (since a scope is just a reference to a context). What does it do with them? It merges them using the plus operator, producing a set union of their elements.
Ok, Now where does flow fit in?
A Flow is used to represent a sequential emission of values that, at some point, ends (because it naturally ends or something bad happens).
All the operations in a Flow are executed sequentially inside the same block of code (and, therefore, the same Coroutine Scope).
Behind the scenes, a Flow is just an interface that exposes a method to collect the emitted values:
fun interface FlowCollector<T> { suspend fun emit(value: T) } interface Flow<T> { suspend fun collect(collector: FlowCollector<T>) }
Job Offers
These are simply 2 methods that are marked suspend and flow and collector are just function calls they work in tandem.
If we look at the compiled code we will find that it simply uses suspend function to create a continuation object which marks how both flow and collecter work in tandem.
it uses suspend function to create compiled code which is basically sequential in nature and scope to control the lifecycle of the flow.
Sources:
https://kt.academy/article/how-flow-works
This article was originally published on proandroiddev.com on December 16, 2022