Utilizzare le cooutine Kotlin con componenti sensibili al ciclo di vita

Le coroutine Kotlin forniscono un'API che ti consente di scrivere codice asincrono. Con le coroutine Kotlin, puoi definire un CoroutineScope, che ti aiuta a gestire quando devono essere eseguite le coroutine. Ogni operazione asincrona viene eseguita all'interno di un ambito specifico.

I componenti sensibili al ciclo di vita forniscono un supporto di prima classe per le coroutine per gli ambiti logici della tua app. Questo documento spiega come utilizzare le coroutine in modo efficace con i componenti sensibili al ciclo di vita.

Aggiungi dipendenze

Gli ambiti delle coroutine integrati descritti in questo argomento sono contenuti nell'API Lifecycle. Assicurati di aggiungere le dipendenze appropriate quando utilizzi questi ambiti.

  • Per le utilità ViewModel in Compose, utilizza implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version").
  • Per le utilità del ciclo di vita in Compose, utilizza implementation("androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version").

Ambiti delle coroutine sensibili al ciclo di vita

Compose e le librerie del ciclo di vita forniscono i seguenti ambiti integrati che puoi utilizzare nella tua app.

ViewModelScope

Viene definito un ViewModelScope per ogni ViewModel nella tua app. Qualsiasi coroutine avviata in questo ambito viene annullata automaticamente se ViewModel viene cancellato. Le coroutine sono utili in questo caso quando hai un lavoro che deve essere eseguito solo se ViewModel è attivo. Ad esempio, se stai calcolando alcuni dati per un layout, devi limitare l'ambito del lavoro a ViewModel in modo che, se ViewModel viene cancellato, il lavoro venga annullato automaticamente per evitare di consumare risorse.

Puoi accedere alla CoroutineScope di un ViewModel tramite la proprietà viewModelScope del ViewModel, come mostrato nell'esempio seguente:

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

Per casi d'uso più avanzati, puoi passare un CoroutineScope personalizzato direttamente nel costruttore di ViewModel per sostituire l'viewModelScope predefinito. Questo approccio offre maggiore controllo e flessibilità, in particolare per:

  • Test: consente di inserire un TestScope, semplificando il controllo del tempo e la verifica del comportamento delle coroutine nei test delle unità.

  • Configurazione personalizzata: puoi configurare l'ambito con un CoroutineDispatcher specifico (ad esempio Dispatchers.Default per calcoli complessi) o un CoroutineExceptionHandler personalizzato prima ancora che ViewModel inizi il suo lavoro.

Ambiti associati alla composizione

Gli effetti collaterali, come animazioni, chiamate di rete o timer, devono essere limitati al ciclo di vita del composable. In questo modo, quando un composable lascia lo schermo (esce dalla composizione), tutte le coroutine in esecuzione vengono annullate automaticamente per evitare perdite di memoria.

Compose fornisce l'API LaunchedEffect per gestire l'ambito della composizione in modo dichiarativo.

LaunchedEffect crea un CoroutineScope che ti consente di eseguire funzioni di sospensione. L'ambito è legato al ciclo di vita della composizione del composable, non al ciclo di vita dell'attività host.

  • Enter: la coroutine viene avviata quando il composable entra nella composizione.
  • Uscita: la coroutine viene annullata quando il composable esce dalla composizione.
  • Riavvio: se una chiave passata a LaunchedEffect cambia, la coroutine esistente viene annullata e ne viene avviata una nuova.

L'esempio seguente mostra come utilizzare LaunchedEffect per creare un'animazione pulsante. La coroutine è legata alla presenza del composable nella composizione e reagisce alle modifiche alla configurazione:

// 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)
    }
}

Per saperne di più su LaunchedEffect, consulta Effetti collaterali in Compose.

Raccolta di flussi sensibili al ciclo di vita

Per raccogliere i flussi in modo sicuro in Jetpack Compose, utilizza l'API collectAsStateWithLifecycle. Questa singola funzione converte un Flow in un oggetto Compose State e gestisce automaticamente la sottoscrizione del ciclo di vita. Per impostazione predefinita, la raccolta inizia quando il ciclo di vita è STARTED e si interrompe quando il ciclo di vita è STOPPED. Per eseguire l'override di questo comportamento predefinito, passa il parametro minActiveState con il metodo del ciclo di vita che preferisci, ad esempio Lifecycle.State.RESUMED.

L'esempio seguente mostra come raccogliere il StateFlow di un ViewModel in un componibile:

@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)
    /* ... */
}

Raccolta parallela di più flussi

In Compose, puoi raccogliere più flussi in parallelo dichiarando più variabili di stato. Poiché collectAsStateWithLifecycle gestisce il proprio ambito sottostante, la raccolta parallela viene gestita automaticamente:

@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()

    // ...
}

Calcola i valori in modo asincrono utilizzando i flussi

Quando devi calcolare i valori in modo asincrono, utilizza StateFlow con l'operatore stateIn.

Lo snippet seguente utilizza un Flow standard convertito in un StateFlow. Il parametro WhileSubscribed(5000) mantiene l'iscrizione attiva per cinque secondi dopo la scomparsa della UI per gestire le modifiche alla configurazione.

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

Utilizza collectAsStateWithLifecycle per convertire i valori raccolti in Compose State, in modo che la UI possa aggiornarsi in modo reattivo ogni volta che i dati cambiano.

Per saperne di più sullo stato, consulta Stato e Jetpack Compose.

Risorse aggiuntive

Visualizza contenuti

Esempi