Uso típico de AsyncTask: quiero ejecutar una tarea en otro subproceso y, una vez finalizada esa tarea, quiero realizar alguna operación en mi subproceso de interfaz de usuario, es decir, ocultar una barra de progreso.
La tarea debe iniciarse en TextureView.SurfaceTextureListener.onSurfaceTextureAvailable y, una vez finalizada, quiero ocultar la barra de progreso. Hacer esto sincrónicamente no funciona porque bloquearía el hilo que construye la interfaz de usuario, dejando la pantalla en negro, sin siquiera mostrar la barra de progreso que quiero ocultar después.
Hasta ahora uso esto:
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 } }Pero estas clases son más que feas, así que me gustaría deshacerme de ellas. Me gustaría hacer esto con kotlin coroutines. He probado algunas variantes pero ninguna de ellas parece funcionar. El que más probablemente sospecharía que funciona es este:
runBlocking { // do async } progressBar.visibility = View.GONE Pero esto no funciona correctamente. Según tengo entendido, runBlocking no inicia un nuevo hilo, como lo haría AsyncTask , que es lo que necesito que haga. Pero al usar la rutina del thread , no veo una forma razonable de recibir una notificación cuando termine. Además, tampoco puedo poner progressBar.visibility = View.GONE en un nuevo subproceso, porque solo el subproceso de la interfaz de usuario puede realizar tales operaciones.
Soy nuevo en coroutines, así que no entiendo muy bien lo que me estoy perdiendo aquí.
Para usar una rutina necesitas un par de cosas:
Por lo general, uso una clase separada para eso, por ejemplo, "Presentador" o "Modelo de vista" :
class Presenter : CoroutineScope { private var job: Job = Job() override val coroutineContext: CoroutineContext get() = Dispatchers.Main + job // to run code in Main(UI) Thread // call this method to cancel a coroutine when you don't need it anymore, // eg when user closes the screen fun cancel() { job.cancel() } fun execute() = launch { onPreExecute() val result = doInBackground() // runs in background thread without blocking the Main Thread onPostExecute(result) } private suspend fun doInBackground(): String = withContext(Dispatchers.IO) { // to run code in Background Thread // do async work delay(1000) // simulate async work return@withContext "SomeResult" } // Runs on the Main(UI) Thread private fun onPreExecute() { // show progress } // Runs on the Main(UI) Thread private fun onPostExecute(result: String) { // hide progress } } Con ViewModel , el código es más conciso usando viewModelScope :
class MyViewModel : ViewModel() { fun execute() = viewModelScope.launch { onPreExecute() val result = doInBackground() // runs in background thread without blocking the Main Thread onPostExecute(result) } private suspend fun doInBackground(): String = withContext(Dispatchers.IO) { // to run code in Background Thread // do async work delay(1000) // simulate async work return@withContext "SomeResult" } // Runs on the Main(UI) Thread private fun onPreExecute() { // show progress } // Runs on the Main(UI) Thread private fun onPostExecute(result: String) { // hide progress } } Para usar viewModelScope agregue la siguiente línea a las dependencias del archivo build.gradle de la aplicación:
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$LIFECYCLE_VERSION" En el momento de escribir final LIFECYCLE_VERSION = "2.3.0-alpha04"
Otro enfoque es crear una función de extensión genérica en 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) } Ahora podemos usarlo con cualquier CoroutineScope :
En ViewModel :
class MyViewModel : ViewModel() { fun someFun() { viewModelScope.executeAsyncTask(onPreExecute = { // ... }, doInBackground = { // ... "Result" // send data to "onPostExecute" }, onPostExecute = { // ... here "it" is a data returned from "doInBackground" }) } } En Activity o Fragment :
lifecycleScope.executeAsyncTask(onPreExecute = { // ... }, doInBackground = { // ... "Result" // send data to "onPostExecute" }, onPostExecute = { // ... here "it" is a data returned from "doInBackground" }) Para usar viewModelScope o lifecycleScope agregue las siguientes líneas a las dependencias del archivo build.gradle de la aplicación:
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$LIFECYCLE_VERSION" // for viewModelScope implementation "androidx.lifecycle:lifecycle-runtime-ktx:$LIFECYCLE_VERSION" // for lifecycleScope Al momento de escribir final LIFECYCLE_VERSION = "2.3.0-alpha05" .
Puede hacer que ProgressBar se ejecute en el subproceso principal de la interfaz de usuario, mientras usa coroutine para ejecutar su tarea de forma asíncrona.
Dentro de su método de anulación divertido onCreate (),
GlobalScope.launch(Dispatchers.Main) { // Coroutine Dispatcher confined to Main UI Thread yourTask() // your task implementation }Puede inicializar,
private var jobStart: Job? = nullEn Kotlin, la declaración var significa que la propiedad es mutable. Si lo declara como val, es inmutable, de solo lectura y no se puede reasignar.
Fuera del método onCreate(), yourTask() se puede implementar como una función de suspensión, que no bloquea el hilo principal de la persona que llama.
Cuando la función se suspende mientras se espera que se devuelva el resultado, su subproceso en ejecución se desbloquea para que se ejecuten otras funciones.
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) } } }Para su barra de progreso, puede crear un botón para mostrar la barra de progreso cuando se hace clic en el botón.
buttonRecognize!!.setOnClickListener { trackProgress(false) }Fuera de 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 } }Un consejo adicional es verificar que su rutina se esté ejecutando en otro hilo, por ejemplo. DefaultDispatcher-trabajador-1,
Log.e("yourTask", "Running on thread ${Thread.currentThread().name}")
Espero que esto sea útil.