Corrutinas de Kotlin en Android

Una corrutina es un patrón de diseño de simultaneidad que puedes usar en Android para simplificar el código que se ejecuta de forma asíncrona. Las corrutinas se agregaron a Kotlin en la versión 1.3 y se basan en conceptos establecidos de otros lenguajes.

En Android, las corrutinas ayudan a administrar tareas de larga duración que, de lo contrario, podrían bloquear el subproceso principal y hacer que tu app dejara de responder. Más del 50% de los desarrolladores profesionales que usan corrutinas informaron que vieron un aumento en la productividad. En este tema, se describe cómo puedes usar las corrutinas de Kotlin para solucionar estos problemas, lo que te permite escribir código de apps más limpio y conciso.

Funciones

Las corrutinas son nuestra solución recomendada para la programación asíncrona en Android. Las funciones más importantes son las siguientes:

  • Ligereza: Puedes ejecutar muchas corrutinas en un solo subproceso debido a la compatibilidad con la suspensión, que no bloquea el subproceso en el que se ejecuta la corrutina. La suspensión ahorra más memoria que el bloqueo y admite muchas operaciones simultáneas.
  • Menos fugas de memoria: Usa la simultaneidad estructurada para ejecutar operaciones dentro de un alcance.
  • Compatibilidad con cancelación integrada: La cancelación se propaga automáticamente a través de la jerarquía de corrutinas en ejecución.
  • Integración con Jetpack: Muchas bibliotecas de Jetpack incluyen extensiones que proporcionan compatibilidad total con corrutinas. Además, algunas bibliotecas proporcionan su propio alcance de corrutina, que puedes usar para la simultaneidad estructurada.

Resumen de ejemplos

Según la Guía de arquitectura de apps, en los ejemplos de este tema, se realiza una solicitud de red y se muestra el resultado al subproceso principal de modo que la app pueda mostrar el resultado al usuario.

Específicamente, el componente de arquitectura ViewModel llama a la capa del repositorio del subproceso principal para activar la solicitud de red. En esta guía, se analizan varias soluciones que usan corrutinas para mantener el subproceso principal desbloqueado.

ViewModel incluye un conjunto de extensiones KTX que funcionan directamente con corrutinas. Esas extensiones son una biblioteca lifecycle-viewmodel-ktx y se usan en esta guía.

Información de dependencia

Para usar corrutinas en tu proyecto de Android, agrega la siguiente dependencia al archivo build.gradle de tu app:

Groovy

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}

Kotlin

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}

Cómo ejecutar en un subproceso en segundo plano

Cuando haces una solicitud de red en el subproceso principal, este espera o se bloquea hasta que recibe una respuesta. Dado que el subproceso está bloqueado, el SO no puede llamar a onDraw(), lo que hace que tu app se bloquee y, potencialmente, genera un diálogo de Aplicación no responde (ANR). Para mejorar la experiencia del usuario, ejecutaremos esta operación en un subproceso en segundo plano.

Primero, veamos nuestra clase Repository y observemos cómo realiza la solicitud de red:

sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}

class LoginRepository(private val responseParser: LoginResponseParser) {
    private const val loginUrl = "https://example.com/login"

    // Function that makes the network request, blocking the current thread
    fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
        val url = URL(loginUrl)
        (url.openConnection() as? HttpURLConnection)?.run {
            requestMethod = "POST"
            setRequestProperty("Content-Type", "application/json; utf-8")
            setRequestProperty("Accept", "application/json")
            doOutput = true
            outputStream.write(jsonBody.toByteArray())
            return Result.Success(responseParser.parse(inputStream))
        }
        return Result.Error(Exception("Cannot open HttpURLConnection"))
    }
}

La clase makeLoginRequest es síncrona y bloquea el subproceso de llamada. Para modelar la respuesta de la solicitud de red, tenemos nuestra propia clase Result.

ViewModel activa la solicitud de red cuando el usuario hace clic, por ejemplo, en un botón:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"
        loginRepository.makeLoginRequest(jsonBody)
    }
}

Con el código anterior, LoginViewModel bloquea el subproceso de IU cuando se realiza la solicitud de red. La solución más simple para quitar la ejecución del subproceso principal es crear una nueva corrutina y ejecutar la solicitud de red en un subproceso de E/S:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        // Create a new coroutine to move the execution off the UI thread
        viewModelScope.launch(Dispatchers.IO) {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            loginRepository.makeLoginRequest(jsonBody)
        }
    }
}

Analicemos el código de corrutinas en la función login:

  • viewModelScope es un CoroutineScope predefinido que se incluye con las extensiones KTX de ViewModel. Ten en cuenta que todas las corrutinas deben ejecutarse en un alcance. CoroutineScope administra una o más corrutinas relacionadas.
  • launch es una función que crea una corrutina y despacha la ejecución de sus funciones al despachador correspondiente.
  • Dispatchers.IO indica que esta corrutina debe ejecutarse en un subproceso reservado para operaciones de E/S.

Se ejecuta la función login de la siguiente manera:

  • La app llama a la función login desde la capa View del subproceso principal.
  • launch crea una nueva corrutina y se realiza la solicitud de red de forma independiente en un subproceso reservado para las operaciones de E/S.
  • Mientras se ejecuta la corrutina, la función login continúa su ejecución y se muestra antes de que finalice la solicitud de red. Ten en cuenta que, para simplificar el proceso, se ignora por ahora la respuesta de la red.

Dado que esta corrutina se inicia con viewModelScope, se ejecuta en el alcance de ViewModel. Si se destruye el ViewModel porque el usuario se aleja de la pantalla, se cancela automáticamente viewModelScope, y todas las corrutinas en ejecución también se cancelan.

En el ejemplo anterior, el problema radica en que cualquier llamada a makeLoginRequest debe recordar quitar la ejecución de manera explícita del subproceso principal. Veamos cómo podemos modificar el Repository para resolver este problema.

Cómo usar corrutinas para la seguridad del subproceso principal

Consideramos que una función es segura para el subproceso principal cuando no bloquea las actualizaciones de la IU en este subproceso. La función makeLoginRequest no es segura, ya que, cuando se llama a makeLoginRequest desde el subproceso principal, se bloquea la IU. Usa la función withContext() de la biblioteca de corrutinas para trasladar la ejecución de una corrutina a un subproceso diferente:

class LoginRepository(...) {
    ...
    suspend fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {

        // Move the execution of the coroutine to the I/O dispatcher
        return withContext(Dispatchers.IO) {
            // Blocking network request code
        }
    }
}

withContext(Dispatchers.IO) traslada la ejecución de la corrutina a un subproceso de E/S, lo que hace que nuestra función de llamada sea segura y habilite la IU según sea necesario.

La clase makeLoginRequest también está marcada con la palabra clave suspend, que es la forma en que Kotlin aplica una función desde una corrutina.

En el siguiente ejemplo, la corrutina se crea en el LoginViewModel. A medida que makeLoginRequest quita la ejecución del subproceso principal, se puede ejecutar la corrutina de la función login en el subproceso principal:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {

        // Create a new coroutine on the UI thread
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"

            // Make the network call and suspend execution until it finishes
            val result = loginRepository.makeLoginRequest(jsonBody)

            // Display result of the network request to the user
            when (result) {
                is Result.Success<LoginResponse> -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

Ten en cuenta que la corrutina todavía es necesaria, ya que makeLoginRequest es una función suspend y todas las funciones suspend deben ejecutarse en una corrutina.

Este código tiene las siguientes diferencias con respecto al ejemplo de login anterior:

  • launch no toma un parámetro Dispatchers.IO. Cuando no pasas un Dispatcher a launch, cualquier corrutina iniciada desde viewModelScope se ejecuta en el subproceso principal.
  • Ahora, el resultado de la solicitud de red se utiliza para mostrar la IU de éxito o falla.

La función de acceso ahora se ejecuta de la siguiente manera:

  • La app llama a la función login() desde la capa View del subproceso principal.
  • launch crea una corrutina nueva en el subproceso principal y esta comienza a ejecutarse.
  • Dentro de la corrutina, la llamada a loginRepository.makeLoginRequest() ahora suspende la ejecución de la corrutina hasta que el bloque withContext de makeLoginRequest() termina de ejecutarse.
  • Una vez que finaliza el bloque withContext, la corrutina de login() reanuda la ejecución en el subproceso principal con el resultado de la solicitud de red.

Cómo controlar excepciones

Para controlar las excepciones que puede generar la capa Repository, usa la compatibilidad integrada con las excepciones de Kotlin. En el siguiente ejemplo, usamos un bloque try-catch:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            val result = try {
                loginRepository.makeLoginRequest(jsonBody)
            } catch(e: Exception) {
                Result.Error(Exception("Network request failed"))
            }
            when (result) {
                is Result.Success<LoginResponse> -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

En este ejemplo, se maneja como un error en la IU cualquier excepción inesperada que arroje la llamada makeLoginRequest().

Recursos adicionales de corrutinas

Para obtener información más detallada sobre las corrutinas en Android, consulta Cómo mejorar el rendimiento de las apps con las corrutinas de Kotlin.

Para obtener más recursos de corrutinas, consulta los siguientes vínculos: