Skip to content

CoroutineScope, Job, SupervisorJob, ViewModelScope— Discussed in depth to understand better- Part 2

Published: at 7 min read

Table of content

Structured Concurrency - Explain

It is a concept which introduces where

import androidx.lifecycle.ViewModel
import kotlinx.coroutines.*

class MainViewModel : ViewModel() {

    // CoroutineScope associated with the ViewModel
    private val viewModelScope = CoroutineScope(Dispatchers.Main + SupervisorJob())

    fun performBackgroundTask() {
        // Launch a coroutine within the ViewModel scope
        viewModelScope.launch {
            try {
                // Simulate a background task
                delay(2000)
                println("Background task completed")
            } catch (e: CancellationException) {
                // Handle cancellation if needed
                println("Coroutine was canceled")
            }
        }
    }

    override fun onCleared() {
        super.onCleared()
        // Cancel all coroutines when the ViewModel is cleared (e.g., when the associated UI component is destroyed)
        viewModelScope.cancel()
    }
}

In the above example :

Building up Job in Coroutine

Job() is a core concept representing a unit of work that can be executed concurrently. It is a handle to a coroutine or a coroutine-like entity and provides control and information about the state of the coroutine.

Key characteristics of a Job include:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job: Job = launch {
        // Coroutine code
        delay(1000)
        println("Coroutine is completed")
    }

    println("Main thread is still working")

    // Delay to allow the coroutine to complete
    delay(2000)

    // Check if the coroutine is still active
    if (job.isActive) {
        println("Coroutine is still active")
    } else if (job.isCancelled) {
        println("Coroutine was cancelled")
    } else if (job.isCompleted) {
        println("Coroutine completed")
    }
}

In this example:

Using Job provides control over the lifecycle of a coroutine, enabling cancellation, awaiting completion, and managing the hierarchical relationships between coroutines. It’s a fundamental building block in the structured concurrency model.

Supervisor Job

SupervisorJob is a type of Job that is used to supervise the execution of multiple child coroutines. It behaves differently from a regular Job in the way it handles failures and cancellations within its children.

key characteristics of a SupervisorJob:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val supervisorJob = SupervisorJob()

    val coroutineScope = CoroutineScope(Dispatchers.Default + supervisorJob)

    val job1 = coroutineScope.launch {
        println("Child coroutine 1 started")
        delay(1000)
        println("Child coroutine 1 completed")
    }

    val job2 = coroutineScope.launch {
        try {
            println("Child coroutine 2 started")
            delay(2000)
            throw RuntimeException("Something went wrong in coroutine 2")
        } catch (e: Exception) {
            println("Coroutine 2 failed: ${e.message}")
        }
    }

    job1.join()
    job2.join()

    supervisorJob.cancel()
}

In the above example :

Using a SupervisorJob is useful in scenarios where one has a collection of related coroutines running concurrently, and one wants them to operate independently in terms of failures and cancellations.

Structured vs Unstructured Concurrency

Structured ConcurrencyUnstructured Concurrency
Every Coroutine needs to be started in a logical scope with a limited life-time.Threads are started globally. Developers responsibility to keep track of their lifetime.
Coroutines started in a scope form a hierarchy.No hierarchy. Threads run in isolation without any relationship between each other.
A parent job won’t complete, until all of its children have completed.All threads run completely independent from each other.
Cancelling a parent will cancel all children.No automatic cancellation mechanism.
If a child coroutine fails, the exception is propagated upwards and depending on the job type, either all siblings are cancelled or not.No automatic exception handling and cancellation mechanism.

GlobalScope

GlobalScope is a special instance of :

Drawbacks of GlobalScope

  1. No Lifecycle Awareness: does not have lifecycle awareness, so coroutines launched with it won’t be automatically canceled when a specific lifecycle (e.g., an Android Activity or Fragment) is destroyed. This can lead to memory leaks and unexpected behavior.

  2. Risk of Leaked Coroutines: Coroutines launched in GlobalScope can outlive the components that started them. If a coroutine is still active after its intended scope is destroyed, it can lead to resource leaks.

ViewModelScope

viewModelScope is a predefined CoroutineScope provided by the Android Architecture Components library,

class NetworkRequestViewModel(
    private val api MockApi = mockApi()
): BaseViewModel<UiState> {
    fun performNetworkRequest(timeout: Long){
        val job= viewModelScope.launch{
            //will get this line as as output
            Timber.d("I am the first statement in the coroutine")
            try{
                val versions = withTimeout(timeout){
                    api.getRecentAndroidVersions()
                }
                uiState.value = UiState.Success(versions)
            }  catch(exception: Exception){
                Timber.e(exception)
                uiState.value = UiState.Error("Network request failed")
            }
        }
        // will print this line after first Timber statement
        Timber.d("I am the first statement after launching the coroutine")

        job.invokeOnCompletion{ throwable ->
            if(throwable is CancellationException) {
                Timber.d("Coroutine was cancelled")
            }
        }
    }
}

LifecycleScope

import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class MainFragment : Fragment() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Launch a coroutine in the lifecycleScope
        lifecycleScope.launch {
            try {
                // Simulate a background task
                delay(2000)
                println("Background task completed")
            } catch (e: Exception) {
                // Handle exceptions if needed
                println("Coroutine failed: ${e.message}")
            }
        }
    }
}

In the above example :

Summary

Coroutines provide a way to write asynchronous, non-blocking code in a more sequential and readable manner. They are built on top of the Kotlin programming language and leverage its language features to simplify asynchronous programming.

Happy Learning !!!

Share :
Written by:Parita Dey

Interested in Writing Blogs, showcase yourself ?

If you're passionate about technology and have insights to share, we'd love to hear from you! Fill out the form below to express your interest in writing technical blogs for us.

If you notice any issues in this blog post or have suggestions, please contact the author directly or send an email to hi@asdevs.dev.