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
, utilizzaandroidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0
o superiore. - Per
LifecycleScope
, utilizzaandroidx.lifecycle:lifecycle-runtime-ktx:2.4.0
o superiore. - Per
liveData
, utilizzaandroidx.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:
- Migliorare le prestazioni dell'app con le coroutine Kotlin
- Panoramica delle coroutine
- Threading in CoroutineWorker
Risorse aggiuntive
Per saperne di più sull'utilizzo delle coroutine con componenti sensibili al ciclo di vita, consulta le seguenti risorse aggiuntive.
Samples
Blog
- Coroutine su Android: modelli di applicazione
- Coroutine facili in Android: viewModelScope
- Test di due emissioni di LiveData consecutive nelle coroutine
Consigliato per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Panoramica di LiveData
- Gestione dei cicli di vita con componenti sensibili al ciclo di vita
- Caricare e visualizzare i dati impaginati