Cómo usar las corrutinas de Kotlin con componentes de arquitectura

Las corrutinas de Kotlin proporcionan una API que te permite escribir código asíncrono. Con las corrutinas de Kotlin, puedes definir un CoroutineScope, lo que te ayuda a administrar cuándo deben ejecutarse las corrutinas. Cada operación asíncrona se ejecuta dentro de un alcance particular.

Los componentes de arquitectura proporcionan compatibilidad de primer nivel con corrutinas para alcances lógicos en tu app, junto con una capa de interoperabilidad con LiveData. En este tema, se explica cómo usar corrutinas de manera eficaz con los componentes de la arquitectura.

Cómo agregar dependencias de KTX

Los alcances integrados de las corrutinas que se describen en este tema se encuentran en las extensiones de KTX de cada componente de arquitectura correspondiente. Asegúrate de agregar las dependencias apropiadas cuando uses estos alcances.

  • Para ViewModelScope, usa androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01 o una versión posterior.
  • Para LifecycleScope, usa androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01 o una versión posterior.
  • Para liveData, usa androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01 o una versión posterior.

Ámbitos de corrutinas optimizados para ciclos de vida

Los componentes de la arquitectura definen los siguientes alcances integrados que puedes usar en tu app.

ViewModelScope

El ViewModelScope se define por cada ViewModel de tu app. Si se borra ViewModel, se cancela automáticamente cualquier corrutina iniciada en este alcance. Las corrutinas son útiles para cuando tienes trabajos que se deben hacer solo si ViewModel está activo. Por ejemplo, si estás procesando datos para un diseño, debes definir el alcance del trabajo en el ViewModel, de modo que si se borra el ViewModel, se cancele automáticamente el trabajo a fin de no consumir recursos.

Puedes acceder al CoroutineScope de un ViewModel mediante la propiedad viewModelScope del ViewModel, como se muestra en el siguiente ejemplo:

class MyViewModel: ViewModel() {
        init {
            viewModelScope.launch {
                // Coroutine that will be canceled when the ViewModel is cleared.
            }
        }
    }
    

LifecycleScope

Se define un LifecycleScope para cada objeto Lifecycle. Se cancelan todas las corrutinas iniciadas en este alcance cuando se destruye el Lifecycle. Puedes acceder al CoroutineScope del Lifecycle mediante las propiedades lifecycle.coroutineScope o lifecycleOwner.lifecycleScope.

En el siguiente ejemplo, se muestra cómo usar lifecycleOwner.lifecycleScope para crear texto procesado previamente de forma asíncrona:

class MyFragment: Fragment() {
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            viewLifecycleOwner.lifecycleScope.launch {
                val params = TextViewCompat.getTextMetricsParams(textView)
                val precomputedText = withContext(Dispatchers.Default) {
                    PrecomputedTextCompat.create(longTextContent, params)
                }
                TextViewCompat.setPrecomputedText(textView, precomputedText)
            }
        }
    }
    

Cómo suspender corrutinas optimizadas para ciclos de vida

Aunque CoroutineScope proporciona una forma adecuada de cancelar automáticamente operaciones de larga duración, es posible que haya otros casos en los que quieras suspender la ejecución de un bloque de código, a menos que el Lifecycle esté en un estado determinado. Por ejemplo, para ejecutar un FragmentTransaction, debes esperar hasta que el Lifecycle esté al menos STARTED. En estos casos, Lifecycle proporciona métodos adicionales: lifecycle.whenCreated, lifecycle.whenStarted y lifecycle.whenResumed. Se suspenderá cualquier ejecución de corrutina dentro de estos bloques si el Lifecycle no está al menos en el estado mínimo deseado.

El siguiente ejemplo incluye un bloque de código que se ejecuta solo cuando el Lifecycle asociado está al menos en el estado STARTED:

class MyFragment: Fragment {
        init { // Notice that we can safely launch in the constructor of the Fragment.
            lifecycleScope.launch {
                whenStarted {
                    // The block inside will run only when Lifecycle is at least STARTED.
                    // It will start executing when fragment is started and
                    // can call other suspend methods.
                    loadingView.visibility = View.VISIBLE
                    val canAccess = withContext(Dispatchers.IO) {
                        checkUserAccess()
                    }

                    // When checkUserAccess returns, the next line is automatically
                    // suspended if the Lifecycle is not *at least* STARTED.
                    // We could safely run fragment transactions because we know the
                    // code won't run unless the lifecycle is at least STARTED.
                    loadingView.visibility = View.GONE
                    if (canAccess == false) {
                        findNavController().popBackStack()
                    } else {
                        showContent()
                    }
                }

                // This line runs only after the whenStarted block above has completed.

            }
        }
    }
    

Si el Lifecycle se destruye mientras una corrutina está activa mediante uno de los métodos when, se cancelará automáticamente la corrutina. En el siguiente ejemplo, el bloque finally se ejecuta una vez que el estado de DESTROYED es Lifecycle:

class MyFragment: Fragment {
        init {
            lifecycleScope.launchWhenStarted {
                try {
                    // Call some suspend functions.
                } finally {
                    // This line might execute after Lifecycle is DESTROYED.
                    if (lifecycle.state >= STARTED) {
                        // Here, since we've checked, it is safe to run any
                        // Fragment transactions.
                    }
                }
            }
        }
    }
    

Cómo usar corrutinas con LiveData

Cuando usas LiveData, es posible que debas calcular valores de forma asíncrona. Por ejemplo, es posible que quieras recuperar las preferencias de un usuario y entregarlas a tu IU. En estos casos, puedes usar la función de compilador de liveData para llamar a una función de suspend, que muestra el resultado como un objeto LiveData.

En el siguiente ejemplo, loadUser() es una función de suspensión declarada en otro lugar. Usa la función del compilador de liveData para llamar a loadUser() de forma asíncrona y, luego, usa emit() para emitir el resultado:

val user: LiveData<User> = liveData {
        val data = database.loadUser() // loadUser is a suspend function.
        emit(data)
    }
    

El bloque de compilación de liveData funciona como un tipo primitivo de simultaneidad estructurada entre las corrutinas y LiveData. El bloque de código comienza a ejecutarse cuando se activa LiveData y se cancela automáticamente después de un tiempo de espera configurable cuando LiveData se vuelve inactivo. Si se cancela antes de completarse, se reinicia si se vuelve a activar LiveData. Si se completó correctamente en una ejecución anterior, no se reiniciará. Ten en cuenta que solo se reinicia si se cancela automáticamente. No se reiniciará si se cancela el bloque por cualquier otro motivo (por ejemplo, si se muestra una CancelationException).

También puedes emitir varios valores desde el bloque. Cada llamada emit() suspende la ejecución del bloque hasta que se establece el valor LiveData en el subproceso principal.

val user: LiveData<Result> = liveData {
        emit(Result.loading())
        try {
            emit(Result.success(fetchUser()))
        } catch(ioException: Exception) {
            emit(Result.error(ioException))
        }
    }
    

También puedes combinar liveData con Transformations, como se muestra en el siguiente ejemplo:

class MyViewModel: ViewModel() {
        private val userId: LiveData<String> = MutableLiveData()
        val user = userId.switchMap { id ->
            liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
                emit(database.loadUserById(id))
            }
        }
    }
    

Puedes emitir varios valores desde un LiveData llamando a la función emitSource() siempre que quieras emitir un nuevo valor. Ten en cuenta que cada llamada a emit() o emitSource() quita la fuente que se había agregado previamente.

class UserDao: Dao {
        @Query("SELECT * FROM User WHERE id = :id")
        fun getUser(id: String): LiveData<User>
    }

    class MyRepository {
        fun getUser(id: String) = liveData<User> {
            val disposable = emitSource(
                userDao.getUser(id).map {
                    Result.loading(it)
                }
            )
            try {
                val user = webservice.fetchUser(id)
                // Stop the previous emission to avoid dispatching the updated user
                // as `loading`.
                disposable.dispose()
                // Update the database.
                userDao.insert(user)
                // Re-establish the emission with success type.
                emitSource(
                    userDao.getUser(id).map {
                        Result.success(it)
                    }
                )
            } catch(exception: IOException) {
                // Any call to `emit` disposes the previous one automatically so we don't
                // need to dispose it here as we didn't get an updated value.
                emitSource(
                    userDao.getUser(id).map {
                        Result.error(exception, it)
                    }
                )
            }
        }
    }
    

Para obtener más información relacionada con las corrutinas, consulta los siguientes vínculos: