Utilizzare le cooutine Kotlin con componenti sensibili al ciclo di vita

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

I componenti sensibili al ciclo di vita forniscono un supporto di prima qualità per le coroutine per gli ambiti logici nella tua app, insieme a un livello di interoperabilità con LiveData. Questo argomento spiega come utilizzare le coroutine in modo efficace con componenti sensibili al ciclo di vita.

Aggiungi dipendenze KTX

Gli ambiti delle coroutine integrati descritti in questo argomento sono contenuti nelle estensioni KTX di ogni componente corrispondente. Assicurati di aggiungere le dipendenze appropriate quando utilizzi questi ambiti.

  • Per ViewModelScope, utilizza androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0 o superiore.
  • Per LifecycleScope, utilizza androidx.lifecycle:lifecycle-runtime-ktx:2.4.0 o superiore.
  • Per liveData, utilizza androidx.lifecycle:lifecycle-livedata-ktx:2.4.0 o superiore.

Ambiti per coroutine sensibili al ciclo di vita

I componenti sensibili al ciclo di vita definiscono 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 il valore ViewModel viene cancellato. Le Coroutine sono utili qui quando hai del lavoro da completare solo se ViewModel è attivo. Ad esempio, se stai elaborando alcuni dati per un layout, dovresti limitare il lavoro a ViewModel in modo che, se il valore ViewModel viene cancellato, il lavoro venga annullato automaticamente per evitare di consumare risorse.

Puoi accedere al 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.
        }
    }
}

Ambito del ciclo di vita

Viene definito un LifecycleScope per ogni oggetto Lifecycle. Qualsiasi coroutine lanciata in questo ambito viene annullata quando viene eliminato Lifecycle. Puoi accedere al CoroutineScope di Lifecycle tramite le proprietà lifecycle.coroutineScope o lifecycleOwner.lifecycleScope.

L'esempio seguente mostra come utilizzare lifecycleOwner.lifecycleScope per creare testo precalcolato in modo asincrono:

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch {
            val params = TextViewCompat.getTextMetricsParams(textView)
            val precomputedText = withContext(Dispatchers.Default) {
                PrecomputedTextCompat.create(longTextContent, params)
            }
            TextViewCompat.setPrecomputedText(textView, precomputedText)
        }
    }
}

Coroutine compatibili con il ciclo di vita riavviabili

Anche se lifecycleScope fornisce un modo corretto per annullare automaticamente le operazioni a lunga esecuzione quando Lifecycle è DESTROYED, potresti avere altri casi in cui vuoi avviare l'esecuzione di un blocco di codice quando Lifecycle si trova in un determinato stato e annullare l'operazione quando è in un altro stato. Ad esempio, potresti voler raccogliere un flusso quando Lifecycle è STARTED e annullare la raccolta quando è STOPPED. Questo approccio elabora le emissioni del flusso solo quando l'UI è visibile sullo schermo, risparmiando risorse ed evitando potenzialmente arresti anomali dell'app.

In questi casi, Lifecycle e LifecycleOwner forniscono l'API Sospende repeatOnLifecycle che fa esattamente questo. L'esempio seguente contiene un blocco di codice che viene eseguito ogni volta che l'elemento Lifecycle associato è almeno nello stato STARTED e viene annullato quando Lifecycle è STOPPED:

class MyFragment : Fragment() {

    val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Create a new coroutine in the lifecycleScope
        viewLifecycleOwner.lifecycleScope.launch {
            // repeatOnLifecycle launches the block in a new coroutine every time the
            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Trigger the flow and start listening for values.
                // This happens when lifecycle is STARTED and stops
                // collecting when the lifecycle is STOPPED
                viewModel.someDataFlow.collect {
                    // Process item
                }
            }
        }
    }
}

Raccolta di flussi basati sul ciclo di vita

Se hai bisogno di eseguire la raccolta sensibile al ciclo di vita su un unico flusso, puoi utilizzare il metodo Flow.flowWithLifecycle() per semplificare il codice:

viewLifecycleOwner.lifecycleScope.launch {
    exampleProvider.exampleFlow()
        .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
        .collect {
            // Process the value.
        }
}

Tuttavia, se hai bisogno di eseguire una raccolta sensibile al ciclo di vita su più flussi in parallelo, devi raccogliere ogni flusso in diverse coroutine. In questo caso, è più efficiente utilizzare direttamente repeatOnLifecycle():

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        // Because collect is a suspend function, if you want to
        // collect multiple flows in parallel, you need to do so in
        // different coroutines.
        launch {
            flow1.collect { /* Process the value. */ }
        }

        launch {
            flow2.collect { /* Process the value. */ }
        }
    }
}

Sospendi le coroutine sensibili al ciclo di vita

Anche se CoroutineScope fornisce un modo corretto per annullare automaticamente le operazioni a lunga esecuzione, potrebbero verificarsi altri casi in cui vuoi sospendere l'esecuzione di un blocco di codice, a meno che Lifecycle non si trovi in un determinato stato. Ad esempio, per eseguire un FragmentTransaction, devi attendere fino a quando Lifecycle non raggiunge almeno STARTED. In questi casi, Lifecycle offre metodi aggiuntivi: lifecycle.whenCreated, lifecycle.whenStarted e lifecycle.whenResumed. Qualsiasi esecuzione di coroutine all'interno di questi blocchi viene sospesa se Lifecycle non si trova almeno nello stato desiderato minimo.

L'esempio seguente contiene un blocco di codice che viene eseguito solo quando l'elemento Lifecycle associato è almeno nello stato STARTED:

class MyFragment: Fragment {
    init { // Notice that we can safely launch in the constructor of the Fragment.
        lifecycleScope.launch {
            whenStarted {
                // The block inside will run only when Lifecycle is at least STARTED.
                // It will start executing when fragment is started and
                // can call other suspend methods.
                loadingView.visibility = View.VISIBLE
                val canAccess = withContext(Dispatchers.IO) {
                    checkUserAccess()
                }

                // When checkUserAccess returns, the next line is automatically
                // suspended if the Lifecycle is not *at least* STARTED.
                // We could safely run fragment transactions because we know the
                // code won't run unless the lifecycle is at least STARTED.
                loadingView.visibility = View.GONE
                if (canAccess == false) {
                    findNavController().popBackStack()
                } else {
                    showContent()
                }
            }

            // This line runs only after the whenStarted block above has completed.

        }
    }
}

Se la Lifecycle viene distrutta mentre è attiva una coroutine tramite uno dei metodi when, la coroutine viene automaticamente annullata. Nell'esempio seguente, il blocco finally viene eseguito quando lo stato Lifecycle è DESTROYED:

class MyFragment: Fragment {
    init {
        lifecycleScope.launchWhenStarted {
            try {
                // Call some suspend functions.
            } finally {
                // This line might execute after Lifecycle is DESTROYED.
                if (lifecycle.state >= STARTED) {
                    // Here, since we've checked, it is safe to run any
                    // Fragment transactions.
                }
            }
        }
    }
}

Utilizzare le coroutine con LiveData

Quando utilizzi LiveData, potrebbe essere necessario calcolare i valori in modo asincrono. Ad esempio, potresti voler recuperare le preferenze di un utente e pubblicarle nella tua UI. In questi casi, puoi utilizzare la funzione del builder di liveData per chiamare una funzione suspend, che fornisce il risultato come un oggetto LiveData.

Nell'esempio seguente, loadUser() è una funzione di sospensione dichiarata altrove. Utilizza la funzione del builder di liveData per chiamare loadUser() in modo asincrono, quindi utilizza emit() per emettere il risultato:

val user: LiveData<User> = liveData {
    val data = database.loadUser() // loadUser is a suspend function.
    emit(data)
}

Il componente di base liveData funge da primitiva di contemporaneità strutturata tra le coroutine e LiveData. L'esecuzione del blocco di codice inizia quando LiveData diventa attivo e viene annullato automaticamente dopo un timeout configurabile quando LiveData diventa inattivo. Se viene annullato prima del completamento, viene riavviato se LiveData diventa di nuovo attivo. Se è stato completato correttamente in un'esecuzione precedente, non viene riavviato. Tieni presente che l'operazione viene riavviata solo se l'operazione viene annullata automaticamente. Se il blocco viene annullato per qualsiasi altro motivo (ad esempio la generazione di un CancellationException), non viene riavviato.

Puoi anche emettere più valori dal blocco. Ogni chiamata emit() sospende l'esecuzione del blocco fino a quando non viene impostato il valore di LiveData nel thread principale.

val user: LiveData<Result> = liveData {
    emit(Result.loading())
    try {
        emit(Result.success(fetchUser()))
    } catch(ioException: Exception) {
        emit(Result.error(ioException))
    }
}

Puoi anche combinare liveData con Transformations, come mostrato nell'esempio seguente:

class MyViewModel: ViewModel() {
    private val userId: LiveData<String> = MutableLiveData()
    val user = userId.switchMap { id ->
        liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
            emit(database.loadUserById(id))
        }
    }
}

Puoi emettere più valori da un valore LiveData chiamando la funzione emitSource() ogni volta che vuoi emettere un nuovo valore. Tieni presente che ogni chiamata a emit() o emitSource() rimuove l'origine aggiunta in precedenza.

class UserDao: Dao {
    @Query("SELECT * FROM User WHERE id = :id")
    fun getUser(id: String): LiveData<User>
}

class MyRepository {
    fun getUser(id: String) = liveData<User> {
        val disposable = emitSource(
            userDao.getUser(id).map {
                Result.loading(it)
            }
        )
        try {
            val user = webservice.fetchUser(id)
            // Stop the previous emission to avoid dispatching the updated user
            // as `loading`.
            disposable.dispose()
            // Update the database.
            userDao.insert(user)
            // Re-establish the emission with success type.
            emitSource(
                userDao.getUser(id).map {
                    Result.success(it)
                }
            )
        } catch(exception: IOException) {
            // Any call to `emit` disposes the previous one automatically so we don't
            // need to dispose it here as we didn't get an updated value.
            emitSource(
                userDao.getUser(id).map {
                    Result.error(exception, it)
                }
            )
        }
    }
}

Per ulteriori informazioni relative alle coroutine, vedi i seguenti link:

Risorse aggiuntive

Per saperne di più sull'utilizzo delle coroutine con componenti sensibili al ciclo di vita, consulta le seguenti risorse aggiuntive.

Samples

Blog