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 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 Twojej 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 w bezpośredni sposób wywoływać żadnych współrzędnych wymaganych do działania logiki biznesowej.
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 jakadvanceUntilIdle
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:
uruchamiaj cały kod współrzędny w pojedynczym wątku testowym, aby przeprowadzać testy;
deterministyczny. runTest
poczeka na wszystkie współprogramy
algorytm szeregowania lub są elementami podrzędnymi kogutyny 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 kodDispatchers
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ę.