Corrutinas de Kotlin te permiten escribir código asíncrono limpio y simplificado que mantiene tu app sea responsiva mientras administras tareas de larga duración, como las llamadas de red o las operaciones de disco.
En este tema, se explora en detalle cómo funcionan las corrutinas en Android. Si no conoces sobre corrutinas, te recomendamos que leas Corrutinas de Kotlin en Android antes de empezar con este tema.
Cómo administrar tareas prolongadas
Las corrutinas se basan en funciones regulares y, para ello, agregan dos operaciones que se ocupan de las tareas de larga duración. Además de invoke
(o call
) y return
, las corrutinas agregan suspend
y resume
:
suspend
pausa la ejecución de la corrutina actual y guarda todas las variables locales.resume
continúa la ejecución de una corrutina suspendida desde donde se detuvo.
Solo puedes llamar a funciones suspend
desde otras funciones suspend
o si utilizas un compilador de corrutinas como launch
para comenzar una corrutina nueva.
En el siguiente ejemplo, se muestra una implementación de corrutina simple para realizar una tarea prolongada hipotética:
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("https://developer.android.com") // Dispatchers.IO for `get`
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }
En ese ejemplo, todavía se ejecuta get()
en el subproceso principal, pero suspende la corrutina antes de iniciar la solicitud de red. Cuando se completa la solicitud de red, get
reanuda la corrutina suspendida en lugar de usar una devolución de llamada para notificar al subproceso principal.
Kotlin utiliza un marco de pila para administrar qué función se ejecuta junto con cualquier variable local. Cuando se suspende una corrutina, se copia y se guarda el marco de pila actual para más tarde. Cuando se reanuda, se copia el marco de pila desde el lugar en el que se guardó, y vuelve a ejecutarse la función. Aunque el código podría parecer una solicitud de bloqueo secuencial ordinaria, la corrutina garantiza que la solicitud de red evite bloquear el subproceso principal.
Cómo usar corrutinas para la seguridad del subproceso principal
Las corrutinas de Kotlin utilizan despachadores para determinar qué subprocesos se utilizan para la ejecución de corrutinas. Si deseas ejecutar código fuera del subproceso principal, puedes indicarles a las corrutinas de Kotlin que realicen el trabajo en el despachador predeterminado o de IO. En Kotlin, todas las corrutinas se deben ejecutar en un despachador, incluso cuando se ejecutan en el subproceso principal. La corrutinas se pueden suspender a sí mismas, y el despachador es responsable de reanudarlas.
Para especificar en qué lugar deberían ejecutarse las corrutinas, Kotlin proporciona tres despachadores que puedes utilizar:
- Dispatchers.Main: Utiliza este despachador para ejecutar una corrutina en el subproceso de Android principal. Solo debes usar este despachador para interactuar con la IU y realizar trabajos rápidos. Por ejemplo, para llamar a funciones
suspend
, ejecutar operaciones del framework de la IU de Android y actualizar objetosLiveData
. - Dispatchers.IO: Este despachador está optimizado para realizar E/S de disco o red fuera del subproceso principal. Algunos ejemplos incluyen usar el componente Room, leer desde archivos o escribir en ellos, y ejecutar operaciones de red.
- Dispatchers.Default: Este despachador está optimizado para realizar trabajo que usa la CPU de manera intensiva fuera del subproceso principal. Algunos casos prácticos de ejemplo son clasificar una lista y analizar JSON.
Continuando con el ejemplo anterior, puedes utilizar despachadores para volver a definir la función get
. Dentro del cuerpo de get
, llama a withContext(Dispatchers.IO)
para crear un bloque que se ejecute en el grupo de subprocesos de IO. Cualquier código que coloques dentro de ese bloque se ejecutará siempre a través del despachador de IO
. Debido a que withContext
es una función suspendida, get
también es una función suspendida.
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("developer.android.com") // Dispatchers.Main
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = // Dispatchers.Main
withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block)
/* perform network IO here */ // Dispatchers.IO (main-safety block)
} // Dispatchers.Main
}
Mediante las corrutinas, puedes despachar subprocesos con control detallado. Debido a que withContext()
te permite controlar el grupo de subprocesos de cualquier línea de código sin introducir devoluciones de llamada, puedes aplicarlo a funciones muy pequeñas, como leer desde una base de datos o realizar una solicitud de red. Una práctica recomendada consiste en usar withContext()
a fin de garantizar que todas las funciones sean seguras para el subproceso principal, lo cual significa que puedes llamar a la función desde el subproceso principal. De esta manera, el emisor nunca tiene que pensar en qué subproceso se debe utilizar para ejecutar la función.
En el ejemplo anterior, aunque fetchDocs()
se ejecuta en el subproceso principal, puede llamar de manera segura a get
, que realiza una solicitud de red en segundo plano.
Debido a que las corrutinas admiten suspend
y resume
, la corrutina en el subproceso principal se reanuda con el resultado de get
tan pronto como se termina el bloqueo de withContext
.
Rendimiento de withContext()
withContext()
no aumenta la sobrecarga en comparación con un modelo basado en devoluciones de llamada equivalente
para implementarlos. Además, en algunos casos, es posible optimizar las llamadas de withContext()
más allá de una implementación basada en devoluciones de llamada equivalente. Por ejemplo, si una función hace diez llamadas a una red, puedes usar un withContext()
externo para indicarle a Kotlin que intercambie subprocesos solo una vez. Luego, aunque la biblioteca de red usa withContext()
varias veces, permanece en el mismo despachador y evita intercambiar subprocesos. Además, Kotlin optimiza el intercambio entre Dispatchers.Default
y Dispatchers.IO
a fin de evitar el intercambio de subprocesos siempre que sea posible.
Cómo iniciar una corrutina
Puedes iniciar corrutinas de dos maneras:
launch
inicia una corrutina nueva y no le muestra el resultado al llamador. Cualquier trabajo que se considere "activar y olvidar" se puede iniciar conlaunch
.async
inicia una corrutina nueva y te permite mostrar un resultado con una suspensión llamadaawait
.
Por lo general, como una función regular no puede llamar a await
, debes usar launch
para lanzar una corrutina nueva desde una función regular. Usa el objeto async
solo cuando esté dentro de otra corrutina o cuando esté dentro de una función suspendida y realice una descomposición paralela.
Descomposición paralela
Todas las corrutinas que se inician dentro de una función de suspend
se deben detener cuando se muestra esa función. Por lo tanto, es probable que debas garantizar que esas corrutinas se completen antes de mostrarlas. Con la simultaneidad estructurada en Kotlin, puedes definir un objeto coroutineScope
que inicie una o más corrutinas. Luego, puedes usar await()
(para una sola corrutina) o awaitAll()
(para varias corrutinas) a fin de garantizar que estas corrutinas se completen antes de mostrarlas desde la función.
Como ejemplo, definamos un objeto coroutineScope
que obtiene dos documentos de manera asíncrona. Con las llamadas a await()
en cada referencia diferida, garantizamos que las dos operaciones de async
se completen antes de mostrar un valor:
suspend fun fetchTwoDocs() =
coroutineScope {
val deferredOne = async { fetchDoc(1) }
val deferredTwo = async { fetchDoc(2) }
deferredOne.await()
deferredTwo.await()
}
También puedes usar awaitAll()
en las colecciones, como se muestra en el siguiente ejemplo:
suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main)
coroutineScope {
val deferreds = listOf( // fetch two docs at the same time
async { fetchDoc(1) }, // async returns a result for the first doc
async { fetchDoc(2) } // async returns a result for the second doc
)
deferreds.awaitAll() // use awaitAll to wait for both network requests
}
Aunque fetchTwoDocs()
lanza corrutinas nuevas con async
, la función usa awaitAll()
para esperar a que esas corrutinas lanzadas se completen antes de mostrarlas. Sin embargo, ten en cuenta que, incluso si no llamaste a awaitAll()
, el constructor de coroutineScope
no reanuda la corrutina que llamó a fetchTwoDocs
hasta que se completan todas las corrutinas nuevas.
Además, coroutineScope
captura todas las excepciones que muestran las corrutinas y las enruta al emisor.
Para obtener más información sobre la descomposición paralela, consulta Cómo componer funciones suspendidas.
Conceptos de corrutinas
CoroutineScope
Un objeto CoroutineScope
realiza un seguimiento de cualquier corrutina que crea con launch
o async
. Para cancelar el trabajo en curso (es decir, las corrutinas en ejecución), se puede llamar a scope.cancel()
en cualquier momento. En Android, algunas bibliotecas de KTX proporcionan su propio objeto CoroutineScope
para ciertas clases de ciclo de vida. Por ejemplo, ViewModel
tiene viewModelScope
, y Lifecycle
tiene lifecycleScope
.
Sin embargo, a diferencia de un despachador, un objeto CoroutineScope
no ejecuta las corrutinas.
También se usa viewModelScope
en los ejemplos de Cómo administrar subprocesos en segundo plano en Android con corrutinas.
Sin embargo, si necesitas crear tu propio objeto CoroutineScope
para controlar el ciclo de vida de las corrutinas ubicadas en una capa específica de tu app, puedes hacerlo de la siguiente manera:
class ExampleClass {
// Job and Dispatcher are combined into a CoroutineContext which
// will be discussed shortly
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine within the scope
scope.launch {
// New coroutine that can call suspend functions
fetchDocs()
}
}
fun cleanUp() {
// Cancel the scope to cancel ongoing coroutines work
scope.cancel()
}
}
Un permiso cancelado no puede crear más corrutinas. Por lo tanto, solo debes llamar a scope.cancel()
cuando se destruye la clase que controla su ciclo de vida. Si usas viewModelScope
, la clase ViewModel
cancelará automáticamente el permiso en el método onCleared()
de ViewModel.
Job
Un objeto Job
es un controlador para una corrutina. Cada corrutina que creas con los objetos launch
o async
muestra una instancia de Job
que identifica de forma única la corrutina y administra su ciclo de vida. También puedes pasar un elemento Job
a CoroutineScope
para administrar más aspectos de su ciclo de vida, como se muestra en el siguiente ejemplo:
class ExampleClass {
...
fun exampleMethod() {
// Handle to the coroutine, you can control its lifecycle
val job = scope.launch {
// New coroutine
}
if (...) {
// Cancel the coroutine started above, this doesn't affect the scope
// this coroutine was launched in
job.cancel()
}
}
}
CoroutineContext
El objeto CoroutineContext
define el comportamiento de una corrutina mediante el siguiente conjunto de elementos:
Job
: Controla el ciclo de vida de la corrutina.CoroutineDispatcher
: Despacha trabajo al subproceso correspondiente.CoroutineName
: Corresponde al nombre de la corrutina, que resulta útil para la depuración.CoroutineExceptionHandler
: Controla excepciones no detectadas.
Para las corrutinas nuevas que se crearon dentro de un permiso, se asigna una nueva instancia Job
a la corrutina nueva, y se heredan los otros elementos CoroutineContext
del permiso en el que se encuentran. Puedes anular los elementos heredados si pasas un objeto CoroutineContext
nuevo a la función launch
o async
. Ten en cuenta que pasar un objeto Job
a un elemento launch
o async
no tendrá ningún efecto, ya que siempre se asigna una nueva instancia de Job
a una corrutina nueva.
class ExampleClass {
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine on Dispatchers.Main as it's the scope's default
val job1 = scope.launch {
// New coroutine with CoroutineName = "coroutine" (default)
}
// Starts a new coroutine on Dispatchers.Default
val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) {
// New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
}
}
}
Recursos adicionales de corrutinas
Para obtener más recursos de corrutinas, consulta los siguientes vínculos: