📜 ⬆️ ⬇️

Patterns and antipatterns Corutin in Kotlin

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 exception may occur in an async block, do not rely on the try/catch .


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

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


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 Exception fun doWork(): Deferred<String> = scope.async { ... } fun loadData() = 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 supervisorJob. Thus, the code below will still crash your application, because async runs as part of the parent cortina (1).


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

Another way to avoid failure, which is preferable, is to wrap the async in a 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 Exception fun doWork(): Deferred<String> = coroutineScope { // (1) async { ... } } fun loadData() = scope.launch { // (2) try { doWork().await() } catch (e: Exception) { ... } } 

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


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) fun login() = scope.launch { withContext(Dispatcher.Main) { view.showLoading() } // (2) networkClient.login(...) withContext(Dispatcher.Main) { view.hideLoading() } // (2) } 

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


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


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

Avoid using unnecessary async / await


If you use the async function and immediately call await , then you should stop doing this.


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

If you want to switch the context of the cortina and immediately suspend the parent cortina, then withContext is the preferred method.


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

From a performance point of view, this is not such a big problem (even if we consider that async creates a new coruntine to do the job), but semantically async implies 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.


 class WorkManager { val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) fun doWork1() { scope.launch { /* do work */ } } fun doWork2() { scope.launch { /* do work */ } } fun cancelAllWork() { job.cancel() } } fun main() { 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 running under the completed job will not be executed (1).


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


 class WorkManager { val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) fun doWork1(): Job = scope.launch { /* do work */ } // (2) fun doWork2(): Job = scope.launch { /* do work */ } // (2) fun cancelAllWork() { scope.coroutineContext.cancelChildren() // (1) } } fun main() { val workManager = WorkManager() workManager.doWork1() workManager.doWork2() workManager.cancelAllWork() workManager.doWork1() } 

Avoid writing a pause function using an implicit dispatcher.


Do not write the suspend function, the execution of which will depend on the particular dispatcher korutin.


 suspend fun login(): 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 cortina that the main dispatcher will not use.


 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.


 suspend fun login(): 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 crash val loginResult = login() ... } launch(Dispatcher.Default) { // (2) no crash ether val loginResult = login() ... } 

Avoid using global scope


If you use GlobalScope everywhere in your Android application, you should stop doing this.


 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 normally 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.


 class MainActivity : AppCompatActivity(), CoroutineScope { private val job = SupervisorJob() override val coroutineContext: CoroutineContext get() = Dispatchers.Main + job override fun onDestroy() { super.onDestroy() coroutineContext.cancelChildren() } fun loadData() = launch { // code } } 

')

Source: https://habr.com/ru/post/432942/


All Articles