Question or issue of Kotlin Programming:
I’m trying to update a list inside the adapter using async, I can see there is too much boilerplate.
Is it the right way to use Kotlin Coroutines?
can this be optimized more?
fun loadListOfMediaInAsync() = async(CommonPool) { try { //Long running task adapter.listOfMediaItems.addAll(resources.getAllTracks()) runOnUiThread { adapter.notifyDataSetChanged() progress.dismiss() } } catch (e: Exception) { e.printStackTrace() runOnUiThread {progress.dismiss()} } catch (o: OutOfMemoryError) { o.printStackTrace() runOnUiThread {progress.dismiss()} } }
How to solve this issue?
Solution no. 1:
After struggling with this question for days, I think the most simple and clear async-await pattern for Android activities using Kotlin is:
override fun onCreate(savedInstanceState: Bundle?) { //... loadDataAsync(); //"Fire-and-forget" } fun loadDataAsync() = async(UI) { try { //Turn on busy indicator. val job = async(CommonPool) { //We're on a background thread here. //Execute blocking calls, such as retrofit call.execute().body() + caching. } job.await(); //We're back on the main thread here. //Update UI controls such as RecyclerView adapter data. } catch (e: Exception) { } finally { //Turn off busy indicator. } }
The only Gradle dependencies for coroutines are: kotlin-stdlib-jre7
, kotlinx-coroutines-android
.
Note: Use job.await()
instead of job.join()
because await()
rethrows exceptions, but join()
does not. If you use join()
you will need to check job.isCompletedExceptionally
after the job completes.
To start concurrent retrofit calls, you can do this:
val jobA = async(CommonPool) { /* Blocking call A */ }; val jobB = async(CommonPool) { /* Blocking call B */ }; jobA.await(); jobB.await();
Or:
val jobs = arrayListOf>(); jobs += async(CommonPool) { /* Blocking call A */ }; jobs += async(CommonPool) { /* Blocking call B */ }; jobs.forEach { it.await(); };
Solution no. 2:
How to launch a coroutine
In the kotlinx.coroutines
library you can start new coroutine using either launch
or async
function.
Conceptually, async
is just like launch
. It starts a separate coroutine which is a light-weight thread that works concurrently with all the other coroutines.
The difference is that launch returns a Job
and does not carry any resulting value, while async
returns a Deferred
– a light-weight non-blocking future that represents a promise to provide a result later. You can use .await()
on a deferred value to get its eventual result, but Deferred
is also a Job
, so you can cancel it if needed.
Coroutine context
In Android we usually use two context:
uiContext
to dispatch execution onto the Android mainUI
thread (for the parent coroutine).bgContext
to dispatch execution in background thread (for the child coroutines).
Example
//dispatches execution onto the Android main UI thread private val uiContext: CoroutineContext = UI //represents a common pool of shared threads as the coroutine dispatcher private val bgContext: CoroutineContext = CommonPool
In following example we are going to use CommonPool
for bgContext
which limit the number of threads running in parallel to the value of Runtime.getRuntime.availableProcessors()-1
. So if the coroutine task is scheduled, but all cores are occupied, it will be queued.
You may want to consider using newFixedThreadPoolContext
or your own implementation of cached thread pool.
launch + async (execute task)
private fun loadData() = launch(uiContext) { view.showLoading() // ui thread val task = async(bgContext) { dataProvider.loadData("Task") } val result = task.await() // non ui thread, suspend until finished view.showData(result) // ui thread }
launch + async + async (execute two tasks sequentially)
Note: task1 and task2 are executed sequentially.
private fun loadData() = launch(uiContext) { view.showLoading() // ui thread // non ui thread, suspend until task is finished val result1 = async(bgContext) { dataProvider.loadData("Task 1") }.await() // non ui thread, suspend until task is finished val result2 = async(bgContext) { dataProvider.loadData("Task 2") }.await() val result = "$result1 $result2" // ui thread view.showData(result) // ui thread }
launch + async + async (execute two tasks parallel)
Note: task1 and task2 are executed in parallel.
private fun loadData() = launch(uiContext) { view.showLoading() // ui thread val task1 = async(bgContext) { dataProvider.loadData("Task 1") } val task2 = async(bgContext) { dataProvider.loadData("Task 2") } val result = "${task1.await()} ${task2.await()}" // non ui thread, suspend until finished view.showData(result) // ui thread }
How to cancel a coroutine
The function loadData
returns a Job
object which may be cancelled. When the parent coroutine is cancelled, all its children are recursively cancelled, too.
If the stopPresenting
function was called while dataProvider.loadData
was still in progress, the function view.showData
will never be called.
var job: Job? = null fun startPresenting() { job = loadData() } fun stopPresenting() { job?.cancel() } private fun loadData() = launch(uiContext) { view.showLoading() // ui thread val task = async(bgContext) { dataProvider.loadData("Task") } val result = task.await() // non ui thread, suspend until finished view.showData(result) // ui thread }
The complete answer is available in my article Android Coroutine Recipes
Solution no. 3:
I think you can get rid of runOnUiThread { ... }
by using UI
context for Android applications instead of CommonPool
.
The UI
context is provided by the kotlinx-coroutines-android module.
Solution no. 4:
We also have another option. if we use Anko library , then it looks like this
doAsync { // Call all operation related to network or other ui blocking operations here. uiThread { // perform all ui related operation here } }
Add dependency for Anko in your app gradle like this.
implementation "org.jetbrains.anko:anko:0.10.5"
Solution no. 5:
Like sdeff said, if you use the UI context, the code inside that coroutine will run on UI thread by default. And, if you need to run an instruction on another thread you can use run(CommonPool) {}
Furthermore, if you don’t need to return nothing from the method, you can use the function launch(UI)
instead of async(UI)
(the former will return a Job
and the latter a Deferred<Unit>
).
An example could be:
fun loadListOfMediaInAsync() = launch(UI) { try { withContext(CommonPool) { //The coroutine is suspended until run() ends adapter.listOfMediaItems.addAll(resources.getAllTracks()) } adapter.notifyDataSetChanged() } catch(e: Exception) { e.printStackTrace() } catch(o: OutOfMemoryError) { o.printStackTrace() } finally { progress.dismiss() } }
If you need more help I recommend you to read the main guide of kotlinx.coroutines and, in addition, the guide of coroutines + UI
Solution no. 6:
If you want to return some thing from background thread use async
launch(UI) { val result = async(CommonPool) { //do long running operation }.await() //do stuff on UI thread view.setText(result) }
If background thread is not returning anything
launch(UI) { launch(CommonPool) { //do long running operation }.await() //do stuff on UI thread }
Solution no. 7:
All the above answers are right, but I was having a hard time finding the right import for the UI
from kotlinx.coroutines
, it was conflicting with UI
from Anko
.
Its
import kotlinx.coroutines.experimental.android.UI
Solution no. 8:
Here’s the right way to use Kotlin Coroutines. Coroutine scope simply suspends the current coroutine until all child coroutines have finished their execution. This example explicitly shows us how child coroutine
works within parent coroutine
.
An example with explanations:
fun main() = blockingMethod { // coroutine scope launch { delay(2000L) // suspends the current coroutine for 2 seconds println("Tasks from some blockingMethod") } coroutineScope { // creates a new coroutine scope launch { delay(3000L) // suspends this coroutine for 3 seconds println("Task from nested launch") } delay(1000L) println("Task from coroutine scope") // this line will be printed before nested launch } println("Coroutine scope is over") // but this line isn't printed until nested launch completes }
Hope this helps.
Solution no. 9:
Please find attached the implementation for a remote API call with Kotlin Coroutines & Retrofit library.
import android.view.View import android.util.Log import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.test.nyt_most_viewed.NYTApp import com.test.nyt_most_viewed.data.local.PreferenceHelper import com.test.nyt_most_viewed.data.model.NytAPI import com.test.nyt_most_viewed.data.model.response.reviews.ResultsItem import kotlinx.coroutines.* import javax.inject.Inject class MoviesReviewViewModel @Inject constructor( private val nytAPI: NytAPI, private val nytApp: NYTApp, appPreference: PreferenceHelper ) : ViewModel() { val moviesReviewsResponse: MutableLiveData> = MutableLiveData() val message: MutableLiveData = MutableLiveData() val loaderProgressVisibility: MutableLiveData = MutableLiveData() val coroutineJobs = mutableListOf() override fun onCleared() { super.onCleared() coroutineJobs.forEach { it.cancel() } } // You will call this method from your activity/Fragment fun getMoviesReviewWithCoroutine() { viewModelScope.launch(Dispatchers.Main + handler) { // Update your UI showLoadingUI() val deferredResult = async(Dispatchers.IO) { [email protected] nytAPI.getMoviesReviewWithCoroutine("full-time") } val moviesReviewsResponse = deferredResult.await() [email protected] = moviesReviewsResponse.results // Update your UI resetLoadingUI() } } val handler = CoroutineExceptionHandler { _, exception -> onMoviesReviewFailure(exception) } /*Handle failure case*/ private fun onMoviesReviewFailure(throwable: Throwable) { resetLoadingUI() Log.d("MOVIES-REVIEWS-ERROR", throwable.toString()) } private fun showLoadingUI() { setLoaderVisibility(View.VISIBLE) setMessage(STATES.INITIALIZED) } private fun resetLoadingUI() { setMessage(STATES.DONE) setLoaderVisibility(View.GONE) } private fun setMessage(states: STATES) { message.value = states.name } private fun setLoaderVisibility(visibility: Int) { loaderProgressVisibility.value = visibility } enum class STATES { INITIALIZED, DONE } }