Hi everybody. In asynchronous programming processing, people often encounter situations where multiple tasks are handled at the same time, or task tasks are processed one after another, one task depends on the results of the other. Today I would like to share some ways to solve this problem using Coroutine.
Consider a situation, where Task_1 is expecting an execution id of Task_2 with the id received from Response of Task_1 and based on the response from Task_2, the conditions and parameters of Task_3 will be changed.
Task_1 → Task_2 with id -> Task_3
How multiple dependent parallel tasks are implemented.
Normally, the first Task will be executed and after receiving the response (Response), data manipulation, the next call will be made.
1 2 3 4 5 6 7 8 9 10 11 | viewModelScope.launch { val data1Response:BaseResponse<Data1>? try{ val call1 = repository.getAPIcall1() } catch (ex: Exception) { ex.printStackTrace() } processData(data1Response) } |
1 2 3 4 | viewModel?.data1?.collect { dataResponse1 -> repository.getAPIcall2() } |
1 2 3 4 | viewModel?.data1?.collect { dataResponse2 -> repository.getAPIcall3() } |
Like the above implementation with the service tasks that depend on each other. We need to Sync up before manipulating the data, as well as processing with the next task.
With Couroutine there are a number of approaches, more optimal processing to handle data asynchronously and teaming results to implement the Business Logic of the problem.
Here are some approaches.
1. Concurrent Approach with Wait Time async-await with Kotlin Coroutines
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | viewModelScope.launch { val data1Response:BaseResponse<Data1>? val data2Response: BaseResponse<Data2>? val data3Response: BaseResponse<Data3>? val call1 = async { repository.getAPIcall1()} val call2 = async { repository.getAPIcall2()} val call3 = async { repository.getAPIcall3() } try { data1Response = call1.await() data2Response = call2.await() data3Response = call3.await() } catch (ex: Exception) { ex.printStackTrace() } processData(data1Response, data2Response, data3Response) } |
Async will expect the Response of the task task. Here, Task_1 will provide data that will be synchronized with Task_2, etc. Then data manipulation can be performed with the received and processed Response. But it will have some waiting time between each call.
There is an implementation where we can combine calling multiple tasks at the same time.
1 2 3 4 5 6 7 8 9 | suspend fun fetchData() = coroutineScope { val mergedResponse = listOf( async { getAPIcall1() }, async { getAPIcall2() } ) mergedResponse.awaitAll() } |
2. Concurrent/ parallel Approach with thread switchingwithContext() will switch to seperate thread
It is similar to async-await. But somewhere will save money. Instead of deploying on the Main Thread, withcontext will switch to a separate Thread and perform the task. It won’t have a timeout like async-await.
If runBlocking is added overlapping withContext(), it will reverse the asynchronous and destructible nature of Coroutines and block the thread. Until the specific task is completed.
There are 5 types of Dispatchers. IO, Main, Default, New Thread, Unconfined
- Default Dispatcher : This is the default dispatcher in most Coroutine systems. It uses a fixed thread pool to execute coroutines. While it’s useful for simple operations, it’s not suitable for resource-intensive or long-running operations.
- IO Dispatcher : This Dispatcher is optimized for performing slow I/O operations, such as reading/writing data from disk or the network. Instead of using a fixed thread pool, IO Dispatcher usually uses some special I/O threads to make the most of system resources.
- Unconfined Dispatcher : This type of dispatcher allows the coroutine to run on any thread. It is not associated with any particular thread and allows the coroutine to switch between threads during execution. This can be useful in some special cases, but can also cause problems with task synchronization and handling.
- New Thread Dispatcher : This Dispatcher creates a new thread for each coroutine executed. This ensures that each coroutine will run independently on a separate thread. However, creating and managing multiple threads can affect performance and system resources.
- Main Dispathcher : Special dispatcher type used to execute coroutines on the main thread of the application. The Main Dispatcher ensures that coroutines running on the main thread are not blocked during execution, to ensure user interface responsiveness.
1 2 3 4 5 6 7 8 | viewModelScope.launch { withContext(Dispatchers.Default) { val apiResponse1 = api.getAPICall1() val apiResponse2 = api.getAPICall2() if (apiResponse1.isSuccessful() && apiResponse2.isSuccessful() { .. } } } |
3. Parallel Approach with data merging
The third approach is a bit different and if we want to have two independent tasks and concatenate them together for fresh response, then Zip Operator will help us to process them in parallel and give us the result. the results we need.
1 2 3 4 5 6 7 8 9 10 11 12 | repository.getData1() .zip(repository.getData2()) { data1, data2 -> return@zip data1 + data2 } .flowOn(Dispatchers.IO) .catch { e -> .. } .collect { it -> handleSuccessResponse(..) } |
summary
Above, I have introduced 3 approaches and implementations, which you can use for each specific problem and requirement:
- When we want to call multiple Tasks in parallel with timeouts, then the async-await approach will be suitable.
- When we want a more efficient approach to switching Threads, withcontext will be suitable
- And to concatenate two Responses together and do some data manipulation, the zip operator method is suitable.
Hope this article helps you more or less about some ways, some options to deal with all tasks, specifically here with Couroutine. See you all in the next post.