Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin Coroutines the right way in Android

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()}
        }
    }
like image 709
Sai Avatar asked Mar 31 '17 03:03

Sai


People also ask

What are Kotlin coroutines good for?

On Android, coroutines help to manage long-running tasks that might otherwise block the main thread and cause your app to become unresponsive. Over 50% of professional developers who use coroutines have reported seeing increased productivity.

How are coroutines implemented in Kotlin?

Standard library: All the support for coroutines is written in Kotlin's core libraries. A coroutine internally uses a Continuation class to capture the contexts for its execution. Then the dynamic aspect is modeled as a Job class. The use of async usually creates a Deferred job, which is a subclass of the Job class.

Why coroutines are better than threads?

Coroutines start execution in one thread, suspend the execution and resume on other thread. Whereas a thread can be kept blocked for a certain period of time and cannot be used until its work gets completed. As coroutine is user-friendly and light weight, scheduling is controlled by the user completely.

Is coroutines run on main thread?

We can for example schedule coroutines on a Java Executor or on Android main looper. However, we can't schedule coroutines on just any thread, it has to cooperate.


Video Answer


3 Answers

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(); };
like image 179
KTCO Avatar answered Oct 17 '22 04:10

KTCO


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

like image 30
Dmytro Danylyk Avatar answered Oct 17 '22 03:10

Dmytro Danylyk


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.

like image 25
Steffen Avatar answered Oct 17 '22 03:10

Steffen