En este codelab, aprenderás a usar corrutinas de Kotlin en una app para Android, una nueva forma de administrar subprocesos en segundo plano que puede simplificar el código al reducir la necesidad de devoluciones de llamada. Las corrutinas son una función de Kotlin que convierte las devoluciones de llamada asíncronas para tareas de larga duración, como el acceso a bases de datos o redes, en código secuencial.
A continuación, te presentamos un fragmento de código que te dará una idea de lo que harás.
// Async callbacks
networkRequest { result ->
// Successful network request
databaseSave(result) { rows ->
// Result saved
}
}
El código basado en devoluciones de llamada se convertirá en código secuencial con corrutinas.
// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved
Comenzarás con una app existente, compilada con componentes de la arquitectura, que usa un estilo de devolución de llamada para tareas de larga duración.
Al final de este codelab, tendrás suficiente experiencia para usar corrutinas en tu app para cargar datos de la red y podrás integrar corrutinas en una app. También conocerás las prácticas recomendadas para las corrutinas y cómo escribir una prueba para el código que usa corrutinas.
Requisitos previos
- Conocimiento de los componentes de la arquitectura
ViewModel
,LiveData
,Repository
yRoom
- Experiencia con la sintaxis de Kotlin, incluidas las funciones de extensión y lambdas
- Conocimientos básicos sobre el uso de subprocesos en Android, como el subproceso principal, los subprocesos en segundo plano y las devoluciones de llamada
Actividades
- Llama al código escrito con corrutinas y obtén resultados.
- Usa funciones de suspensión para hacer que el código asíncrono sea secuencial.
- Usa
launch
yrunBlocking
para controlar cómo se ejecuta el código. - Aprende técnicas para convertir las APIs existentes en corrutinas con
suspendCoroutine
. - Usar corrutinas con componentes de arquitectura
- Conoce las prácticas recomendadas para probar corrutinas.
Requisitos
- Android Studio 3.5 (es posible que el codelab funcione con otras versiones, pero algo podría faltar o verse diferente).
Si a medida que avanzas con este codelab encuentras algún problema (errores de código, errores gramaticales, texto poco claro, etc.), infórmalo mediante el vínculo Informa un error que se encuentra en la esquina inferior izquierda del codelab.
Descarga el código
Haz clic en el siguiente vínculo para descargar todo el código de este codelab:
… o clona el repositorio de GitHub desde la línea de comandos con el siguiente comando:
$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git
Preguntas frecuentes
Primero, veamos el aspecto de la app de ejemplo inicial. Sigue estas instrucciones para abrir la app de muestra en Android Studio.
- Si descargaste el archivo ZIP
kotlin-coroutines
, descomprímelo. - Abre el proyecto
coroutines-codelab
en Android Studio. - Selecciona el módulo de aplicación
start
. - Haz clic en el botón
Ejecutar y elige un emulador o conecta tu dispositivo Android, que debe poder ejecutar Android Lollipop (el SDK mínimo compatible es el 21). Debería aparecer la pantalla de corrutinas de Kotlin:
Esta app de inicio usa subprocesos para aumentar el recuento con una pequeña demora después de que presionas la pantalla. También recuperará un título nuevo de la red y lo mostrará en la pantalla. Pruébalo ahora y deberías ver el cambio en el recuento y el mensaje después de una breve demora. En este codelab, convertirás esta aplicación para que use corrutinas.
Esta app usa componentes de arquitectura para separar el código de la IU en MainActivity
de la lógica de la aplicación en MainViewModel
. Tómate un momento para familiarizarte con la estructura del proyecto.
MainActivity
muestra la IU, registra objetos de escucha de clics y puede mostrar unSnackbar
. Pasa eventos aMainViewModel
y actualiza la pantalla segúnLiveData
enMainViewModel
.MainViewModel
controla los eventos enonMainViewClicked
y se comunicará conMainActivity
a través deLiveData.
Executors
defineBACKGROUND,
, que puede ejecutar elementos en un subproceso en segundo plano.TitleRepository
recupera los resultados de la red y los guarda en la base de datos.
Cómo agregar corrutinas a un proyecto
Para usar corrutinas en Kotlin, debes incluir la biblioteca coroutines-core
en el archivo build.gradle (Module: app)
de tu proyecto. Los proyectos del codelab ya lo hicieron por ti, por lo que no necesitas hacerlo para completar el codelab.
Las corrutinas en Android están disponibles como una biblioteca principal y extensiones específicas de Android:
- kotlinx-coroutines-core : Es la interfaz principal para usar corrutinas en Kotlin.
- kotlinx-coroutines-android : Compatibilidad con el subproceso principal de Android en corrutinas
La app de inicio ya incluye las dependencias en build.gradle.
. Cuando crees un proyecto de app nuevo, deberás abrir build.gradle (Module: app)
y agregar las dependencias de corrutinas al proyecto.
dependencies { ... implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x" }
En Android, es fundamental evitar el bloqueo del subproceso principal. El subproceso principal es un subproceso único que controla todas las actualizaciones de la IU. También es el subproceso que llama a todos los controladores de clics y a otras devoluciones de llamada de la IU. Por lo tanto, debe ejecutarse sin problemas para garantizar una excelente experiencia del usuario.
Para que tu app se muestre al usuario sin pausas visibles, el subproceso principal debe actualizar la pantalla cada 16 ms o con más frecuencia, lo que equivale a unos 60 fotogramas por segundo. Muchas tareas comunes tardan más que eso, como analizar grandes conjuntos de datos JSON, escribir datos en una base de datos o recuperar datos de la red. Por lo tanto, llamar a código como este desde el subproceso principal puede hacer que la app se detenga, salte o incluso se bloquee. Si bloqueas el subproceso principal durante demasiado tiempo, la app incluso puede fallar y mostrar un diálogo de Aplicación no responde.
Mira el siguiente video para obtener una introducción sobre cómo las corrutinas resuelven este problema en Android con la introducción de la seguridad del subproceso principal.
El patrón de devolución de llamada
Un patrón para realizar tareas de larga duración sin bloquear el subproceso principal son las devoluciones de llamada. Si usas devoluciones de llamada, podrás iniciar tareas de larga duración en un subproceso que se ejecute en segundo plano. Cuando se completa la tarea, se llama a la devolución de llamada para informarte el resultado en el subproceso principal.
Consulta un ejemplo del patrón de devolución de llamada.
// Slow request with callbacks
@UiThread
fun makeNetworkRequest() {
// The slow network request runs on another thread
slowFetch { result ->
// When the result is ready, this callback will get the result
show(result)
}
// makeNetworkRequest() exits after calling slowFetch without waiting for the result
}
Debido a que este código está anotado con @UiThread
, debe ejecutarse lo suficientemente rápido como para hacerlo en el subproceso principal. Esto significa que debe devolver el control muy rápidamente para que no se retrase la próxima actualización de la pantalla. Sin embargo, dado que slowFetch
tardará segundos o incluso minutos en completarse, el subproceso principal no puede esperar el resultado. La devolución de llamada show(result)
permite que slowFetch
se ejecute en un subproceso en segundo plano y muestre el resultado cuando esté listo.
Cómo usar corrutinas para quitar devoluciones de llamada
Las devoluciones de llamada son un patrón muy útil, pero tienen algunos inconvenientes. El código que utiliza devoluciones de llamadas en gran medida puede ser difícil de leer y razonar. Además, las devoluciones de llamada no permiten el uso de algunas funciones del lenguaje, como excepciones.
Las corrutinas de Kotlin te permiten convertir en código secuencial el código basado en devoluciones de llamada. Por lo general, el código escrito de forma secuencial es más fácil de leer y hasta puede usar funciones de lenguaje como excepciones.
En definitiva, hacen exactamente lo mismo: esperan hasta que un resultado esté disponible desde una tarea de larga duración y continúan su ejecución. Sin embargo, en el código se ven muy diferentes.
La palabra clave suspend
es la forma en que Kotlin marca una función, o un tipo de función, disponible para las corrutinas. Cuando una corrutina llama a una función marcada como suspend
, en lugar de bloquearse hasta que se muestre esa función como una llamada a función normal, suspende la ejecución hasta que el resultado esté listo y, luego, reanuda donde la dejó con el resultado. Mientras se suspende a la espera de un resultado, desbloquea el subproceso en el que se ejecuta para que se puedan ejecutar otras funciones o corrutinas.
Por ejemplo, en el siguiente código, makeNetworkRequest()
y slowFetch()
son funciones suspend
.
// Slow request with coroutines
@UiThread
suspend fun makeNetworkRequest() {
// slowFetch is another suspend function so instead of
// blocking the main thread makeNetworkRequest will `suspend` until the result is
// ready
val result = slowFetch()
// continue to execute after the result is ready
show(result)
}
// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
Al igual que con la versión de devolución de llamada, makeNetworkRequest
debe regresar del subproceso principal de inmediato porque está marcado como @UiThread
. Esto significa que, por lo general, no puede llamar a métodos de bloqueo como slowFetch
. Aquí es donde la palabra clave suspend
hace su magia.
En comparación con el código basado en devoluciones de llamada, el código de corrutinas logra el mismo resultado de desbloquear el subproceso actual con menos código. Debido a su estilo secuencial, es fácil encadenar varias tareas de larga duración sin crear múltiples devoluciones de llamada. Por ejemplo, el código que recupera un resultado de dos extremos de red y lo guarda en la base de datos se puede escribir como una función en corrutinas sin devoluciones de llamada. Sería algo como lo siguiente:
// Request data from network and save it to database with coroutines
// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
// slowFetch and anotherFetch are suspend functions
val slow = slowFetch()
val another = anotherFetch()
// save is a regular function and will block this thread
database.save(slow, another)
}
// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }
En la siguiente sección, agregarás corrutinas a la app de ejemplo.
En este ejercicio, escribirás una corrutina para mostrar un mensaje después de una demora. Para comenzar, asegúrate de tener el módulo start
abierto en Android Studio.
Información sobre CoroutineScope
En Kotlin, todas las corrutinas se ejecutan dentro de un CoroutineScope
. Un alcance controla las corrutinas desde el principio con su trabajo. Cuando cancelas el trabajo de un alcance, se cancelan todas las corrutinas que se iniciaron en ese alcance. En Android, puedes usar un alcance para cancelar todas las corrutinas en ejecución cuando, por ejemplo, el usuario navega fuera de un Activity
o Fragment
. Los ámbitos también te permiten especificar un dispatcher predeterminado. Un despachador controla qué subproceso ejecuta una corrutina.
En el caso de las corrutinas iniciadas por la IU, suele ser correcto iniciarlas en Dispatchers.Main
, que es el subproceso principal en Android. Una corrutina iniciada en Dispatchers.Main
no bloqueará el subproceso principal mientras esté suspendida. Dado que una corrutina ViewModel
casi siempre actualiza la IU en el subproceso principal, iniciar corrutinas en el subproceso principal te ahorra cambios de subprocesos adicionales. Una corrutina iniciada en el subproceso principal puede cambiar de despachador en cualquier momento después de su inicio. Por ejemplo, puede usar otro dispatcher para analizar un resultado JSON grande fuera del subproceso principal.
Cómo usar viewModelScope
La biblioteca lifecycle-viewmodel-ktx
de AndroidX agrega un CoroutineScope a los ViewModels que está configurado para iniciar corrutinas relacionadas con la IU. Para usar esta biblioteca, debes incluirla en el archivo build.gradle (Module: start)
de tu proyecto. Ese paso ya se completó en los proyectos del codelab.
dependencies { ... implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x" }
La biblioteca agrega un viewModelScope
como una función de extensión de la clase ViewModel
. Este alcance está vinculado a Dispatchers.Main
y se cancelará automáticamente cuando se borre el ViewModel
.
Cómo cambiar de subprocesos a corrutinas
En MainViewModel.kt
, busca el siguiente TODO junto con este código:
MainViewModel.kt
/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
// TODO: Convert updateTaps to use coroutines
tapCount++
BACKGROUND.submit {
Thread.sleep(1_000)
_taps.postValue("$tapCount taps")
}
}
Este código usa BACKGROUND ExecutorService
(definido en util/Executor.kt
) para ejecutarse en un subproceso en segundo plano. Dado que sleep
bloquea el subproceso actual, congelaría la IU si se llamara en el subproceso principal. Un segundo después de que el usuario hace clic en la vista principal, se solicita una barra de notificaciones.
Puedes ver cómo sucede esto quitando el BACKGROUND del código y volviéndolo a ejecutar. El ícono giratorio de carga no se mostrará y todo "saltará" al estado final un segundo después.
MainViewModel.kt
/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
// TODO: Convert updateTaps to use coroutines
tapCount++
Thread.sleep(1_000)
_taps.postValue("$tapCount taps")
}
Reemplaza updateTaps
por este código basado en corrutinas que hace lo mismo. Deberás importar launch
y delay
.
MainViewModel.kt
/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
// launch a coroutine in viewModelScope
viewModelScope.launch {
tapCount++
// suspend this coroutine for one second
delay(1_000)
// resume in the main dispatcher
// _snackbar.value can be called directly from main thread
_taps.postValue("$tapCount taps")
}
}
Este código hace lo mismo, espera un segundo antes de mostrar una barra de mensajes. Sin embargo, existen algunas diferencias importantes:
viewModelScope.
launch
iniciará una corrutina en elviewModelScope
. Esto significa que, cuando se cancele el trabajo que pasamos aviewModelScope
, se cancelarán todas las corrutinas de este trabajo o alcance. Si el usuario abandonó la actividad antes de que regresaradelay
, esta corrutina se cancelará automáticamente cuando se llame aonCleared
al destruirse el ViewModel.- Dado que
viewModelScope
tiene un despachador predeterminado deDispatchers.Main
, esta corrutina se iniciará en el subproceso principal. Más adelante, veremos cómo usar diferentes subprocesos. - La función
delay
es una funciónsuspend
. Esto se muestra en Android Studio con el íconoen el margen izquierdo. Aunque esta corrutina se ejecuta en el subproceso principal,
delay
no bloqueará el subproceso durante un segundo. En cambio, el dispatcher programará la corrutina para que se reanude en un segundo en la siguiente instrucción.
Ejecútalo. Cuando hagas clic en la vista principal, deberías ver una barra de notificaciones un segundo después.
En la siguiente sección, veremos cómo probar esta función.
En este ejercicio, escribirás una prueba para el código que acabas de escribir. En este ejercicio, se muestra cómo probar corrutinas que se ejecutan en Dispatchers.Main
con la biblioteca kotlinx-coroutines-test. Más adelante en este codelab, implementarás una prueba que interactúa con corrutinas directamente.
Revisa el código existente
Abre MainViewModelTest.kt
en la carpeta androidTest
.
MainViewModelTest.kt
class MainViewModelTest {
@get:Rule
val coroutineScope = MainCoroutineScopeRule()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var subject: MainViewModel
@Before
fun setup() {
subject = MainViewModel(
TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("initial")
))
}
}
Una regla es una forma de ejecutar código antes y después de la ejecución de una prueba en JUnit. Se usan dos reglas para permitirnos probar MainViewModel en una prueba fuera del dispositivo:
InstantTaskExecutorRule
es una regla de JUnit que configuraLiveData
para ejecutar cada tarea de forma síncrona.MainCoroutineScopeRule
es una regla personalizada en esta base de código que configuraDispatchers.Main
para usar unTestCoroutineDispatcher
dekotlinx-coroutines-test
. Esto permite que las pruebas avancen un reloj virtual para las pruebas y que el código useDispatchers.Main
en las pruebas de unidades.
En el método setup
, se crea una instancia nueva de MainViewModel
con objetos simulados de prueba, que son implementaciones simuladas de la red y la base de datos que se proporcionan en el código de inicio para ayudar a escribir pruebas sin usar la red o la base de datos reales.
Para esta prueba, los objetos simulados solo son necesarios para satisfacer las dependencias de MainViewModel
. Más adelante en este codelab, actualizarás los objetos simulados para que admitan corrutinas.
Escribe una prueba que controle corrutinas
Agrega una prueba nueva que garantice que los toques se actualicen un segundo después de hacer clic en la vista principal:
MainViewModelTest.kt
@Test
fun whenMainClicked_updatesTaps() {
subject.onMainViewClicked()
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
coroutineScope.advanceTimeBy(1000)
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}
Cuando llamemos a onMainViewClicked
, se iniciará la corrutina que acabamos de crear. Esta prueba verifica que el texto de los toques permanezca en "0 toques" inmediatamente después de que se llame a onMainViewClicked
y, luego, 1 segundo después, se actualice a "1 toque".
Esta prueba usa virtual-time para controlar la ejecución de la corrutina que inicia onMainViewClicked
. El MainCoroutineScopeRule
te permite pausar, reanudar o controlar la ejecución de las corrutinas que se inician en el Dispatchers.Main
. Aquí llamamos a advanceTimeBy(1_000)
, lo que hará que el despachador principal ejecute de inmediato las corrutinas programadas para reanudarse 1 segundo después.
Esta prueba es completamente determinística, lo que significa que siempre se ejecutará de la misma manera. Además, como tiene control total sobre la ejecución de las corrutinas iniciadas en Dispatchers.Main
, no tiene que esperar un segundo para que se establezca el valor.
Ejecuta la prueba existente
- Haz clic con el botón derecho en el nombre de la clase
MainViewModelTest
en tu editor para abrir un menú contextual. - En el menú contextual, elige
Run 'MainViewModelTest'.
- Para ejecuciones futuras, puedes seleccionar esta configuración de prueba en las configuraciones junto al botón
de la barra de herramientas. De forma predeterminada, la configuración se llamará MainViewModelTest.
Deberías ver que la prueba se aprobó. Además, debería tardar mucho menos de un segundo en ejecutarse.
En el siguiente ejercicio, aprenderás a convertir APIs de devolución de llamada existentes para usar corrutinas.
En este paso, comenzarás a convertir un repositorio para que use corrutinas. Para ello, agregaremos corrutinas a ViewModel
, Repository
, Room
y Retrofit
.
Es una buena idea comprender de qué se encarga cada parte de la arquitectura antes de cambiarlas para que usen corrutinas.
MainDatabase
implementa una base de datos con Room que guarda y carga unTitle
.MainNetwork
implementa una API de red que recupera un título nuevo. Usa Retrofit para recuperar títulos.Retrofit
está configurado para devolver errores o datos simulados de forma aleatoria, pero, de lo contrario, se comporta como si realizara solicitudes de red reales.TitleRepository
implementa una sola API para recuperar o actualizar el título combinando datos de la red y la base de datos.MainViewModel
representa el estado de la pantalla y controla los eventos. Le indicará al repositorio que actualice el título cuando el usuario presione la pantalla.
Dado que la solicitud de red se basa en eventos de la IU y queremos iniciar una corrutina en función de ellos, el lugar natural para comenzar a usar corrutinas es en el ViewModel
.
La versión de devolución de llamada
Abre MainViewModel.kt
para ver la declaración de refreshTitle
.
MainViewModel.kt
/**
* Update title text via this LiveData
*/
val title = repository.title
// ... other code ...
/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
// TODO: Convert refreshTitle to use coroutines
_spinner.value = true
repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
override fun onCompleted() {
_spinner.postValue(false)
}
override fun onError(cause: Throwable) {
_snackBar.postValue(cause.message)
_spinner.postValue(false)
}
})
}
Se llama a esta función cada vez que el usuario hace clic en la pantalla, y hará que el repositorio actualice el título y escriba el nuevo título en la base de datos.
Esta implementación usa una devolución de llamada para hacer lo siguiente:
- Antes de iniciar una búsqueda, se muestra un ícono giratorio de carga con
_spinner.value = true
- Cuando obtiene un resultado, borra el spinner de carga con
_spinner.value = false
. - Si se produce un error, le indica a una barra de notificaciones que se muestre y borra el spinner.
Ten en cuenta que la devolución de llamada onCompleted
no recibe el objeto title
. Como escribimos todos los títulos en la base de datos de Room
, la IU se actualiza al título actual observando un LiveData
que actualiza Room
.
En la actualización de corrutinas, mantendremos el mismo comportamiento. Es un buen patrón usar una fuente de datos observable, como una base de datos de Room
, para mantener automáticamente actualizada la IU.
La versión de corrutinas
Volvamos a escribir refreshTitle
con corrutinas.
Como la necesitaremos de inmediato, creemos una función de suspensión vacía en nuestro repositorio (TitleRespository.kt
). Define una función nueva que use el operador suspend
para indicarle a Kotlin que funciona con corrutinas.
TitleRepository.kt
suspend fun refreshTitle() {
// TODO: Refresh from network and write to database
delay(500)
}
Cuando termines este codelab, actualizarás esto para usar Retrofit y Room para recuperar un título nuevo y escribirlo en la base de datos con corrutinas. Por ahora, solo pasará 500 milisegundos simulando que trabaja y, luego, continuará.
En MainViewModel
, reemplaza la versión de devolución de llamada de refreshTitle
por una que inicie una corrutina nueva:
MainViewModel.kt
/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
Analicemos esta función paso a paso:
viewModelScope.launch {
Al igual que la corrutina para actualizar el recuento de toques, comienza por iniciar una corrutina nueva en viewModelScope
. Se usará Dispatchers.Main
, lo cual es correcto. Aunque refreshTitle
realizará una solicitud de red y una consulta de base de datos, puede usar corrutinas para exponer una interfaz segura para el subproceso principal. Esto significa que será seguro llamarlo desde el subproceso principal.
Como usamos viewModelScope
, cuando el usuario abandone esta pantalla, el trabajo que inició esta corrutina se cancelará automáticamente. Esto significa que no realizará solicitudes de red ni consultas de bases de datos adicionales.
Las siguientes líneas de código llaman a refreshTitle
en repository
.
try {
_spinner.value = true
repository.refreshTitle()
}
Antes de que esta corrutina haga algo, inicia el spinner de carga y, luego, llama a refreshTitle
como una función normal. Sin embargo, dado que refreshTitle
es una función de suspensión, se ejecuta de manera diferente a una función normal.
No es necesario que pasemos una devolución de llamada. La corrutina se suspenderá hasta que refreshTitle
la reanude. Si bien parece una llamada a una función de bloqueo normal, esperará automáticamente hasta que se completen la red y la consulta de la base de datos antes de reanudarse sin bloquear el subproceso principal.
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
Las excepciones en las funciones de suspensión funcionan igual que los errores en las funciones normales. Si arrojas un error en una función de suspensión, se arrojará a la persona que llama. Por lo tanto, aunque se ejecutan de manera muy diferente, puedes usar bloques try/catch normales para controlarlas. Esto es útil porque te permite confiar en la compatibilidad integrada con el lenguaje para el control de errores en lugar de crear un control de errores personalizado para cada devolución de llamada.
Además, si arrojas una excepción fuera de una corrutina, esta cancelará su elemento superior de forma predeterminada. Esto significa que es fácil cancelar varias tareas relacionadas juntas.
Luego, en un bloque finally, podemos asegurarnos de que el spinner siempre se desactive después de que se ejecute la consulta.
Vuelve a ejecutar la aplicación seleccionando la configuración de inicio y, luego, presionando. Deberías ver un indicador de carga cuando presiones en cualquier lugar. El título seguirá siendo el mismo porque aún no conectamos nuestra red o base de datos.
En el siguiente ejercicio, actualizarás el repositorio para que realmente funcione.
En este ejercicio, aprenderás a cambiar el subproceso en el que se ejecuta una corrutina para implementar una versión funcional de TitleRepository
.
Revisa el código de devolución de llamada existente en refreshTitle
Abre TitleRepository.kt
y revisa la implementación existente basada en devoluciones de llamada.
TitleRepository.kt
// TitleRepository.kt
fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
// This request will be run on a background thread by retrofit
BACKGROUND.submit {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle().execute()
if (result.isSuccessful) {
// Save it to database
titleDao.insertTitle(Title(result.body()!!))
// Inform the caller the refresh is completed
titleRefreshCallback.onCompleted()
} else {
// If it's not successful, inform the callback of the error
titleRefreshCallback.onError(
TitleRefreshError("Unable to refresh title", null))
}
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
titleRefreshCallback.onError(
TitleRefreshError("Unable to refresh title", cause))
}
}
}
En TitleRepository.kt
, el método refreshTitleWithCallbacks
se implementa con una devolución de llamada para comunicar el estado de carga y error al llamador.
Esta función realiza varias acciones para implementar la actualización.
- Cambiar a otro hilo con
BACKGROUND
ExecutorService
- Ejecuta la solicitud de red
fetchNextTitle
con el método de bloqueoexecute()
. Esto ejecutará la solicitud de red en el subproceso actual, en este caso, uno de los subprocesos enBACKGROUND
. - Si el resultado es exitoso, guárdalo en la base de datos con
insertTitle
y llama al métodoonCompleted()
. - Si el resultado no fue exitoso o hay una excepción, llama al método onError para informarle al llamador sobre la actualización fallida.
Esta implementación basada en devoluciones de llamada es segura para el subproceso principal porque no lo bloqueará. Sin embargo, debe usar una devolución de llamada para informar al llamador cuando se complete el trabajo. También llama a las devoluciones de llamada en el subproceso BACKGROUND
al que cambió.
Cómo bloquear llamadas desde corrutinas
Sin introducir corrutinas en la red o la base de datos, podemos hacer que este código sea seguro para el hilo principal con corrutinas. Esto nos permitirá deshacernos de la devolución de llamada y pasar el resultado al subproceso que la llamó inicialmente.
Puedes usar este patrón siempre que necesites realizar un trabajo de bloqueo o que requiera mucha CPU desde una corrutina, como ordenar y filtrar una lista grande o leer desde el disco.
Para alternar entre cualquier despachador, las corrutinas utilizan withContext
. Llamar a withContext
cambia al otro despachador solo para la lambda y, luego, regresa al despachador que lo llamó con el resultado de ella.
De forma predeterminada, las corrutinas de Kotlin proporcionan tres Dispatchers: Main
, IO
y Default
. El despachador IO está optimizado para trabajo de E/S, como leer desde la red o el disco, mientras que el despachador predeterminado está optimizado para tareas con uso intensivo de CPU.
TitleRepository.kt
suspend fun refreshTitle() {
// interact with *blocking* network and IO calls from a coroutine
withContext(Dispatchers.IO) {
val result = try {
// Make network request using a blocking call
network.fetchNextTitle().execute()
} catch (cause: Throwable) {
// If the network throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
if (result.isSuccessful) {
// Save it to database
titleDao.insertTitle(Title(result.body()!!))
} else {
// If it's not successful, inform the callback of the error
throw TitleRefreshError("Unable to refresh title", null)
}
}
}
Esta implementación usa llamadas de bloqueo para la red y la base de datos, pero sigue siendo un poco más simple que la versión de devolución de llamada.
Este código aún usa llamadas de bloqueo. Llamar a execute()
y insertTitle(...)
bloqueará el subproceso en el que se ejecuta esta corrutina. Sin embargo, al cambiar a Dispatchers.IO
con withContext
, bloqueamos uno de los subprocesos en el despachador de E/S. La corrutina que llamó a este método, que posiblemente se ejecuta en Dispatchers.Main
, se suspenderá hasta que se complete la expresión lambda withContext
.
En comparación con la versión de devolución de llamada, hay dos diferencias importantes:
withContext
devuelve su resultado al despachador que lo llamó, en este caso,Dispatchers.Main
. La versión de devolución de llamada llamó a las devoluciones de llamada en un subproceso del servicio de ejecuciónBACKGROUND
.- El llamador no tiene que pasar una devolución de llamada a esta función. Pueden confiar en la suspensión y la reanudación para obtener el resultado o el error.
Vuelve a ejecutar la app
Si vuelves a ejecutar la app, verás que la nueva implementación basada en corrutinas carga resultados de la red.
En el siguiente paso, integrarás corrutinas en Room y Retrofit.
Para continuar con la integración de corrutinas, usaremos la compatibilidad con las funciones de suspensión en la versión estable de Room y Retrofit, y, luego, simplificaremos considerablemente el código que acabamos de escribir con las funciones de suspensión.
Corrutinas en Room
Primero, abre MainDatabase.kt
y haz que insertTitle
sea una función de suspensión:
MainDatabase.kt
// add the suspend modifier to the existing insertTitle
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)
Cuando lo hagas, Room hará que tu consulta sea segura para el subproceso principal y la ejecutará automáticamente en un subproceso en segundo plano. Sin embargo, también significa que solo puedes llamar a esta consulta desde una corrutina.
Y eso es todo lo que tienes que hacer para usar corrutinas en Room. Es muy útil.
Corrutinas en Retrofit
A continuación, veamos cómo integrar corrutinas con Retrofit. Abre MainNetwork.kt
y cambia fetchNextTitle
a una función de suspensión.
MainNetwork.kt
// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String
interface MainNetwork {
@GET("next_title.json")
suspend fun fetchNextTitle(): String
}
Para usar funciones de suspensión con Retrofit, debes hacer dos cosas:
- Agrega un modificador suspend a la función
- Quita el wrapper
Call
del tipo de datos que se muestra. Aquí devolvemosString
, pero también podrías devolver un tipo complejo respaldado por JSON. Si aún quieres proporcionar acceso alResult
completo de Retrofit, puedes devolverResult<String>
en lugar deString
desde la función suspendida.
Retrofit hará que las funciones de suspensión sean seguras para el subproceso principal automáticamente, de modo que puedas llamarlas directamente desde Dispatchers.Main
.
Cómo usar Room y Retrofit
Ahora que Room y Retrofit admiten funciones de suspensión, podemos usarlas desde nuestro repositorio. Abre TitleRepository.kt
y observa cómo el uso de funciones de suspensión simplifica en gran medida la lógica, incluso en comparación con la versión de bloqueo:
Repository.kt del título
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle()
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
Vaya, es mucho más corto. ¿Qué pasó? Resulta que depender de la suspensión y la reanudación permite que el código sea mucho más corto. Retrofit nos permite usar tipos de datos que se muestran, como String
o un objeto User
aquí, en lugar de un Call
. Esto es seguro, ya que, dentro de la función de suspensión, Retrofit
puede ejecutar la solicitud de red en un subproceso en segundo plano y reanudar la corrutina cuando se complete la llamada.
Aún mejor, nos deshicimos del withContext
. Dado que Room y Retrofit proporcionan funciones de suspensión seguras para el subproceso principal, es seguro organizar este trabajo asíncrono desde Dispatchers.Main
.
Cómo corregir errores del compilador
Pasar a las corrutinas implica cambiar la firma de las funciones, ya que no puedes llamar a una función de suspensión desde una función normal. Cuando agregaste el modificador suspend
en este paso, se generaron algunos errores del compilador que muestran lo que sucedería si cambiaras una función para que se suspenda en un proyecto real.
Revisa el proyecto y corrige los errores del compilador cambiando la función a suspend creada. A continuación, se incluyen las soluciones rápidas para cada caso:
TestingFakes.kt
Actualiza los objetos simulados de prueba para admitir los nuevos modificadores suspend.
TitleDaoFake
- Presiona Alt + Intro para agregar modificadores de suspensión a todas las funciones de la jerarquía.
MainNetworkFake
- Presiona Alt + Intro para agregar modificadores de suspensión a todas las funciones de la jerarquía.
- Reemplaza
fetchNextTitle
por esta función
override suspend fun fetchNextTitle() = result
MainNetworkCompletableFake
- Presiona Alt + Intro para agregar modificadores de suspensión a todas las funciones de la jerarquía.
- Reemplaza
fetchNextTitle
por esta función
override suspend fun fetchNextTitle() = completable.await()
TitleRepository.kt
- Borra la función
refreshTitleWithCallbacks
, ya que ya no se usa.
Ejecuta la app
Vuelve a ejecutar la app. Una vez que se compile, verás que carga datos con corrutinas desde el ViewModel hasta Room y Retrofit.
¡Felicitaciones! Cambiaste por completo esta app para que use corrutinas. Para terminar, hablaremos un poco sobre cómo probar lo que acabamos de hacer.
En este ejercicio, escribirás una prueba que llame a una función suspend
directamente.
Como refreshTitle
se expone como una API pública, se probará directamente, lo que mostrará cómo llamar a las funciones de corrutinas desde las pruebas.
Esta es la función refreshTitle
que implementaste en el último ejercicio:
TitleRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle()
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
Escribe una prueba que llame a una función de suspensión
Abre TitleRepositoryTest.kt
en la carpeta test
, que tiene dos TODO.
Intenta llamar a refreshTitle
desde la primera prueba whenRefreshTitleSuccess_insertsRows
.
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
subject.refreshTitle()
}
Dado que refreshTitle
es una función suspend
, Kotlin no sabe cómo llamarla, excepto desde una corrutina o desde otra función de suspensión, y obtendrás un error del compilador como "Suspend function refreshTitle should be called only from a coroutine or another suspend function".
El ejecutor de pruebas no sabe nada sobre las corrutinas, por lo que no podemos convertir esta prueba en una función de suspensión. Podríamos launch
una corrutina con un CoroutineScope
como en un ViewModel
, pero las pruebas deben ejecutar las corrutinas hasta su finalización antes de devolverlas. Una vez que una función de prueba devuelve un valor, la prueba finaliza. Las corrutinas que se inician con launch
son código asíncrono, que puede completarse en algún momento en el futuro. Por lo tanto, para probar ese código asíncrono, necesitas alguna forma de indicarle a la prueba que espere hasta que se complete tu corrutina. Dado que launch
es una llamada que no bloquea, significa que regresa de inmediato y puede seguir ejecutando una corrutina después de que regresa la función. No se puede usar en pruebas. Por ejemplo:
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
// launch starts a coroutine then immediately returns
GlobalScope.launch {
// since this is asynchronous code, this may be called *after* the test completes
subject.refreshTitle()
}
// test function returns immediately, and
// doesn't see the results of refreshTitle
}
Esta prueba a veces fallará. La llamada a launch
se devolverá de inmediato y se ejecutará al mismo tiempo que el resto del caso de prueba. La prueba no tiene forma de saber si refreshTitle
ya se ejecutó o no, y cualquier aserción, como verificar que se actualizó la base de datos, sería inestable. Además, si refreshTitle
generó una excepción, no se generará en la pila de llamadas de prueba. En cambio, se arrojará al controlador de excepciones no detectadas de GlobalScope
.
La biblioteca kotlinx-coroutines-test
tiene la función runBlockingTest
que se bloquea mientras llama a funciones de suspensión. Cuando runBlockingTest
llama a una función suspendida o launches
a una corrutina nueva, la ejecuta al instante de forma predeterminada. Podríamos decir que es una manera de convertir corrutinas y funciones suspendidas en llamadas a funciones normales.
Además, runBlockingTest
volverá a arrojar excepciones no detectadas por ti. Esto facilita la prueba cuando una corrutina arroja una excepción.
Implementa una prueba con una corrutina
Encapsula la llamada a refreshTitle
con runBlockingTest
y quita el wrapper GlobalScope.launch
de subject.refreshTitle().
TitleRepositoryTest.kt
@Test
fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest {
val titleDao = TitleDaoFake("title")
val subject = TitleRepository(
MainNetworkFake("OK"),
titleDao
)
subject.refreshTitle()
Truth.assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK")
}
Esta prueba usa los objetos simulados proporcionados para verificar que refreshTitle
inserte "OK" en la base de datos.
Cuando la prueba llame a runBlockingTest
, se bloqueará hasta que se complete la corrutina iniciada por runBlockingTest
. Luego, dentro, cuando llamamos a refreshTitle
, se usa el mecanismo normal de suspensión y reanudación para esperar a que se agregue la fila de la base de datos a nuestro objeto falso.
Una vez que se completa la corrutina de prueba, se devuelve runBlockingTest
.
Cómo escribir una prueba de tiempo de espera
Queremos agregar un tiempo de espera corto a la solicitud de red. Primero, escribamos la prueba y, luego, implementemos el tiempo de espera. Crea una prueba nueva:
TitleRepositoryTest.kt
@Test(expected = TitleRefreshError::class)
fun whenRefreshTitleTimeout_throws() = runBlockingTest {
val network = MainNetworkCompletableFake()
val subject = TitleRepository(
network,
TitleDaoFake("title")
)
launch {
subject.refreshTitle()
}
advanceTimeBy(5_000)
}
Esta prueba usa el objeto MainNetworkCompletableFake
falso proporcionado, que es un objeto falso de red diseñado para suspender a los llamadores hasta que la prueba los reanude. Cuando refreshTitle
intente realizar una solicitud de red, se quedará en espera para siempre porque queremos probar los tiempos de espera.
Luego, inicia una corrutina independiente para llamar a refreshTitle
. Esta es una parte clave de las pruebas de tiempo de espera. El tiempo de espera debe ocurrir en una corrutina diferente de la que crea runBlockingTest
. De esta manera, podemos llamar a la siguiente línea, advanceTimeBy(5_000)
, que adelantará el tiempo en 5 segundos y hará que la otra corrutina agote el tiempo de espera.
Esta es una prueba de tiempo de espera completa, y se aprobará una vez que implementemos el tiempo de espera.
Ejecútalo ahora y observa qué sucede:
Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]
Una de las funciones de runBlockingTest
es que no te permitirá filtrar corrutinas después de que se complete la prueba. Si hay corrutinas sin terminar, como nuestra corrutina de lanzamiento, al final de la prueba, esta fallará.
Cómo agregar un tiempo de espera
Abre TitleRepository
y agrega un tiempo de espera de cinco segundos a la recuperación de la red. Puedes hacerlo con la función withTimeout
:
TitleRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = withTimeout(5_000) {
network.fetchNextTitle()
}
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
Ejecuta la prueba. Cuando ejecutes las pruebas, verás que todas se aprueban.
En el siguiente ejercicio, aprenderás a escribir funciones de orden superior con corrutinas.
En este ejercicio, refactorizarás refreshTitle
en MainViewModel
para usar una función general de carga de datos. Esto te enseñará a compilar funciones de orden superior que usan corrutinas.
La implementación actual de refreshTitle
funciona, pero podemos crear una corrutina de carga de datos general que siempre muestre el ícono giratorio. Esto puede ser útil en una base de código que carga datos en respuesta a varios eventos y desea asegurarse de que el ícono giratorio de carga se muestre de forma coherente.
Revisar la implementación actual: cada línea, excepto repository.refreshTitle()
, es código estándar para mostrar el spinner y los errores.
// MainViewModel.kt
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
// this is the only part that changes between sources
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
Cómo usar corrutinas en funciones de orden superior
Agrega este código a MainViewModel.kt
MainViewModel.kt
private fun launchDataLoad(block: suspend () -> Unit): Job {
return viewModelScope.launch {
try {
_spinner.value = true
block()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
Ahora refactoriza refreshTitle()
para usar esta función de orden superior.
MainViewModel.kt
fun refreshTitle() {
launchDataLoad {
repository.refreshTitle()
}
}
Al abstraer la lógica en torno a la visualización de un ícono giratorio de carga y la visualización de errores, simplificamos el código real necesario para cargar datos. Mostrar un spinner o un error es algo que se puede generalizar fácilmente para cualquier carga de datos, mientras que la fuente y el destino de los datos reales deben especificarse cada vez.
Para compilar esta abstracción, launchDataLoad
toma un argumento block
que es una lambda de suspensión. Una lambda de suspensión te permite llamar a funciones de suspensión. Así es como Kotlin implementa los compiladores de corrutinas launch
y runBlocking
que hemos estado usando en este codelab.
// suspend lambda
block: suspend () -> Unit
Para crear una expresión lambda de suspensión, comienza con la palabra clave suspend
. La flecha de la función y el tipo de datos que se muestra Unit
completan la declaración.
No es necesario que declares tus propias lambdas de suspensión, pero pueden ser útiles para crear abstracciones como esta que encapsulan la lógica repetida.
En este ejercicio, aprenderás a usar código basado en corrutinas de WorkManager.
¿Qué es WorkManager?
En Android, hay muchas opciones para realizar trabajos diferibles en segundo plano. En este ejercicio, se muestra cómo integrar WorkManager con corrutinas. WorkManager es una biblioteca compatible, flexible y simple para realizar trabajos diferibles en segundo plano. WorkManager es la solución recomendada para estos casos de uso en Android.
WorkManager es parte de Android Jetpack y un componente de arquitectura para trabajos en segundo plano que requieren una combinación de ejecución oportunista y garantizada. La ejecución oportunista implica que WorkManager realizará el trabajo en segundo plano tan pronto como sea posible. La ejecución garantizada implica que WorkManager se encargará de la lógica a los efectos de iniciar tu trabajo en diferentes situaciones, incluso si sales de la app.
Por este motivo, WorkManager es una buena opción para las tareas que deben completarse en algún momento.
Algunos ejemplos de tareas que muestran un buen uso de WorkManager:
- Subir registros
- Aplicar filtros a imágenes y guardar la imagen
- Sincronizar datos locales con la red de forma periódica
Cómo usar corrutinas con WorkManager
WorkManager proporciona diferentes implementaciones de su clase ListanableWorker
base para diferentes casos de uso.
La clase Worker más simple nos permite que WorkManager ejecute alguna operación síncrona. Sin embargo, después de haber trabajado hasta ahora para convertir nuestra base de código para que use corrutinas y funciones de suspensión, la mejor manera de usar WorkManager es a través de la clase CoroutineWorker
que permite definir nuestra función doWork()
como una función de suspensión.
Para comenzar, abre RefreshMainDataWork
. Ya extiende CoroutineWorker
, y debes implementar doWork
.
Dentro de la función suspend
doWork
, llama a refreshTitle()
desde el repositorio y devuelve el resultado adecuado.
Después de completar la tarea pendiente, el código se verá de la siguiente manera:
override suspend fun doWork(): Result {
val database = getDatabase(applicationContext)
val repository = TitleRepository(network, database.titleDao)
return try {
repository.refreshTitle()
Result.success()
} catch (error: TitleRefreshError) {
Result.failure()
}
}
Ten en cuenta que CoroutineWorker.doWork()
es una función de suspensión. A diferencia de la clase Worker
más simple, este código NO se ejecuta en el Executor especificado en tu configuración de WorkManager, sino que usa el dispatcher en el miembro coroutineContext
(de forma predeterminada, Dispatchers.Default
).
Cómo probar nuestro CoroutineWorker
Ninguna base de código debería estar completa sin pruebas.
WorkManager ofrece varias formas diferentes de probar tus clases Worker
. Para obtener más información sobre la infraestructura de pruebas original, puedes leer la documentación.
WorkManager v2.1 introduce un nuevo conjunto de APIs para admitir una forma más sencilla de probar las clases de ListenableWorker
y, como consecuencia, CoroutineWorker. En nuestro código, usaremos una de estas APIs nuevas: TestListenableWorkerBuilder
.
Para agregar nuestra prueba nueva, actualiza el archivo RefreshMainDataWorkTest
en la carpeta androidTest
.
El contenido del archivo es el siguiente:
package com.example.android.kotlincoroutines.main
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.example.android.kotlincoroutines.fakes.MainNetworkFake
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {
@Test
fun testRefreshMainDataWork() {
val fakeNetwork = MainNetworkFake("OK")
val context = ApplicationProvider.getApplicationContext<Context>()
val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context)
.setWorkerFactory(RefreshMainDataWork.Factory(fakeNetwork))
.build()
// Start the work synchronously
val result = worker.startWork().get()
assertThat(result).isEqualTo(Result.success())
}
}
Antes de comenzar la prueba, le indicamos a WorkManager
sobre la fábrica para que podamos insertar la red falsa.
La prueba en sí usa TestListenableWorkerBuilder
para crear nuestro trabajador, que luego podemos ejecutar llamando al método startWork()
.
WorkManager es solo un ejemplo de cómo se pueden usar las corrutinas para simplificar el diseño de las APIs.
En este codelab, abarcamos los conceptos básicos que necesitarás para comenzar a usar corrutinas en tu app.
Vimos los siguientes temas:
- Cómo integrar corrutinas en apps para Android desde la IU y los trabajos de WorkManager para simplificar la programación asíncrona
- Cómo usar corrutinas dentro de un
ViewModel
para recuperar datos de la red y guardarlos en una base de datos sin bloquear el subproceso principal - Y cómo cancelar todas las corrutinas cuando finaliza el
ViewModel
.
Para probar el código basado en corrutinas, abarcamos ambos aspectos probando el comportamiento y llamando directamente a las funciones suspend
desde las pruebas.
Más información
Consulta el codelab "Corrutinas avanzadas con LiveData y flujo de Kotlin" para obtener más información sobre el uso avanzado de corrutinas en Android.
Las corrutinas de Kotlin tienen muchas funciones que no se abordaron en este codelab. Si te interesa obtener más información sobre las corrutinas de Kotlin, lee las guías de corrutinas publicadas por JetBrains. También consulta "Cómo mejorar el rendimiento de la app con corrutinas de Kotlin" para obtener más patrones de uso de corrutinas en Android.