Patterns and antipatterns Corutin in Kotlin

Original author: Dmytro Danylyk
  • Transfer

Patterns and antipatterns Corutin in Kotlin


I decided to write about some things that, in my opinion, should and should not be avoided when using Kotlin Korutin.


Wrap asynchronous calls with coroutineScope or use SupervisorJob to handle exceptions.


If an asyncexception may occur in a block , do not rely on the block try/catch.


val job: Job = Job()
val scope = CoroutineScope(Dispatchers.Default + job)
// may throw ExceptionfundoWork(): Deferred<String> = scope.async { ... }   // (1)funloadData() = scope.launch {
    try {
        doWork().await()                               // (2)
    } catch (e: Exception) { ... }
}

In the example above, the function doWorkstarts a new coruntine (1), which can throw an unhandled exception. If you try to wrap the doWorkblock try/catch(2), the application will still fall.


This is because the failure of any child job component leads to the immediate failure of its parent.


One way to avoid the error is to use SupervisorJob(1).


Failure or cancellation of the child component will not lead to the failure of the parent and will not affect other components.

val job = SupervisorJob()                               // (1)val scope = CoroutineScope(Dispatchers.Default + job)
// may throw ExceptionfundoWork(): Deferred<String> = scope.async { ... }
funloadData() = scope.launch {
    try {
        doWork().await()
    } catch (e: Exception) { ... }
}

Note : this will only work if you explicitly start your asynchronous call within the framework of the coroutine with SupervisorJob. Thus, the code below will still crash your application, because it asyncruns as part of the parent cortina (1).


val job = SupervisorJob()                               
val scope = CoroutineScope(Dispatchers.Default + job)
funloadData() = scope.launch {
    try {
        async {                                         // (1)// may throw Exception 
        }.await()
    } catch (e: Exception) { ... }
}

Another way to avoid failure, which is preferable, is to wrap asyncin coroutineScope(1). Now, when an exception occurs inside async, it cancels all other Korutins created in this area, without touching the external area. (2)


val job = SupervisorJob()                               
val scope = CoroutineScope(Dispatchers.Default + job)
// may throw ExceptionfundoWork(): Deferred<String> = coroutineScope {     // (1)
    async { ... }
}
funloadData() = scope.launch {                       // (2)try {
        doWork().await()
    } catch (e: Exception) { ... }
}

In addition, you can handle exceptions inside the block async.


Use the main controller for root corutin


If you need to perform background work and update the user interface inside your root cororne, start it using the main dispatcher.


val scope = CoroutineScope(Dispatchers.Default)          // (1)funlogin() = scope.launch {
    withContext(Dispatcher.Main) { view.showLoading() }  // (2)  
    networkClient.login(...)
    withContext(Dispatcher.Main) { view.hideLoading() }  // (2)
}

In the example above, we launch root coruntine using the CoroutineScopedefault dispatcher (1). With this approach, each time we need to update the user interface, we will have to switch the context (2).


In most cases, it is preferable to create CoroutineScopeimmediately with the main dispatcher, which will lead to simplified code and less explicit context switching.


val scope = CoroutineScope(Dispatchers.Main)
funlogin() = scope.launch {
    view.showLoading()    
    withContext(Dispatcher.IO) { networkClient.login(...) }
    view.hideLoading()
}

Avoid using unnecessary async / await


If you use the function asyncand immediately call await, then you should stop doing it.


launch {
    valdata = async(Dispatchers.Default) { /* code */ }.await()
}

If you want to switch the context of the coruntine and immediately suspend the parent coruntine, then withContextthis is the most preferred way to do this.


launch {
    valdata = withContext(Dispatchers.Default) { /* code */ }
}

From a performance point of view, this is not such a big problem (even if you consider that it asynccreates a new coruntine to do the job), but semantically asyncimplies that you want to run several coruntines in the background and only then wait for them.


Avoid job cancellation


If you need to cancel the quortenine, do not cancel the job.


classWorkManager{
    val job = SupervisorJob()
    val scope = CoroutineScope(Dispatchers.Default + job)
    fundoWork1() {
        scope.launch { /* do work */ }
    }
    fundoWork2() {
        scope.launch { /* do work */ }
    }
    funcancelAllWork() {
        job.cancel()
    }
}
funmain() {
    val workManager = WorkManager()
    workManager.doWork1()
    workManager.doWork2()
    workManager.cancelAllWork()
    workManager.doWork1() // (1)
}

The problem with the code above is that when we cancel a job, we transfer it to a completed state. Korutin run under the completed job will not be executed (1).


If you want to cancel all corouts in a certain area, you can use the function cancelChildren. In addition, it is good practice to provide the ability to cancel individual job (2).


classWorkManager{
    val job = SupervisorJob()
    val scope = CoroutineScope(Dispatchers.Default + job)
    fundoWork1(): Job = scope.launch { /* do work */ } // (2)fundoWork2(): Job = scope.launch { /* do work */ } // (2)funcancelAllWork() {
        scope.coroutineContext.cancelChildren()         // (1)                             
    }
}
funmain() {
    val workManager = WorkManager()
    workManager.doWork1()
    workManager.doWork2()
    workManager.cancelAllWork()
    workManager.doWork1()
}

Avoid writing a pause function using an implicit dispatcher.


Do not write a function suspendwhose execution will depend on a particular controller korutin.


suspendfunlogin(): Result {
    view.showLoading()
    val result = withContext(Dispatcher.IO) {  
        someBlockingCall() 
    }
    view.hideLoading()
    return result
}

In the example above, the login function is a suspension function and it will fail if you start it from a coroutine that will not be used by the main controller.


launch(Dispatcher.Main) {     // (1) всё в порядкеval loginResult = login()
    ...
}
launch(Dispatcher.Default) {  // (2) возникнет ошибкаval loginResult = login()
    ...
}

CalledFromWrongThreadException: only the source thread that created the hierarchy of View components has access to them.

Create your suspension function so that it can be performed from any dispatcher with quorutine.


suspendfunlogin(): Result = withContext(Dispatcher.Main) {
    view.showLoading()
    val result = withContext(Dispatcher.IO) {  
        someBlockingCall() 
    }
    view.hideLoading()
return result
}

Now we can call our login function from any dispatcher.


launch(Dispatcher.Main) {     // (1) no crashval loginResult = login()
    ...
}
launch(Dispatcher.Default) {  // (2) no crash etherval loginResult = login()
    ...
}

Avoid using global scope


If you use GlobalScopeeverywhere in your Android application, you should stop doing it.


GlobalScope.launch {
    // code
}

The global scope is used to launch top-level corutin, which run for the entire lifetime of the application and are not canceled ahead of time.

Application code should usually be used by the application defined by CoroutineScope , so using async or launch in GlobalScope is highly discouraged.

In Android, korutin can be easily limited to the life cycle of an Activity, Fragment, View, or ViewModel.


classMainActivity : AppCompatActivity(), CoroutineScope {
    privateval job = SupervisorJob()
    overrideval coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job
    overridefunonDestroy() {
        super.onDestroy()
        coroutineContext.cancelChildren()
    }
    funloadData() = launch {
        // code
    }
}

Also popular now: