Auf dieser Seite werden mehrere Best Practices vorgestellt, die sich positiv auf die Skalierbarkeit und Testbarkeit Ihrer App auswirken, wenn Sie Coroutinen verwenden.
Dispatcher einschleusen
Codieren Sie Dispatchers nicht fest, wenn Sie neue Coroutinen erstellen oder withContext aufrufen.
// DO inject Dispatchers class NewsRepository( private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default ) { suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ } } // DO NOT hardcode Dispatchers class NewsRepository { // DO NOT use Dispatchers.Default directly, inject it instead suspend fun loadNews() = withContext(Dispatchers.Default) { /* ... */ } }
Dieses Muster für die Abhängigkeitsinjektion erleichtert das Testen, da Sie diese Dispatcher in Einheiten- und Instrumentierungstests durch einen Test-Dispatcher ersetzen können, um Ihre Tests deterministischer zu machen.
Suspend-Funktionen sollten sicher vom Hauptthread aus aufrufbar sein
Suspend-Funktionen sollten main-safe sein, d. h., sie können sicher vom Hauptthread aus aufgerufen werden. Wenn eine Klasse in einer Coroutine lange blockierende Vorgänge ausführt, ist sie dafür verantwortlich, die Ausführung mit withContext vom Hauptthread zu verlagern. Dies gilt für alle Klassen in Ihrer App, unabhängig davon, in welchem Teil der Architektur sich die Klasse befindet.
class NewsRepository(private val ioDispatcher: CoroutineDispatcher) { // As this operation is manually retrieving the news from the server // using a blocking HttpURLConnection, it needs to move the execution // to an IO dispatcher to make it main-safe suspend fun fetchLatestNews(): List<Article> { withContext(ioDispatcher) { /* ... implementation ... */ } } } // This use case fetches the latest news and the associated author. class GetLatestNewsWithAuthorsUseCase( private val newsRepository: NewsRepository, private val authorsRepository: AuthorsRepository ) { // This method doesn't need to worry about moving the execution of the // coroutine to a different thread as newsRepository is main-safe. // The work done in the coroutine is lightweight as it only creates // a list and add elements to it suspend operator fun invoke(): Result<List<ArticleWithAuthor>> { val news = newsRepository.fetchLatestNews() val response = mutableListOf<ArticleWithAuthor>() for (article in news) { val author = authorsRepository.getAuthor(article.author) response.add(ArticleWithAuthor(article, author)) } return Result.Success(response) } }
Dieses Muster macht Ihre App skalierbarer, da sich Klassen, die Suspend-Funktionen aufrufen, nicht darum kümmern müssen, welchen Dispatcher sie für welche Art von Arbeit verwenden sollen. Diese Verantwortung liegt bei der Klasse, die die Arbeit ausführt.
Das ViewModel sollte Coroutinen erstellen
ViewModel -Klassen sollten Coroutinen erstellen, anstatt Suspend-Funktionen zur Ausführung von Geschäftslogik zu verwenden. Suspend-Funktionen im ViewModel können nützlich sein, wenn anstelle der Bereitstellung des Status über einen Datenstream nur ein einzelner Wert ausgegeben werden muss.
// DO create coroutines in the ViewModel class LatestNewsViewModel( private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase ) : ViewModel() { private val _uiState = MutableStateFlow<LatestNewsUiState>(LatestNewsUiState.Loading) val uiState: StateFlow<LatestNewsUiState> = _uiState fun loadNews() { viewModelScope.launch { val latestNewsWithAuthors = getLatestNewsWithAuthors() _uiState.value = LatestNewsUiState.Success(latestNewsWithAuthors) } } }
// Prefer observable state rather than suspend functions from the ViewModel class LatestNewsViewModel( private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase ) : ViewModel() { // DO NOT do this. News would probably need to be refreshed as well. // Instead of exposing a single value with a suspend function, news should // be exposed using a stream of data as in the code snippet above. suspend fun loadNews() = getLatestNewsWithAuthors() }
Ansichten sollten keine Coroutinen direkt auslösen, um Geschäftslogik auszuführen.
Übertragen Sie diese Verantwortung stattdessen an das ViewModel. Dadurch lässt sich Ihre Geschäftslogik einfacher testen, da ViewModel-Objekte Einheitentests unterzogen werden können, anstatt Instrumentierungstests zu verwenden, die zum Testen von Ansichten erforderlich sind.
Außerdem überstehen Ihre Coroutinen Konfigurationsänderungen automatisch, wenn die Arbeit im viewModelScope gestartet wird. Wenn Sie Coroutinen stattdessen mit lifecycleScope erstellen, müssen Sie das manuell verarbeiten.
Wenn die Coroutine den Bereich von ViewModel überdauern muss, lesen Sie den Abschnitt
Coroutinen in der Geschäfts- und Datenschicht erstellen.
Veränderliche Typen nicht verfügbar machen
Es ist besser, unveränderliche Typen für andere Klassen verfügbar zu machen. Auf diese Weise werden alle Änderungen am veränderlichen Typ in einer Klasse zentralisiert, was die Fehlerbehebung erleichtert, wenn etwas schiefgeht.
// DO expose immutable types class LatestNewsViewModel : ViewModel() { private val _uiState = MutableStateFlow(LatestNewsUiState.Loading) val uiState: StateFlow<LatestNewsUiState> = _uiState /* ... */ }
class LatestNewsViewModel : ViewModel() { // DO NOT expose mutable types val uiState = MutableStateFlow(LatestNewsUiState.Loading) /* ... */ }
Die Daten- und Geschäftsschicht sollten Suspend-Funktionen und Flows verfügbar machen
Klassen in der Daten- und Geschäftsschicht machen in der Regel Funktionen verfügbar, um einmalige Aufrufe auszuführen oder um über Datenänderungen im Laufe der Zeit benachrichtigt zu werden. Klassen in diesen Schichten sollten Suspend-Funktionen für einmalige Aufrufe und Flow zur Benachrichtigung über Datenänderungen verfügbar machen.
// Classes in the data and business layer expose // either suspend functions or Flows class ExampleRepository { suspend fun makeNetworkRequest() { /* ... */ } fun getExamples(): Flow<Example> { /* ... */ } }
Diese Best Practice ermöglicht es dem Aufrufer, in der Regel der Präsentationsschicht, die Ausführung und den Lebenszyklus der Arbeit in diesen Schichten zu steuern und bei Bedarf abzubrechen.
Coroutinen in der Geschäfts- und Datenschicht erstellen
Für Klassen in der Daten- oder Geschäftsschicht, die aus verschiedenen Gründen Coroutinen erstellen müssen, gibt es verschiedene Optionen.
Wenn die in diesen Coroutinen auszuführende Arbeit nur relevant ist, wenn der Nutzer sich auf dem aktuellen Bildschirm befindet, sollte sie dem Lebenszyklus des Aufrufers folgen. In den meisten Fällen ist der Aufrufer das ViewModel und der Aufruf wird abgebrochen, wenn der Nutzer den Bildschirm verlässt und das ViewModel gelöscht wird. In diesem Fall,
coroutineScope
oder supervisorScope
sollten verwendet werden.
class GetAllBooksAndAuthorsUseCase( private val booksRepository: BooksRepository, private val authorsRepository: AuthorsRepository, ) { suspend fun getBookAndAuthors(): BookAndAuthors { // In parallel, fetch books and authors and return when both requests // complete and the data is ready return coroutineScope { val books = async { booksRepository.getAllBooks() } val authors = async { authorsRepository.getAllAuthors() } BookAndAuthors(books.await(), authors.await()) } } }
Wenn die auszuführende Arbeit so lange relevant ist, wie die App geöffnet ist, und die Arbeit nicht an einen bestimmten Bildschirm gebunden ist, sollte die Arbeit den Lebenszyklus des Aufrufers überdauern. Für dieses Szenario sollte ein externer CoroutineScope verwendet werden, wie
im Blogpost Coroutinen und Muster für Arbeit, die nicht abgebrochen werden sollte erläutert.
class ArticlesRepository( private val articlesDataSource: ArticlesDataSource, private val externalScope: CoroutineScope, ) { // As we want to complete bookmarking the article even if the user moves // away from the screen, the work is done creating a new coroutine // from an external scope suspend fun bookmarkArticle(article: Article) { externalScope.launch { articlesDataSource.bookmarkArticle(article) } .join() // Wait for the coroutine to complete } }
externalScope sollte von einer Klasse erstellt und verwaltet werden, die länger als der aktuelle Bildschirm aktiv ist. Sie kann von der Klasse Application oder einem ViewModel verwaltet werden, das auf einen Navigationsgraphen beschränkt ist.
TestDispatchers in Tests einschleusen
Eine Instanz von
TestDispatcher
sollte in Tests in Ihre Klassen eingeschleust werden. In der Bibliothek
kotlinx-coroutines-test sind zwei Implementierungen verfügbar:
StandardTestDispatcher: Stellt Coroutinen, die darauf gestartet wurden, mit einem Scheduler in die Warteschlange und führt sie aus, wenn der Testthread nicht ausgelastet ist. Sie können den Testthread anhalten, damit andere in die Warteschlange gestellte Coroutinen mit Methoden wieadvanceUntilIdleausgeführt werden können.UnconfinedTestDispatcher: Führt neue Coroutinen sofort und blockierend aus. Dadurch wird das Schreiben von Tests im Allgemeinen einfacher, aber Sie haben weniger Kontrolle darüber, wie Coroutinen während des Tests ausgeführt werden.
Weitere Informationen finden Sie in der Dokumentation der einzelnen Dispatcher-Implementierungen.
Verwenden Sie den
runTest
Coroutinen-Builder, um Coroutinen zu testen. runTest uses a
TestCoroutineScheduler
um Verzögerungen in Tests zu überspringen und die virtuelle Zeit zu steuern. Sie können diesen Scheduler auch verwenden, um bei Bedarf zusätzliche Test-Dispatcher zu erstellen.
class ArticlesRepositoryTest { @Test fun testBookmarkArticle() = runTest { // Pass the testScheduler provided by runTest's coroutine scope to // the test dispatcher val testDispatcher = UnconfinedTestDispatcher(testScheduler) val articlesDataSource = FakeArticlesDataSource() val repository = ArticlesRepository( articlesDataSource, defaultDispatcher = testDispatcher ) val article = Article() repository.bookmarkArticle(article) assertThat(articlesDataSource.isBookmarked(article)).isTrue() } }
Alle TestDispatchers sollten denselben Scheduler verwenden. So können Sie Ihren gesamten Coroutinen-Code im einzelnen Testthread ausführen, um Ihre Tests deterministisch zu machen. runTest wartet, bis alle Coroutinen, die sich auf demselben Scheduler befinden oder untergeordnete Elemente der Test-Coroutine sind, abgeschlossen sind, bevor die Funktion zurückkehrt.
GlobalScope vermeiden
Dies ähnelt der Best Practice Dispatcher einschleusen. Wenn Sie
GlobalScope,
den CoroutineScope fest codieren, den eine Klasse verwendet, was einige Nachteile
mit sich bringt:
Fördert das Festcodieren von Werten. Wenn Sie
GlobalScopefest codieren, codieren Sie möglicherweise auchDispatchersfest.Erschwert das Testen erheblich, da Ihr Code in einem unkontrollierten Bereich ausgeführt wird und Sie die Ausführung nicht steuern können.
Sie können keinen gemeinsamen
CoroutineContextfür alle Coroutinen verwenden, die in den Bereich selbst eingebunden sind.
Erwägen Sie stattdessen, einen CoroutineScope für Arbeit einzuschleusen, die den aktuellen Bereich überdauern muss. Weitere Informationen finden Sie im Abschnitt
Coroutinen in der Geschäfts- und Datenschicht erstellen.
// DO inject an external scope instead of using GlobalScope. // GlobalScope can be used indirectly. Here as a default parameter makes sense. class ArticlesRepository( private val articlesDataSource: ArticlesDataSource, private val externalScope: CoroutineScope = GlobalScope, private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default ) { // As we want to complete bookmarking the article even if the user moves // away from the screen, the work is done creating a new coroutine // from an external scope suspend fun bookmarkArticle(article: Article) { externalScope.launch(defaultDispatcher) { articlesDataSource.bookmarkArticle(article) } .join() // Wait for the coroutine to complete } }
// DO NOT use GlobalScope directly class ArticlesRepository( private val articlesDataSource: ArticlesDataSource, ) { // As we want to complete bookmarking the article even if the user moves away // from the screen, the work is done creating a new coroutine with GlobalScope suspend fun bookmarkArticle(article: Article) { GlobalScope.launch { articlesDataSource.bookmarkArticle(article) } .join() // Wait for the coroutine to complete } }
Weitere Informationen zu GlobalScope und seinen Alternativen finden Sie im
Blogpost Coroutinen und Muster für Arbeit, die nicht abgebrochen werden sollte.
Coroutine abbrechbar machen
Der Abbruch in Coroutinen ist kooperativ. Das bedeutet, dass eine Coroutine erst abgebrochen wird, wenn sie angehalten wird oder auf einen Abbruch prüft, nachdem ihr Job abgebrochen wurde. Wenn Sie blockierende Vorgänge in einer Coroutine ausführen, muss die Coroutine abbrechbar sein.
Wenn Sie beispielsweise mehrere Dateien von der Festplatte lesen, prüfen Sie vor dem Lesen jeder Datei, ob die Coroutine abgebrochen wurde. Eine Möglichkeit, auf einen Abbruch zu prüfen, ist der Aufruf der
ensureActive
Funktion.
someScope.launch { for (file in files) { ensureActive() // Check for cancellation readFile(file) } }
Alle Suspend-Funktionen von kotlinx.coroutines wie withContext und delay sind abbrechbar. Wenn Ihre Coroutine sie aufruft, sind keine weiteren Schritte erforderlich.
Weitere Informationen zum Abbruch in Coroutinen finden Sie im Blogpost Abbruch in Coroutinen.
Auf Ausnahmen achten
Nicht behandelte Ausnahmen, die in Coroutinen ausgelöst werden, können zum Absturz Ihrer App führen. Wenn Ausnahmen wahrscheinlich sind, fangen Sie sie im Hauptteil aller Coroutinen ab, die mit viewModelScope oder lifecycleScope erstellt wurden.
class LoginViewModel( private val loginRepository: LoginRepository ) : ViewModel() { fun login(username: String, token: String) { viewModelScope.launch { try { loginRepository.login(username, token) // Update UI, user logged in successfully } catch (exception: IOException) { // Update UI, login attempt failed } } } }
Weitere Informationen finden Sie im Blogpost Ausnahmen in Coroutinen, oder unter Coroutinen-Ausnahmen in der Kotlin-Dokumentation.
Weitere Informationen zu Coroutinen
Weitere Ressourcen zu Coroutinen finden Sie auf der Seite Zusätzliche Ressourcen für Kotlin-Coroutinen und -Flows.