5

I spent some time to find a developer friendly solution (without adding dependencies to the project) of how to perform some hard task in background thread and after the task is completed return result to main thread. I found "AsyncTask" which allows to do that. But to use it you need to write boilerplate code for each task you need to run in Background. I am iOS developer who decided to try Android-related developing. So in Swift you can simply use the next code to make this task:

DispatchQueue.global().async(execute: { //Do some hard task in background DispatchQueue.main.async(execute: { //Return to main }) }) 

This looks pretty simple. But in Kotlin I didn't find such simple solution and decided to create it.

Here is what I made:

I created Generic class

import android.os.AsyncTask class BaseAsyncTask<M>: AsyncTask<()->M, Int, M>() { var completion: ((M)->Unit)? = null override fun doInBackground(vararg params: (() -> M)?): M? { for (p in params) { return p?.invoke() } return null } override fun onPostExecute(result: M) { super.onPostExecute(result) completion?.invoke(result) } } 

And Manager

class AsyncManager { companion object { fun <M>execute(inBackground: ()->M, inMain: (M)->Unit): BaseAsyncTask<M> { val task = BaseAsyncTask<M>() task.completion = inMain task.execute(inBackground) return task } fun <M>execute(inBackground: ()->M): BaseAsyncTask<M> { val task = BaseAsyncTask<M>() task.execute(inBackground) return task } } } 

Now I use it like this:

AsyncManager.execute({ //Do some hard task in background }, { //Return to main }) 

Looks developer friendly.

Log.e("MAIN", "MAIN THREAD SHOULD NOT BE BLOCKED") AsyncManager.execute({ Log.e("TASK", "Started background task") val retval = "The value from background" Thread.sleep(5000) Log.e("TASK", "Finished background task with result: " + retval) retval }, { Log.e("TASK", "Started task in Main thread with result from Background: " + it) }) Log.e("MAIN", "MAIN THREAD SHOULD NOT BE BLOCKED - 1") 

And the log:

2019-03-27 17:11:00.719 17082-17082/com.test.testapp E/MAIN: MAIN THREAD SHOULD NOT BE BLOCKED

2019-03-27 17:11:00.722 17082-17082/com.test.testapp E/MAIN: MAIN THREAD SHOULD NOT BE BLOCKED - 1

2019-03-27 17:11:00.722 17082-17124/com.test.testapp E/TASK: Started background task

2019-03-27 17:11:05.737 17082-17124/com.test.testapp E/TASK: Finished background task with result: The value from background

2019-03-27 17:11:05.738 17082-17082/com.test.testapp E/TASK: Started task in Main thread with result from Background: The value from background

So the question is what professional Android developers think about this solution. What problem can I get in case I'll use it. And maybe there is some reason not to use this solution.

2 Answers 2

10

If you're using Kotlin, the correct way to do this is via Coroutines, which would allow you to write code such as:

// Launch a coroutine that by default goes to the main thread GlobalScope.launch(Dispatchers.Main) { // Switch to a background (IO) thread val retval = withContext(Dispatchers.IO) { Log.e("TASK", "Started background task") val retval = "The value from background" Thread.sleep(5000) Log.e("TASK", "Finished background task with result: " + retval) retval } // Now you're back the main thread Log.e("TASK", "Started task in Main thread with result from Background: " + retval) } 

Note that Kotlin coroutines operate under structured concurrency, so you'd generally want to avoid using GlobalScope and instead scope your coroutine to be tied to your Activity / Fragment lifecycle. This generally needs to be done yourself right now.

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks for the answer. I was looking for the solution without adding dependencies to the project.
@Dmitry - I think you need to get over that mental model. Writing a much worse version of something that already exists 100x better and better tested is never a sustainable approach.
Thank you. I'll think about this.
As same as mentioned in answer, Kotlin coroutines can be defined by the scope which helps to manage when coroutines should run.. One of the most useful scope is ViewModelScope that is defined for each ViewModel in your app. for more info developer.android.com/topic/libraries/architecture/…
How do I do this in Compose
4

ianhanniballake's answer is correct, but is perhaps a bit incomplete, so I figured I'd provide a full generic example.

build.gradle(:app):

dependencies { // this line is probably already present implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3" } 

A global CoroutineScope is not bound to any job.
GlobalScope is used to launch top-level coroutines which are operating on the whole application lifetime and are not cancelled prematurely. Application code usually should use an application-defined CoroutineScope.
Using async or launch on the instance of GlobalScope is highly discouraged. taken from here

So you want to use any class with a lifecycle as CoroutineScope, so that when it dies, it takes the running background tasks with it to the grave. Often, people recommend to use an activity for this. However, there is a case to be made that you don't want any external class to use your activity as their CoroutineScope, so you can use a protected field instead:

protected val scope = CoroutineScope(Job() + Dispatchers.Main) 

At the time of writing, I do not know why we have to create a Job() here. What I do know is that the + operator is overloaded to merge these two context into one. For the Dispatcher part, you can choose a reasonable one. The options include

  • Dispatchers.Main for the UI thread
  • Dispatchers.Default for a pool of background threads
  • Dispatchers.IO for blocking operations that are I/O intensive
  • Dispatchers.Unconfined for when you really know what you are doing. This article discourages its use "normally".

Now with all this out of the way, the code becomes surprisingly simple:

import kotlin.coroutines.* // ... myButton.setOnClickListener() { v: View? -> myButton.setColorToYellow() // some UI thread work scope.launch(Dispatchers.Default) { val result = longComputation() // some background work withContext(Dispatchers.Main) { // some UI thread work for when the background work is done root.findViewById<TextView>(R.id.text_home).text = "Result: $result" } } myButton.setColorToRed() // more UI thread work. this is done instantly } 

Of course, this can be done anywhere - I'm just using a button and an onClickListener to give an example with a possible use case.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.