Corrutinas de Kotlin en Android

Organiza tus páginas con colecciones Guarda y categoriza el contenido según tus preferencias.

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. Corrutinas se agregaron a Kotlin en la versión 1.3 y se basan en conceptos 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 simultaneidad estructurada para ejecutar operaciones dentro de un permiso.
  • Compatibilidad integrada con la cancelación : 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:

El contenido y las muestras de código que aparecen en esta página están sujetas a las licencias que se describen en la Licencia de Contenido . Java y OpenJDK son marcas registradas de Oracle o sus afiliados.

Última actualización: 2024-08-29 (UTC)