Sprawdzone metody dotyczące współprogramów na Androidzie

Na tej stronie przedstawiamy kilka sprawdzonych metod, które mogą przynieść pozytywne efekty przez bardziej skalowalna i testowalna przy korzystaniu z współrzędnych.

Dyspozytory do strzyżenia

Nie koduj na stałe elementu Dispatchers podczas tworzenia nowych współprogramów ani wywołań 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) { /* ... */ }
}

Ten wzorzec wstrzykiwania zależności ułatwia testowanie, ponieważ możesz je zastąpić w testach jednostkowych i instrumentacyjnych dla dyspozytorów, dyspozytor testowy aby testy były bardziej deterministyczne.

Wywołanie funkcji zawieszania z wątku głównego powinno być bezpieczne

Funkcje zawieszania powinny być bezpieczne, co oznacza, że można je bezpiecznie wywoływać z w wątku głównym. Jeśli klasa wykonuje długotrwałe operacje blokujące w odpowiada za przeniesienie wykonania z wątku głównego za pomocą withContext Dotyczy to wszystkich zajęć w aplikacji (niezależnie od ich części) na architekturę, w której pracuje klasa.

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

Ten wzorzec zwiększa skalowalność aplikacji, ponieważ klasy wywołują funkcje zawieszania nie musisz się martwić o to, którego Dispatcher używać do różnych zadań. Ten odpowiedzialność za to, co robisz, spoczywa na klasie, która je wykonuje.

Model widoku danych powinien utworzyć współrzędne

Preferowane zajęcia: ViewModel na tworzeniu współrzędnych zamiast narażania funkcji zawieszania na potrzeby prowadzenia działalności logikę logiczną. Zawieszanie funkcji w ViewModel może być przydatne, jeśli zamiast dla ekspozycji za pomocą strumienia danych, wystarczy emitować tylko jedną wartość.

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

Widoki nie powinny bezpośrednio wyzwalać żadnych współprogramów, aby realizować logikę biznesową. Zamiast tego przełóż tę odpowiedzialność na ViewModel. Dzięki temu Twoja firma łatwiejsze do przetestowania, ponieważ obiekty ViewModel można testować jednostkowe, zamiast z testami instrumentacji, które są wymagane do testowania wyświetleń.

Poza tym współrzędne przetrwają zmiany konfiguracji automatycznie, jeśli rozpocznie się w viewModelScope. Jeśli utworzysz z współrzędnymi za pomocą funkcji lifecycleScope, musisz robić to ręcznie. Jeśli współrzędna musi wyjść poza zakres obiektu ViewModel, zapoznaj się z Tworzenie współrzędnych w sekcji warstwy biznesowej i danych

Nie ujawniaj zmiennych typów

Preferuje ujawnianie typów stałych innym klasom. W ten sposób wszystkie zmiany typ zmienny jest scentralizowany w jednej klasie, co ułatwia debugowanie, gdy coś poszło nie tak.

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

    /* ... */
}

Warstwa danych i biznesowa powinna udostępniać funkcje zawieszania i przepływy

Klasy w warstwie danych i warstwach biznesowych zwykle narażają funkcje na wykonanie oraz otrzymywać powiadomienia o zmianach danych w czasie. Zajęcia w tych zajęciach warstwy powinny ujawniać funkcje zawieszania dla wywołań jednorazowych i Flow to powiadamiać o zmianach danych.

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

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

Dzięki tej sprawdzonej metodzie rozmówca, zwykle warstwa prezentacji, kontrolować wykonywanie i cykl życia pracy wykonywanej w tych warstwach oraz możesz anulować.

Tworzenie współrzędnych w warstwie biznesowej i danych

Na potrzeby klas w warstwie danych lub biznesowych, które muszą utworzyć współrzędne dla są różne powody, są różne opcje.

Jeśli praca do wykonania w tych współudziałach ma znaczenie tylko wtedy, gdy użytkownik na bieżącym ekranie, powinien być zgodny z cyklem życia elementu wywołującego. Najwięcej wywołaniem będzie model ViewModel, a połączenie zostanie anulowane, gdy użytkownik opuszcza ekran, a model ViewModel zostaje wyczyszczony. W tym przypadku coroutineScope lub supervisorScope należy użyć funkcji.

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

czy zadanie do wykonania jest odpowiednie, dopóki aplikacja jest otwarta i czy jest nie są powiązane z żadnym ekranem, wówczas praca powinna być cyklu życia usługi. W tym scenariuszu należy użyć zewnętrznego tagu CoroutineScope omówiono w artykule Korutyny Wzorce pracy, których nie należy anulować w poście na blogu

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

Zajęcia externalScope powinny zostać utworzone i zarządzane przez zajęcia, które istnieją dłużej niż bieżący ekran, może nim zarządzać klasa Application lub Pole ViewModel jest ograniczone do wykresu nawigacyjnego.

Wstrzykiwanie symulatorów testów w testach

Instancja TestDispatcher należy stosować w ramach testów. Dostępne są 2 opcje. na stronach docelowych Biblioteka kotlinx-coroutines-test:

  • StandardTestDispatcher: Kolejkuje uruchomione w niej współrzędne za pomocą algorytmu szeregowania i wykonania gdy wątek testowy nie jest zajęty. Możesz zawiesić wątek testowy, aby pozwolić na inne współprogramy w kolejce są uruchamiane za pomocą metod takich jak advanceUntilIdle

  • UnconfinedTestDispatcher: Uruchamia nowe współprogramy z zapasem, w sposób blokujący. Zwykle sprawia to, że piszemy testowanie jest łatwiejsze, ale daje mniejszą kontrolę nad współpracą która została wykonana podczas testu.

Więcej informacji znajdziesz w dokumentacji każdego wdrożenia dyspozytora.

Aby przetestować współprace, użyj runTest do kreatora współprogramów. Komponent runTest używa TestCoroutineScheduler. Pomiń opóźnienia w testach i pozwalają kontrolować czas wirtualny. Możesz też w razie potrzeby użyj tego algorytmu szeregowania, aby utworzyć dodatkowych dyspozytorów testowych.

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

Wszystkie TestDispatchers powinny współdzielić ten sam algorytm szeregowania. Dzięki temu możesz: uruchamiać cały kod współrzędny w pojedynczym wątku testowym, aby przeprowadzić testy; deterministyczny. runTest poczeka na wszystkie współprogramy algorytmu szeregowania lub są elementami podrzędnymi reguły testowej, która ma zostać ukończona przed powrociem.

Unikaj obiektu GlobalScope

Jest to podobne do sprawdzonych metod dotyczących dyspozytorów wstrzykiwania. Za pomocą GlobalScope, kodujesz na stałe CoroutineScope, który wykorzystuje klasa, co przynosi pewne wady. :

  • Promuje wartości zakodowane na stałe. Jeśli zakodujesz GlobalScope na stałe, być może kod Dispatchers na stałe.

  • Bardzo utrudnia przeprowadzanie testów, ponieważ kod jest wykonywany w niekontrolowanym zakresie. nie będziesz mieć kontroli nad jego wykonaniem.

  • Nie możesz mieć wspólnej reguły CoroutineContext do wykonania we wszystkich współrzędnych w samym zakresie.

Zamiast tego rozważ wstrzykiwanie typu CoroutineScope w pracy, która nie musi być żywa bieżącego zakresu. Zobacz Tworzenie współrzędnych w sekcji warstwy biznesowej i danych .

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

Więcej informacji o aplikacji GlobalScope i jej alternatywnych rozwiązaniach znajdziesz w Korutyny Wzorce pracy, których nie należy anulować w poście na blogu

Włącz możliwość anulowania procedur

Anulowanie w korutynach działa w ramach współpracy. Oznacza to, że gdy Działanie Job zostało anulowane, współprogram nie zostanie anulowany, dopóki nie zostanie zawieszony lub nie sprawdzi się w celu anulowania. Jeśli blokujesz operacje w współrzędnie, upewnij się, że współprogramu można anulować.

Jeśli na przykład odczytujesz wiele plików z dysku, zanim zaczniesz odczytując każdy plik, sprawdź, czy współrzędna została anulowana. W jedną stronę aby sprawdzić, czy została anulowana, można zadzwonić pod ensureActive .

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

Wszystkie funkcje zawieszania z kotlinx.coroutines, takie jak withContext i Można anulować: delay. Jeśli Twój koder je wywołuje, nie musisz tego robić wykonywać wszelkie dodatkowe czynności.

Więcej informacji o anulowaniu w współrzędnych znajdziesz w Anulowanie w poście na blogu o współrzędnych.

Uwaga na wyjątki

Nieobsłużone wyjątki zgłoszone w współrzędnych mogą spowodować awarię aplikacji. Jeśli wyjątki są bardziej prawdopodobne, wychwytuj je w treści wszystkich współprogramów utworzonych za pomocą viewModelScope lub 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
            }
        }
    }
}

Więcej informacji znajdziesz w poście na blogu Wyjątki w kodach, lub obsługa wyjątków od reguł w dokumentacji Kotlin.

Więcej informacji o współprogramach

Więcej zasobów dotyczących współprogramów znajdziesz w dokumentacji Dodatkowe materiały na temat współrzędnych i przepływu Kotlin stronę.