52

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()}
        }
    }
KTCO
  • 2,115
  • 23
  • 21
Sai
  • 15,188
  • 20
  • 81
  • 121
  • 6
    **Note: Most of the answers are invalid with the changes in API in the stable release of coroutines.** – aksh1618 Mar 03 '19 at 18:36

9 Answers9

46

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<Deferred<Unit>>();
jobs += async(CommonPool) { /* Blocking call A */ };
jobs += async(CommonPool) { /* Blocking call B */ };
jobs.forEach { it.await(); };
KTCO
  • 2,115
  • 23
  • 21
  • 14
    Be aware that this basically does the same thing as a non-static AsyncTask, with the same potential issues. You can "fire" it but not "forget" it because it interacts with your Activity at the end. I would recommend that you start the coroutine in onStart() and cancel it in onStop(), to avoid performing work when the Activity is not visible and prevent updating views after the Activity is destroyed. Another solution would be to move the coroutine to a Loader or to a ViewModel (from the Architecture components). – BladeCoder Aug 18 '17 at 23:20
  • That is a very good point regarding potential life-cycle issues. I agree that all coroutines (jobs) should be added to a collection of some type, so proper clean-up can be done in onStop(). I also use this "fire-and-forget" approach in response to user actions (button clicks). Thank you for your comment and recommendations. – KTCO Aug 19 '17 at 00:18
  • Yes, it's not good for Android apps. Try https://proandroiddev.com/android-coroutine-recipes-33467a4302e9 – user25 Jan 01 '19 at 15:19
41

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 main UI 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

Dmytro Danylyk
  • 19,684
  • 11
  • 62
  • 68
9

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.

Steffen
  • 2,197
  • 2
  • 24
  • 38
6

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"
Shomu
  • 2,734
  • 24
  • 32
Suraj Nair
  • 1,729
  • 14
  • 14
  • Can I get the progress of the async task somehow? –  Feb 02 '18 at 12:59
  • 1
    This answer adds additional dependency - anko, which is currently in ver. `0.10.8`. I believe [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) are more than enough to achieve this, what OP asked. Especially in version `1.0.1`. – mmBs Nov 23 '18 at 15:16
  • Can we use Anko Async in ViewModel. Or it can only be used in activity or fragment. – Sumit Kumar Dec 28 '19 at 14:54
3

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

Enleur
  • 7
  • 1
  • 4
3

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
}
Rocky
  • 537
  • 4
  • 5
  • How can I cancel the task like on AsyncTask, with the ability to choose if it's cancelling the task nicely, or with thread-interruption: `https://developer.android.com/reference/android/os/AsyncTask#cancel(boolean)` , and how can I check inside that it was cancelled, to stop doing what it does? – android developer Jan 03 '21 at 02:19
2

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
Atiq
  • 14,435
  • 6
  • 54
  • 69
  • This has been deprecated, can you help with the working one – clementiano Jun 13 '19 at 22:06
  • See: https://github.com/Kotlin/kotlinx.coroutines implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9" – Kenneth Argo Oct 24 '20 at 18:08
1

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.

Andy Jazz
  • 49,178
  • 17
  • 136
  • 220
1

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<List<ResultsItem>> = MutableLiveData()

val message: MutableLiveData<String> = MutableLiveData()
val loaderProgressVisibility: MutableLiveData<Int> = MutableLiveData()

val coroutineJobs = mutableListOf<Job>()

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) {
            return@async nytAPI.getMoviesReviewWithCoroutine("full-time")
        }

        val moviesReviewsResponse = deferredResult.await()
        this@MoviesReviewViewModel.moviesReviewsResponse.value = 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
}
}
Muhammad Maqsood
  • 1,622
  • 19
  • 25