Best Practices für Koroutinen in Android

Auf dieser Seite werden mehrere Best Practices vorgestellt, die sich positiv auswirken, indem sie Ihre App bei Verwendung von Koroutinen skalierbarer und testbarer machen.

Dispatcher

Keine Hartcodierung von Dispatchers beim Erstellen neuer Koroutinen oder beim Aufrufen von withContext.

// 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 Abhängigkeitsinjektionsmuster vereinfacht das Testen, da Sie diese in Einheiten- und Instrumentierungstests mit einem Test-Disponent um die Tests deterministischer zu machen.

Aussetzen von Funktionen sollte sicher aus dem Hauptthread aufgerufen werden können.

Sperren-Funktionen sollten hauptsicher sein, d. h., sie können sicher aus dem im Hauptthread. Wenn eine Klasse lange blockierende Vorgänge in einem coroutine wird die Ausführung mithilfe von withContext Dies gilt für alle Kurse in Ihrer App, unabhängig davon, Architektur der Klasse zu verstehen.

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(): List<ArticleWithAuthor> {
        val news = newsRepository.fetchLatestNews()

        val response: List<ArticleWithAuthor> = mutableEmptyList()
        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 Klassen, die Sperrfunktionen aufrufen, Sie brauchen sich keine Gedanken darüber zu machen, welches Dispatcher Sie für welche Art von Arbeit verwenden. Dieses Verantwortung liegt in der Klasse, die die Arbeit erledigt.

ViewModel sollte Koroutinen erstellen

ViewModel Klassen sollten bevorzugt werden Erstellen von Koroutinen anstelle von Sperrfunktionen zur Ausführung von Geschäftsabläufen Logik. Aussetzen von Funktionen im ViewModel kann nützlich sein, wenn anstelle von Datenstrom verfügbar gemacht wird, muss nur ein einziger Wert ausgegeben werden.

// 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 Koroutinen direkt auslösen, um Geschäftslogik auszuführen. Übertrage die Verantwortung stattdessen dem ViewModel. Das macht Ihr Unternehmen Die Logik ist einfacher zu testen, da ViewModel-Objekte in einem Einheitentest ausgeführt werden können, anstatt Instrumentierungstests, die zum Testen von Aufrufen erforderlich sind.

Außerdem bleiben Ihre Koroutinen unverändert über Konfigurationsänderungen erhalten automatisch, wenn die Arbeit in der viewModelScope begonnen wird. Wenn Sie Koroutinen mit lifecycleScope erstellen, müssten Sie dies manuell erledigen. Wenn die Koroutine den Bereich von ViewModel überdauern muss, sieh dir die Koroutinen im Abschnitt „Geschäfts- und Datenschicht“ erstellen.

Keine änderbaren Typen verfügbar machen

Bevorzugen Sie die Bereitstellung unveränderlicher Typen für andere Klassen. Auf diese Weise ändern sich alle Der änderbare Typ ist in einer Klasse zentralisiert, was die Fehlerbehebung beim 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äftsebene sollte Sperrungsfunktionen und Abläufe anzeigen.

Klassen der Daten- und Geschäftsebene bieten in der Regel Funktionen, die One-Shot-Anrufe oder um über Datenänderungen im Laufe der Zeit informiert zu werden. Klassen in diesen Layers sollten Sperren von Funktionen für One-Shot-Aufrufe und Flow to Benachrichtigungen über Datenänderungen erhalten.

// Classes in the data and business layer expose
// either suspend functions or Flows
class ExampleRepository {
    suspend fun makeNetworkRequest() { /* ... */ }

    fun getExamples(): Flow<Example> { /* ... */ }
}

Durch diese Best Practice kann der Aufrufer, in der Regel die Präsentationsebene, die Ausführung und den Lebenszyklus der Arbeit in diesen Schichten jederzeit kündigen.

Koroutinen in der Geschäfts- und der Datenschicht erstellen

Für Klassen in der Daten- oder Geschäftsebene, die Koroutinen für verschiedene Gründe, es gibt unterschiedliche Optionen.

Wenn die in diesen Abläufen zu erledigende Arbeit nur relevant ist, wenn die Nutzenden angezeigt wird, sollte sie dem Lebenszyklus des Aufrufers folgen. In den meisten ist der Aufrufer das ViewModel. Der Aufruf wird abgebrochen, wenn die verlässt der Nutzer den Bildschirm, und ViewModel wird gelöscht. In diesem Fall coroutineScope oder supervisorScope verwendet werden sollte.

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 zu erledigende Arbeit relevant ist, solange die App geöffnet wird und die Arbeit nicht an einen bestimmten Bildschirm gebunden sind, dann sollte die Arbeit Lebenszyklus. In diesem Szenario sollte eine externe CoroutineScope verwendet werden als im Abschnitt Koroutinen und Muster für Aufgaben, die nicht gekündigt werden sollten.

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 einem Kurs erstellt und verwaltet werden, der länger als Bildschirm hat, könnte er von der Application-Klasse oder einem ViewModel, die einem Navigationsdiagramm zugeordnet ist.

TestDispatcher in Tests einschleusen

Eine Instanz von TestDispatcher in Tests in Ihre Klassen eingeschleust werden. Es gibt zwei Implementierungen im kotlinx-coroutines-test-Bibliothek:

  • StandardTestDispatcher: Stellt Koroutinen, die darauf mit einem Planer gestartet wurden, in die Warteschlange und führt Folgendes aus: wenn der Testthread nicht ausgelastet ist. Sie können den Testthread aussetzen, andere Koroutinen in der Warteschlange mit Methoden wie advanceUntilIdle

  • UnconfinedTestDispatcher: Führt neue Koroutinen eifrig und blockierend aus. Dadurch wird im Allgemeinen einfacher zu testen, gibt Ihnen aber weniger Kontrolle über gemeinsame Routinen. die während des Tests ausgeführt werden.

Weitere Informationen finden Sie in der Dokumentation der einzelnen Disponentenimplementierungen.

Verwenden Sie zum Testen von Koroutinen die Methode runTest Coroutine-Builder. runTest verwendet eine TestCoroutineScheduler Verzögerungen bei Tests zu vermeiden und die virtuelle Zeit zu kontrollieren. Sie können auch Verwenden Sie diesen Planer, 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,
            testDispatcher
        )
        val article = Article()
        repository.bookmarkArticle(article)
        assertThat(articlesDataSource.isBookmarked(article)).isTrue()
    }
}

Alle TestDispatchers sollten denselben Planer verwenden. So können Sie Führen Sie Ihren gesamten Koroutinecode im einzelnen Testthread aus, um Ihre Tests durchzuführen. deterministisch. runTest wartet auf alle Koroutinen, die zur selben Zeit gehören oder sind untergeordnete Elemente der Testkoroutine, die vor der Rückkehr abgeschlossen werden muss.

GlobalScope vermeiden

Dies entspricht der Best Practice für Disponenten einschleusen. Durch die Verwendung von GlobalScope, Sie hartcodieren die CoroutineScope, die eine Klasse verwendet, was einige Nachteile mit sich bringt. damit:

  • Unterstützt hartcodierte Werte. Wenn Sie GlobalScope hartcodieren, sind Sie möglicherweise Dispatchers ebenfalls hartcodiert.

  • Das Testen ist sehr erschwert, da der Code in einem unkontrollierten Bereich ausgeführt wird. können Sie die Ausführung nicht steuern.

  • Eine gemeinsame CoroutineContext kann nicht für alle Koroutinen ausgeführt werden in den Umfang selbst integriert.

Erwägen Sie stattdessen, einen CoroutineScope für Arbeit einzufügen, die noch bestehen muss. den aktuellen Umfang. In der Koroutinen im Abschnitt „Geschäfts- und Datenschicht“ erstellen um mehr über dieses Thema zu erfahren.

// 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 den Alternativen finden Sie in der Coroutinen und Muster für Aufgaben, die nicht gekündigt werden sollten.

Koroutine deaktivieren

Die Stornierung in Koroutinen ist kooperativ, das heißt, wenn die Job wurde abgebrochen. Die Koroutine wird erst abgebrochen, wenn sie unterbrochen oder geprüft wird zur Stornierung. Wenn Sie blockierende Vorgänge in einer Koroutine ausführen, achten Sie darauf, Die Koroutine ist stornierbar.

Wenn Sie beispielsweise mehrere Dateien von der Festplatte lesen, Prüfen Sie, ob die Koroutine abgebrochen wurde. Eine Möglichkeit, um auf Stornierung prüfen möchten, besteht darin, ensureActive .

someScope.launch {
    for(file in files) {
        ensureActive() // Check for cancellation
        readFile(file)
    }
}

Alle Sperrfunktionen von kotlinx.coroutines wie withContext und delay können gekündigt werden. Wenn Ihre gemeinsame Routine sie anruft, sollten Sie nichts tun müssen. zusätzliche Arbeit.

Weitere Informationen zur Kündigung von gemeinsamen Abläufen findest du in der Blogpost zur Kündigung von gemeinsamen Routinen

Achten Sie auf Ausnahmen

Nicht behandelte Ausnahmen, die in Koroutinen ausgelöst werden, können Ihre App zum Absturz bringen. Falls-Ausnahmen wahrscheinlich ist, können Sie sie im Text jeder Koroutinen erfassen, die mit viewModelScope oder lifecycleScope.

class LoginViewModel(
    private val loginRepository: LoginRepository
) : ViewModel() {

    fun login(username: String, token: String) {
        viewModelScope.launch {
            try {
                loginRepository.login(username, token)
                // Notify view user logged in successfully
            } catch (exception: IOException) {
                // Notify view login attempt failed
            }
        }
    }
}

Weitere Informationen finden Sie im Blogpost. Ausnahmen bei Koroutinen, oder Behandlung von Coroutine-Ausnahmen in der Kotlin-Dokumentation.

Weitere Informationen zu Koroutinen

Weitere Ressourcen für Koroutinen finden Sie in der Zusätzliche Ressourcen für Kotlin-Koroutinen und -Ablauf Seite.