Cómo usar las corrutinas de Kotlin con componentes optimizados para ciclos de vida

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

Los componentes optimizados para ciclos de vida proporcionan compatibilidad de primer nivel con las corrutinas para alcances lógicos en tu app. En este documento, se explica cómo usar corrutinas de manera eficaz con componentes optimizados para ciclos de vida.

Cómo agregar dependencias

Los alcances integrados de las corrutinas que se describen en este tema se encuentran en la API de Lifecycle. Asegúrate de agregar las dependencias apropiadas cuando uses estos alcances.

  • Para las utilidades de ViewModel en Compose, usa implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version").
  • Para las utilidades de Lifecycle en Compose, usa implementation("androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version").

Ámbitos de corrutinas optimizados para ciclos de vida

Compose y las bibliotecas de Lifecycle proporcionan los siguientes alcances integrados que puedes usar en tu app.

ViewModelScope

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

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

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

Para casos de uso más avanzados, puedes pasar un CoroutineScope personalizado directamente al constructor del ViewModel para reemplazar el viewModelScope predeterminado. Este enfoque ofrece más control y flexibilidad, en especial para lo siguiente:

  • Pruebas: Te permite insertar un TestScope, lo que facilita el control del tiempo y la verificación del comportamiento de las corrutinas en las pruebas de unidades.

  • Configuración personalizada: Puedes configurar el alcance con un CoroutineDispatcher específico (como Dispatchers.Default para un procesamiento pesado) o un CoroutineExceptionHandler personalizado antes de que el ViewModel comience su trabajo.

Permisos vinculados a la composición

Los efectos secundarios, como las animaciones, las llamadas de red o los temporizadores, deben limitarse al ciclo de vida del elemento componible. De esta manera, cuando un elemento componible sale de la pantalla (sale de la composición), se cancelan automáticamente todas las corrutinas en ejecución para evitar pérdidas de memoria.

Compose proporciona la API de LaunchedEffect para controlar el alcance de la composición de forma declarativa.

LaunchedEffect crea un CoroutineScope que te permite ejecutar funciones de suspensión. El alcance está vinculado al ciclo de vida de la composición del elemento componible, no al Lifecycle de la actividad host.

  • Enter: La corrutina comienza cuando el elemento componible ingresa a la composición.
  • Salida: La corrutina se cancela cuando el elemento componible sale de la composición.
  • Relanzamiento: Si cambia alguna clave que se pasó a LaunchedEffect, se cancela la corrutina existente y se lanza una nueva.

En el siguiente ejemplo, se muestra cómo usar LaunchedEffect para crear una animación de pulsación. La corrutina está vinculada a la presencia del elemento componible en la composición y reacciona a los cambios de configuración:

// Allow the pulse rate to be configured, so it can be sped up if the user is running
// out of time
var pulseRateMs by remember { mutableLongStateOf(3000L) }
val alpha = remember { Animatable(1f) }
LaunchedEffect(pulseRateMs) { // Restart the effect when the pulse rate changes
    while (isActive) {
        delay(pulseRateMs) // Pulse the alpha every pulseRateMs to alert the user
        alpha.animateTo(0f)
        alpha.animateTo(1f)
    }
}

Para obtener más información sobre LaunchedEffect, consulta Efectos secundarios en Compose.

Recopilación de flujos optimizados para ciclos de vida

Para recopilar flujos de forma segura en Jetpack Compose, usa la API de collectAsStateWithLifecycle. Esta única función convierte un Flow en un objeto State de Compose y administra automáticamente la suscripción del ciclo de vida por ti. De forma predeterminada, la recopilación comienza cuando el ciclo de vida es STARTED y se detiene cuando el ciclo de vida es STOPPED. Para anular este comportamiento predeterminado, pasa el parámetro minActiveState con el método de ciclo de vida que desees, como Lifecycle.State.RESUMED.

En el siguiente ejemplo, se muestra cómo recopilar el StateFlow de un ViewModel en un elemento componible:

@Composable
private fun ConversationScreen(
    conversationViewModel: ConversationViewModel = viewModel()
) {

    val messages by conversationViewModel.messages.collectAsStateWithLifecycle()

    ConversationScreen(
        messages = messages,
        onSendMessage = { message: Message -> conversationViewModel.sendMessage(message) }
    )
}

@Composable
private fun ConversationScreen(
    messages: List<Message>,
    onSendMessage: (Message) -> Unit
) {

    MessagesList(messages, onSendMessage)
    /* ... */
}

Recolección paralela de varios flujos

En Compose, puedes recopilar varios flujos en paralelo declarando múltiples variables de estado. Dado que collectAsStateWithLifecycle administra su propio alcance subyacente, la recopilación paralela se controla automáticamente:

@Composable
fun DashboardScreen(viewModel: DashboardViewModel = viewModel()) {
    // Both flows are collected safely in parallel and will emit updates when either changes, the composables will recompose
    val userData by viewModel.userFlow.collectAsStateWithLifecycle()
    val feedData by viewModel.feedFlow.collectAsStateWithLifecycle()

    // ...
}

Cómo calcular valores de forma asíncrona con Flows

Cuando necesites calcular valores de forma asíncrona, usa StateFlow con el operador stateIn.

En el siguiente fragmento, se usa un Flow estándar convertido en un StateFlow. El parámetro WhileSubscribed(5000) mantiene la suscripción activa durante cinco segundos después de que desaparece la IU para controlar los cambios de configuración.

val uiState: StateFlow<Result> = flow {
    emit(repository.fetchData())
}
.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(5_000),
    initialValue = Result.Loading
)

Usa collectAsStateWithLifecycle para convertir los valores recopilados en State de Compose, de modo que tu IU pueda actualizarse de forma reactiva cada vez que cambien los datos.

Para obtener más información sobre el estado, consulta El estado y Jetpack Compose.

Recursos adicionales

Mira contenido

Ejemplos