Concetti e implementazione di Jetpack Compose
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 in un ambito specifico.
I componenti che tengono conto del ciclo di vita forniscono un supporto di prima classe 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 i componenti che tengono conto del ciclo di vita.
Aggiungere dipendenze KTX
Gli ambiti delle coroutine integrati descritti in questo argomento sono contenuti nelle estensioni KTX per ogni componente corrispondente. Assicurati di aggiungere le dipendenze appropriate quando utilizzi questi ambiti.
- Per
ViewModelScope, utilizzaandroidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0o versioni successive. - Per
LifecycleScope, utilizzaandroidx.lifecycle:lifecycle-runtime-ktx:2.4.0o versioni successive. - Per
liveData, utilizzaandroidx.lifecycle:lifecycle-livedata-ktx:2.4.0o versioni successive.
Ambiti delle coroutine che tengono conto del ciclo di vita
I componenti che tengono conto del ciclo di vita definiscono i seguenti ambiti integrati che puoi utilizzare nella tua app.
ViewModelScope
Un ViewModelScope è definito per ogni ViewModel nella tua app. Qualsiasi
coroutine avviata in questo ambito viene annullata automaticamente se il ViewModel viene
cancellato. Le coroutine sono utili in questo caso quando hai un lavoro da svolgere solo se ViewModel è attivo. Ad esempio, se stai calcolando alcuni dati per un layout, devi limitare il lavoro a ViewModel in modo che, se ViewModel viene cancellato, il lavoro venga annullato automaticamente per evitare di consumare risorse.
Puoi accedere a CoroutineScope di un ViewModel tramite la proprietà viewModelScope di ViewModel, come mostrato nell'esempio seguente:
class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.
}
}
}
LifecycleScope
Un LifecycleScope è definito per ogni oggetto Lifecycle. Qualsiasi coroutine avviata in questo ambito viene annullata quando Lifecycle viene eliminato. Puoi accedere a 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 che tengono conto del 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 è in un determinato stato e annullare 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 di 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 di sospensione
repeatOnLifecycle che fa esattamente questo. L'esempio seguente contiene un blocco di codice che viene eseguito ogni volta che 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 che tengono conto del ciclo di vita
Se devi eseguire una raccolta che tiene conto del ciclo di vita su un singolo flusso, puoi
utilizzare il Flow.flowWithLifecycle() metodo per semplificare il codice:
viewLifecycleOwner.lifecycleScope.launch {
exampleProvider.exampleFlow()
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect {
// Process the value.
}
}
Tuttavia, se devi eseguire una raccolta che tiene conto del ciclo di vita su più flussi in parallelo, devi raccogliere ogni flusso in coroutine diverse. 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. */ }
}
}
}
Coroutine che tengono conto del ciclo di vita di sospensione
Anche se CoroutineScope fornisce un modo corretto per annullare automaticamente le operazioni a lunga esecuzione, potresti avere altri casi in cui vuoi sospendere l'esecuzione di un blocco di codice a meno che Lifecycle non sia in un determinato stato. Ad esempio, per eseguire un FragmentTransaction, devi attendere che Lifecycle sia almeno STARTED. In questi casi, Lifecycle fornisce metodi aggiuntivi: lifecycle.whenCreated, lifecycle.whenStarted e lifecycle.whenResumed. Qualsiasi coroutine eseguita all'interno di questi blocchi viene sospesa se Lifecycle non è almeno nello stato minimo desiderato.
L'esempio seguente contiene un blocco di codice che viene eseguito solo quando 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 Lifecycle viene eliminato mentre una coroutine è attiva tramite uno dei metodi when, la coroutine viene annullata automaticamente. Nell'esempio seguente, il blocco finally viene eseguito una volta che 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, potresti dover calcolare i valori in modo asincrono.
Ad esempio, potresti voler recuperare le preferenze di un utente e mostrarle nell'UI. In questi casi, puoi utilizzare la funzione di creazione liveData per chiamare una funzione suspend, che restituisce il risultato come oggetto LiveData.
Nell'esempio seguente, loadUser() è una funzione di sospensione dichiarata altrove. Utilizza la funzione di creazione 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 liveData componente di base funge da primitiva di concorrenza strutturata tra coroutine e LiveData. Il blocco di codice inizia l'esecuzione 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 viene riavviato solo se annullato automaticamente. Se il blocco viene annullato per qualsiasi altro motivo (ad es. generazione di un CancellationException), non viene riavviato.
Puoi anche emettere più valori dal blocco. Ogni chiamata emit() sospende l'esecuzione del blocco finché il valore LiveData non viene impostato 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 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 sulle coroutine, consulta i seguenti link:
- Migliorare le prestazioni delle app con le coroutine Kotlin
- Panoramica delle coroutine
- Threading in CoroutineWorker
Risorse aggiuntive
Per saperne di più sull'utilizzo delle coroutine con i componenti che tengono conto del ciclo di vita, consulta le seguenti risorse aggiuntive.
Esempi
Blog
- Coroutines on Android: Application patterns
- Easy coroutines in Android: viewModelScope
- Testing two consecutive LiveData emissions in coroutines