Kotlin-Koroutinen mit lebenszyklusbezogenen Komponenten (Ansichten) verwenden

Konzepte und Jetpack Compose-Implementierung

Kotlin-Koroutinen bieten eine API, mit der Sie asynchronen Code schreiben können. Mit Kotlin-Koroutinen können Sie einen CoroutineScope definieren, der Ihnen hilft zu verwalten, wann Ihre Koroutinen ausgeführt werden sollen. Jeder asynchrone Vorgang wird in einem bestimmten Bereich ausgeführt.

Lebenszyklusbezogene Komponenten bieten erstklassige Unterstützung für Koroutinen für logische Bereiche in Ihrer App sowie eine Interoperabilitätsebene mit LiveData. In diesem Thema wird erläutert, wie Sie Koroutinen effektiv mit lebenszyklusbezogenen Komponenten verwenden.

KTX-Abhängigkeiten hinzufügen

Die in diesem Thema beschriebenen integrierten Koroutinenbereiche sind in den KTX Erweiterungen für die jeweilige Komponente enthalten. Achten Sie darauf, die entsprechenden Abhängigkeiten hinzuzufügen, wenn Sie diese Bereiche verwenden.

  • Verwenden Sie für ViewModelScope die Version androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0 oder höher.
  • Verwenden Sie für LifecycleScope die Version androidx.lifecycle:lifecycle-runtime-ktx:2.4.0 oder höher.
  • Verwenden Sie für liveData die Version androidx.lifecycle:lifecycle-livedata-ktx:2.4.0 oder höher.

Lebenszyklusbezogene Koroutinenbereiche

Lebenszyklusbezogene Komponenten definieren die folgenden integrierten Bereiche, die Sie in Ihrer App verwenden können.

ViewModelScope

Für jedes ViewModel in Ihrer App wird ein ViewModelScope definiert. Alle in diesem Bereich gestarteten Koroutinen werden automatisch abgebrochen, wenn das ViewModel gelöscht wird. Koroutinen sind hier nützlich, wenn Sie Aufgaben haben, die nur ausgeführt werden müssen, wenn das ViewModel aktiv ist. Wenn Sie beispielsweise einige Daten für ein Layout berechnen, sollten Sie die Aufgabe auf das ViewModel beschränken, damit sie automatisch abgebrochen wird, wenn das ViewModel gelöscht wird, um Ressourcen zu sparen.

Sie können über die Eigenschaft viewModelScope des ViewModel auf den CoroutineScope eines ViewModel zugreifen, wie im folgenden Beispiel gezeigt:

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

LifecycleScope

Für jedes Lifecycle-Objekt wird ein LifecycleScope definiert. Alle in diesem Bereich gestarteten Koroutinen werden abgebrochen, wenn das Lifecycle zerstört wird. Sie können über die Eigenschaften lifecycle.coroutineScope oder lifecycleOwner.lifecycleScope auf den CoroutineScope des Lifecycle zugreifen.

Im folgenden Beispiel wird gezeigt, wie Sie mit lifecycleOwner.lifecycleScope vorab berechneten Text asynchron erstellen:

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

Neustartfähige lebenszyklusbezogene Koroutinen

Obwohl lifecycleScope eine geeignete Möglichkeit bietet, lang andauernde Vorgänge automatisch abzubrechen, wenn das Lifecycle den Status DESTROYED hat, gibt es möglicherweise andere Fälle, in denen Sie die Ausführung eines Codeblocks starten möchten, wenn sich das Lifecycle in einem bestimmten Status befindet, und sie abbrechen möchten, wenn es sich in einem anderen Status befindet. Sie können beispielsweise einen Flow erfassen, wenn das Lifecycle den Status STARTED hat, und die Erfassung abbrechen, wenn es den Status STOPPED hat. Bei diesem Ansatz werden die Flow-Emissionen nur verarbeitet, wenn die Benutzeroberfläche auf dem Bildschirm sichtbar ist. So werden Ressourcen gespart und App-Abstürze möglicherweise vermieden.

Für diese Fälle bieten Lifecycle und LifecycleOwner die suspend repeatOnLifecycle API, die genau das tut. Das folgende Beispiel enthält einen Codeblock, der jedes Mal ausgeführt wird, wenn das zugehörige Lifecycle mindestens den Status STARTED hat, und abgebrochen wird, wenn das Lifecycle den Status STOPPED hat:

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

Lebenszyklusbezogene Flow-Erfassung

Wenn Sie nur eine lebenszyklusbezogene Erfassung für einen einzelnen Flow ausführen müssen, können Sie die Flow.flowWithLifecycle() Methode verwenden, um Ihren Code zu vereinfachen:

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

Wenn Sie jedoch eine lebenszyklusbezogene Erfassung für mehrere Flows parallel ausführen müssen, müssen Sie jeden Flow in verschiedenen Koroutinen erfassen. In diesem Fall ist es effizienter, repeatOnLifecycle() direkt zu verwenden:

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

Lebenszyklusbezogene Koroutinen aussetzen

Obwohl CoroutineScope eine geeignete Möglichkeit bietet, lang andauernde Vorgänge automatisch abzubrechen, gibt es möglicherweise andere Fälle, in denen Sie die Ausführung eines Codeblocks aussetzen möchten, es sei denn, das Lifecycle befindet sich in einem bestimmten Status. Wenn Sie beispielsweise eine FragmentTransaction ausführen möchten, müssen Sie warten, bis das Lifecycle mindestens den Status STARTED hat. Für diese Fälle bietet Lifecycle zusätzliche Methoden: lifecycle.whenCreated, lifecycle.whenStarted und lifecycle.whenResumed. Alle Koroutinen, die in diesen Blöcken ausgeführt werden, werden ausgesetzt, wenn das Lifecycle nicht mindestens den gewünschten Mindeststatus hat.

Das folgende Beispiel enthält einen Codeblock, der nur ausgeführt wird, wenn das zugehörige Lifecycle mindestens den Status STARTED hat:

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.

        }
    }
}

Wenn das Lifecycle zerstört wird, während eine Koroutine über eine der when-Methoden aktiv ist, wird die Koroutine automatisch abgebrochen. Im folgenden Beispiel wird der finally-Block ausgeführt, sobald das Lifecycle den Status DESTROYED hat:

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.
                }
            }
        }
    }
}

Koroutinen mit LiveData verwenden

Wenn Sie LiveData verwenden, müssen Sie möglicherweise Werte asynchron berechnen. Sie können beispielsweise die Einstellungen eines Nutzers abrufen und sie auf der Benutzeroberfläche bereitstellen. In diesen Fällen können Sie mit der Builder-Funktion liveData eine suspend-Funktion aufrufen und das Ergebnis als LiveData-Objekt bereitstellen.

Im folgenden Beispiel ist loadUser() eine suspend-Funktion, die an anderer Stelle deklariert wurde. Verwenden Sie die Builder-Funktion liveData, um loadUser() asynchron aufzurufen, und geben Sie dann das Ergebnis mit emit() aus:

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

Der liveData Baustein dient als strukturiertes Parallelitäts Primitiv zwischen Koroutinen und LiveData. Der Codeblock wird ausgeführt, wenn LiveData aktiv wird, und nach einem konfigurierbaren Timeout automatisch abgebrochen, wenn LiveData inaktiv wird. Wenn er vor Abschluss abgebrochen wird, wird er neu gestartet, wenn LiveData wieder aktiv wird. Wenn er bei einer vorherigen Ausführung erfolgreich abgeschlossen wurde, wird er nicht neu gestartet. Er wird nur neu gestartet, wenn er automatisch abgebrochen wird. Wenn der Block aus einem anderen Grund abgebrochen wird (z.B. durch Auslösen einer CancellationException), wird er nicht neu gestartet.

Sie können auch mehrere Werte aus dem Block ausgeben. Jeder emit()-Aufruf setzt die Ausführung des Blocks aus, bis der LiveData-Wert im Hauptthread festgelegt ist.

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

Sie können liveData auch mit Transformations kombinieren, wie im folgenden Beispiel gezeigt:

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

Sie können mehrere Werte aus einem LiveData ausgeben, indem Sie die Funktion emitSource() aufrufen, wenn Sie einen neuen Wert ausgeben möchten. Beachten Sie, dass mit jedem Aufruf von emit() oder emitSource() die zuvor hinzugefügte Quelle entfernt wird.

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

Weitere Informationen zu Koroutinen finden Sie unter den folgenden Links:

Zusätzliche Ressourcen

Weitere Informationen zur Verwendung von Koroutinen mit lebenszyklusbezogenen Komponenten finden Sie in den folgenden zusätzlichen Ressourcen.

Beispiele

Blogs