Deep Dive Into Android Lifecycle Coroutines

Halil Ozercan
Jan 12 · 8 min read
Image for post
Image for post

Lifecycle Coroutine Extensions were introduced some time ago to ease the usage of coroutines in Android world. You can learn more about them here.

In this post, I’m going to assume the reader already has experimented with lifecycleCoroutineScope in either at least one activity or fragment. Although the documentation gives quite good information about how to use these extension scopes, I find it’s a little mystical about how these extensions are actually implemented.

In this post, we are going to read the source code together and make sense of some coroutine concepts with cross-references to some other documentation that we might need.

Most of the article might be redundant for speed-code-readers. Thus, I came up with a repeated structure for the whole post, so those readers can skip through uninteresting parts to them. The structure looks like this;

Subject

  1. 👩‍💻 Code that needs dissecting
  2. 🤓 Reading the code with references to docs
  3. 🏫 Learning points in one or two sentences (highlighted)

Let’s start with our entry point from a LifecycleOwner to the coroutine world.

lifecycleCoroutineScope

👩‍💻 Code

val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope

There is nothing much to talk about here. This extension value is simply a delegate to another extension field that is defined on a Lifecycle object.

🤓 Reading

It’s always a good idea to start the reading from the documentation when it’s available. This time, the documentation is pretty straight-forward. It mentions that this scope is tied to the lifecycle that it’s defined on and will be cancelled when the corresponding lifecycle is DESTROYED . However, our first key take-away is mentioned the last;

This scope is bound to Dispatchers.Main.immediate

We will definitely touch on this while reading the implementation of this special scope. For now, we can continue with the actual implementation of the extension value.

We are immediately in for a surprise from the first line while(true) . That’s quite interesting because why would there be a need to endlessly try to return a coroutine scope? We will get to this in a minute.

val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?

This is also strange but expected. If you check out the source code of ViewModel class as well, you are going to see a similar field called mBagOfTags . These are internal references to hold any extension value related to coroutines or anything else that might come up later. In this case, we see that lifecycle might already be holding to a LifecycleCoroutineScopeImpl from before. In that case, extension value getter returns this existing field.

Otherwise, a new LifecycleCoroutineScopeImpl is initialized and put in this AtomicReference object (mInternalScopeRef) if it’s currently null. At this point, we understand why this function is wrapped in an endless loop.

Here we need to make a note of two things.

  • LifecycleCoroutineScopeImpl requires a coroutineContext and SupervisorJob() + Dispatcher.Main.immediate is passed.
  • Once newScope is successfully put inside mInternalScopeRef , register function is called.

Remembering these 2 points will help us make sense of the actual implementation of this scope.

Learning the difference between Dispatchers.Main and Dispatcher.Main.immediate is crucial before moving forward. I’m going to directly refer to the documentation here. https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-main-coroutine-dispatcher/immediate.html

Returns dispatcher that executes coroutines immediately when it is already in the right context (e.g. current looper is the same as this handler’s looper) without an additional re-dispatch.

Immediate dispatcher is quite important if we are already on the main thread and working on Android Views or animations. This dispatcher enables us to execute any code immediately on the main thread without waiting for scheduling to get into effect.

🏫 Learned

  • lifecycleCoroutineScope is kind of a lazy property
  • lifecycleCoroutineScope uses Main.immediate

Next, we will focus on the class that enables us to mix Lifecycle world with the coroutine world;

LifecycleCoroutineScopeImpl

👩‍💻 Code

I removed the repetitive documentation parts from LifecycleCoroutineScope abstract class to make it more compact for readability.

🤓 Reading

This class looks fun and more approachable. We should check out the base class and which interfaces are implemented before checking out the default constructor because it clearly uses override values.

LifecycleCoroutineScope: An abstract class that holds onto a lifecycle object. It has no abstract functions but defines 3 different functions that can launch new coroutines on the given lifecycle from the parent. To make things more confusing, it extends from CoroutineScope. So, it can launch coroutines in on itself.

LifecycleEventObserver: Implementing this interface enables LifecycleCoroutineScopeImpl to add itself as an observer to any lifecycle object. I believe things started to get real confusing for me at this point.

Looking at the constructor, we see that this class simply requires a coroutineContext to make itself a proper CoroutineScope and a lifecycle object to listen to lifecycle events. As we all know that this scope will respect the lifecycle, that’s the whole point.

The class definition starts with an init statement right away. It is there to check whether this lifecycle is already destroyed because this class might be initialized from a thread different thread than Main.

We remember the register function from lifecycleCoroutineScope extension value. It is called right after this class is initialized. The sole purpose of this function is to a launch a new coroutine to be ran in the Main thread to start listening to lifecycle events. It is crucial that this check runs on Main dispatcher because accessing lifecycle state from any other thread is considered unsafe.

register adds this class as an observer to the given lifecycle property, which brings us to onStateChanged . Again, this function has a very basic purpose; cancel the coroutineContext when lifecycle is destroyed.

🏫 Learned

  • LifecycleCoroutineScopeImpl is a CoroutineScope
  • Through listening to lifecycle events, LifecycleCoroutineScopeImplcan cancel itself when lifecycle is DESTROYED.

Let’s summarize our findings to understand our current toolbox better.

We have access to a special kind of CoroutineScope through either a LifecycleOwner or Lifecycle object. This scope only starts if the lifecycle is initialized and gets cancelled when the lifecycle is destroyed. Also, this scope uses Dispatchers.Main.immediate as coroutine context.

Anyone can launch a coroutine on LifecycleCoroutineScopeImpl at anytime. However, the real power behind this scope comes from the functions defined under LifecycleCoroutineScope abstract class. These functions take the form of launchWhenX . They are very similar to each other in a sense that they are executed when lifecycle is at least in stage X which can be either CREATED , STARTED , or RESUMED .

📣 Important Notice: launchWhenX are not replacements to onCreate, onStart, or onResume in activities or fragments. They only guarantee that the lambda that’s passed to the function is executed when the conditions in lifecycle is ready. The execution can be immediate, or at a future point.

Before deep diving into these functions, we should see what is the difference between them.

= lifecycle.whenCreated(block)
= lifecycle.whenStarted(block)
= lifecycle.whenResumed(block)

Ok.. These look like simple delegations. Let’s go one step further.

whenStateAtLeast(Lifecycle.State.CREATED, block)
whenStateAtLeast(Lifecycle.State.STARTED, block)
whenStateAtLeast(Lifecycle.State.RESUMED, block)

Hmm.. They are actually calling the same function with different parameters. Maybe deep diving directly into this function makes more sense. However, we should not forget that

fun launchWhenCreated(block: suspend CoroutineScope.() -> Unit): Job = launch {

All launchWhenX functions start with a launch call on LifecycleCoroutineScope . So, whenStateAtLeast suspendable function is actually called on this new coroutine. We should keep that in mind while evaluating.

whenStateAtLeast

👩‍💻 Code

🤓 Reading

Here we finally have the real deal because things will get very tricky from now on. First of all, we should always remember that we are simply in a suspendable function which is supposed to run in a LifecycleCoroutineScope ‘s child coroutine. Let’s start from first line;

withContext(Dispatchers.Main.immediate)

Once again, we are making sure whatever that is going to happen in this function, happens in the main thread.

val job = coroutineContext[Job] ?: error(...

Access the job of parent coroutine. https://kotlinlang.org/docs/reference/coroutines/coroutine-context-and-dispatchers.html#job-in-the-context

val dispatcher = PausingDispatcher()

PausingDispatcher is one of the few cases that I’m not going to dive in because it deserves its own post. In a nutshell, PausingDispatcher is a special kind of CoroutineDispatcher that includes a dispatching queue which can be paused and resumed at any given time.

val controller =
LifecycleController(this@whenStateAtLeast, minState, dispatcher.dispatchQueue, job)

LifecycleController will be the glue that holds everything together at this point. It has access to the minimum lifecycle state that is required to dispatch our coroutine in a PausingDispatcher . I also included the source code for this class.

LifecycleController has a very similar structure to LifecycleCoroutineScopeImpl . It is a lifecycle event observer that manipulates the dispatchQueue upon lifecycle changes. When state is at least minState , queue resumes. If lifecycle is destroyed, parentJob gets cancelled. Finally, it pauses the dispatchQueue in any other case. By simply creating this controller, lifecycle events and PausingDispatcher are linked together.

try {
withContext(dispatcher, block)
} finally {
controller.finish()
}

Finally, we simply run the given block in this special dispatcher. Also, controller is informed to seize its observation when the execution ends.

In the end, we have a way of running coroutines that are paused, resumed, and cancelled upon significant lifecycle events and these coroutines are dispatched in Dispatchers.Main.immediate + PausingDispatcher .

📣 Important Takeaway: launchWhenX functions are not cancelled when lifecycle leaves the desired state. They simple get paused. Cancellation happens only if lifecycle reaches DESTROYED state. This is way too important if you are collecting flows in one of these coroutines. Your collection won’t end if your activity or fragment simply goes to background. It will only get paused. So, you don’t have to restart your collection at each onStart or onResume.

🏫 Learned

  • launchWhenX functions also use Dispatchers.Main.immediate
  • whenStateAtLeast suspend functions do not actually depend on usage of LifecycleCoroutineScope . They can be called from any coroutine.
  • Be very careful if you are going to use withContext or flowOn in launchWhenX functions because they will modify the context(PausingDispatcher) that your code runs in. Do not forget that the parent coroutineScope still gets cancelled at only DESTROYED event. Changing the dispatcher might have unforeseen consequences if not thoroughly tested.
  • Everytime you call a launchWhenX function, a new LifecycleController , a new PausingDispatcher gets allocated. Don’t be very generous with running too many coroutines with these launcher functions.

In this article, I’ve tried to get into the implementation of coroutine extensions and learn its inner workings. Coroutines are easier to grasp but harder to master and there are many small details that might go missing from the unexperienced eye. Those small things usually come back to bite you when they are the least expected. Thus, I believe it’s beneficial for everyone to have at least one skim through over the actual code that implement the API that we will be using hopefully for a long time.

The Startup

Medium's largest active publication, followed by +771K people. Follow to join our community.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store