Typical use for AsyncTask: I want to run a task in another thread and after that task is done, I want to perform some operation in my UI thread, namely hiding a progress bar.
The task is to be started in TextureView.SurfaceTextureListener.onSurfaceTextureAvailable
and after it finished I want to hide the progress bar. Doing this synchronously does not work because it would block the thread building the UI, leaving the screen black, not even showing the progress bar I want to hide afterwards.
So far I use this:
inner class MyTask : AsyncTask<ProgressBar, Void, ProgressBar>() {
override fun doInBackground(vararg params: ProgressBar?) : ProgressBar {
// do async
return params[0]!!
}
override fun onPostExecute(result: ProgressBar?) {
super.onPostExecute(result)
result?.visibility = View.GONE
}
}
But these classes are beyond ugly so I'd like to get rid of them. I'd like to do this with kotlin coroutines. I've tried some variants but none of them seem to work. The one I would most likely suspect to work is this:
runBlocking {
// do async
}
progressBar.visibility = View.GONE
But this does not work properly. As I understand it, the runBlocking
does not start a new thread, as AsyncTask
would, which is what I need it to do. But using the thread
coroutine, I don't see a reasonable way to get notified when it finished. Also, I can't put progressBar.visibility = View.GONE
in a new thread either, because only the UI thread is allowed to make such operations.
I'm new to coroutines so I don't quite understand what I'm missing here.
Another approach is to create generic extension function on CoroutineScope
:
fun <R> CoroutineScope.executeAsyncTask(
onPreExecute: () -> Unit,
doInBackground: () -> R,
onPostExecute: (R) -> Unit
) = launch {
onPreExecute()
val result = withContext(Dispatchers.IO) { // runs in background thread without blocking the Main Thread
doInBackground()
}
onPostExecute(result)
}
Now we can use it with any CoroutineScope
:
In ViewModel
:
class MyViewModel : ViewModel() {
fun someFun() {
viewModelScope.executeAsyncTask(onPreExecute = {
// ...
}, doInBackground = {
// ...
"Result" // send data to "onPostExecute"
}, onPostExecute = {
// ... here "it" is a data returned from "doInBackground"
})
}
}
In Activity
or Fragment
:
lifecycleScope.executeAsyncTask(onPreExecute = {
// ...
}, doInBackground = {
// ...
"Result" // send data to "onPostExecute"
}, onPostExecute = {
// ... here "it" is a data returned from "doInBackground"
})
To use viewModelScope
or lifecycleScope
add next line(s) to dependencies of the app's build.gradle file:
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$LIFECYCLE_VERSION" // for viewModelScope
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$LIFECYCLE_VERSION" // for lifecycleScope
At the time of writing final LIFECYCLE_VERSION = "2.3.0-alpha05"
.
You can get ProgressBar to run on the UI Main Thread, while using coroutine to run your task asynchronously.
Inside your override fun onCreate() method,
GlobalScope.launch(Dispatchers.Main) { // Coroutine Dispatcher confined to Main UI Thread
yourTask() // your task implementation
}
You can initialize,
private var jobStart: Job? = null
In Kotlin, var declaration means the property is mutable. If you declare it as val, it is immutable, read-only & cannot be reassigned.
Outside the onCreate() method, yourTask() can be implemented as a suspending function, which does not block main caller thread.
When the function is suspended while waiting for the result to be returned, its running thread is unblocked for other functions to execute.
private suspend fun yourTask() = withContext(Dispatchers.Default){ // with a given coroutine context
jobStart = launch {
try{
// your task implementation
} catch (e: Exception) {
throw RuntimeException("To catch any exception thrown for yourTask", e)
}
}
}
For your progress bar, you can create a button to show the progress bar when the button is clicked.
buttonRecognize!!.setOnClickListener {
trackProgress(false)
}
Outside of onCreate(),
private fun trackProgress(isCompleted:Boolean) {
buttonRecognize?.isEnabled = isCompleted // ?. safe call
buttonRecognize!!.isEnabled // !! non-null asserted call
if(isCompleted) {
loading_progress_bar.visibility = View.GONE
} else {
loading_progress_bar.visibility = View.VISIBLE
}
}
An additional tip is to check that your coroutine is indeed running on another thread, eg. DefaultDispatcher-worker-1,
Log.e("yourTask", "Running on thread ${Thread.currentThread().name}")
Hope this is helpful.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With