Bu sayfada, eş yordamlar kullanırken uygulamanızı daha ölçeklenebilir ve test edilebilir hale getirerek olumlu etki yaratan çeşitli en iyi uygulamalar sunulmaktadır.
Sevk Görevlileri Ekle
Yeni eş yordamlar oluştururken veya withContext
yöntemini çağırırken Dispatchers
kodunu gömmeyin.
// 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) { /* ... */ }
}
Bu bağımlılık ekleme kalıbı, testlerinizi daha belirleyici hale getirmek için birim ve araç testlerindeki dağıtımcıları bir test görev dağıtıcısı ile değiştirebileceğiniz için testi kolaylaştırır.
Askıya alma işlevleri, ana iş parçacığından güvenli bir şekilde çağrılmalıdır
Askıya alma işlevleri, "main güvenli" olmalıdır, yani ana iş parçacığından güvenle çağrılabilmelidir. Bir sınıf, bir eş yordamda uzun süreli engelleme işlemleri yapıyorsa yürütmeyi withContext
kullanarak ana iş parçacığının dışına taşımaktan sorumludur. Bu, sınıfın hangi mimarinin içinde bulunduğuna bakılmaksızın uygulamanızdaki tüm sınıflar için geçerlidir.
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)
}
}
Askıya alma işlevlerini çağıran sınıflar, Dispatcher
uygulamasının hangi iş türü için ne kullanılacağı konusunda endişe duymasına gerek olmadığı için bu kalıp uygulamanızı daha ölçeklenebilir hale getirir. Bu sorumluluk işi yapan sınıftadır.
ViewModel, eş yordamlar oluşturmalıdır
ViewModel
sınıfları, iş mantığını gerçekleştirmek için askıya alma işlevlerini açığa çıkarmak yerine eş yordamlar oluşturmayı tercih etmelidir. Veri akışı kullanarak durumu göstermek yerine yalnızca tek bir değerin yayınlanması gerekiyorsa ViewModel
içindeki askıya alma işlevleri yararlı olabilir.
// 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()
}
Görünümler, iş mantığını gerçekleştirmek için herhangi bir eş yordamı doğrudan tetiklememelidir.
Bunun yerine, bu sorumluluğu ViewModel
'a erteleyin. ViewModel
nesneleri, görünümleri test etmek için gerekli araç testlerini kullanmak yerine birim test edilebildiğinden iş mantığınızın test edilmesini kolaylaştırırsınız.
Buna ek olarak, iş viewModelScope
içinde başlatılırsa eş değerleriniz yapılandırma değişikliklerinden otomatik olarak durumunda kalır. Bunun yerine lifecycleScope
kullanarak eş yordamlar oluşturursanız bu işlemi manuel olarak gerçekleştirmeniz gerekir.
Eş yordamın ViewModel
kapsamının dışına çıkması gerekiyorsa İşletme ve veri katmanında eş yordam oluşturma konusuna göz atın.
Değişken türleri gösterme
Sabit türlerin diğer sınıflara gösterilmesini tercih ederim. Böylece değişken türde yapılan tüm değişiklikler tek bir sınıfta toplanır. Böylece bir şeyler ters gittiğinde hata ayıklama işlemi kolaylaşır.
// 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)
/* ... */
}
Veri ve iş katmanı, askıya alma işlevlerini ve akışları göstermelidir
Veri ve iş katmanlarındaki sınıflar genellikle tek seferlik aramalar yapmak veya zaman içindeki veri değişikliklerinden haberdar olmak için işlevleri açığa çıkarır. Bu katmanlardaki sınıflar, tek seferlik çağrılar için askıya alma işlevleri ve Veri değişiklikleri hakkında bildirim almak için akış seçeneklerini sağlamalıdır.
// Classes in the data and business layer expose
// either suspend functions or Flows
class ExampleRepository {
suspend fun makeNetworkRequest() { /* ... */ }
fun getExamples(): Flow<Example> { /* ... */ }
}
Bu en iyi uygulama, çağıran kişinin (genellikle sunum katmanı), bu katmanlarda gerçekleşen işin yürütülmesini ve yaşam döngüsünü kontrol edebilmesini ve gerektiğinde işlemi iptal edebilmesini sağlar.
İş ve veri katmanında eş yordamlar oluşturma
Veri veya iş katmanında bulunan ve farklı nedenlerle eş yordamlar oluşturması gereken sınıflar için farklı seçenekler vardır.
Bu eş yordamlarda yapılacak iş, yalnızca kullanıcı mevcut ekranda olduğunda alakalıysa çağrıyı yapanın yaşam döngüsünü takip etmelidir. Çoğu durumda, arayan ViewModel olur ve kullanıcı ekrandan ayrıldığında ve ViewModel temizlendiğinde arama iptal edilir. Bu durumda coroutineScope
veya supervisorScope
kullanılmalıdır.
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())
}
}
}
Uygulama açık olduğu sürece yapılacak iş ilgiliyse ve iş belirli bir ekrana bağlı
olmadığı sürece söz konusu iş, arayanın yaşam döngüsünden uzun ömürlü olmalıdır. Bu senaryoda, İptal edilmemesi gereken işler için Kurallar ve Kalıplar bölümünde açıklandığı gibi harici bir CoroutineScope
kullanılmalıdır.
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
, mevcut ekrandan daha uzun süren bir sınıf tarafından oluşturulmalı ve yönetilmelidir. Bu sınıf, Application
sınıfı veya kapsamı bir gezinme grafiğine ayarlanmış bir ViewModel
tarafından yönetilebilir.
TestDispatcher'ları testlere ekleme
Testlerde sınıflarınıza bir TestDispatcher
örneği eklenmelidir. kotlinx-coroutines-test
kitaplığında kullanılabilir iki uygulama vardır:
StandardTestDispatcher
: Burada başlatılan eş yordamlarını bir planlayıcı kullanarak sıraya koyar ve test iş parçacığı meşgul olmadığında yürütür. Sıraya alınmış diğer ortak programlarınadvanceUntilIdle
gibi yöntemlerle çalıştırılmasına izin vermek için test iş parçacığını askıya alabilirsiniz.UnconfinedTestDispatcher
: Yeni eş yordamlarını istekli bir şekilde, engelleyici bir şekilde çalıştırır. Bu yöntem genellikle testlerin yazılmasını kolaylaştırır, ancak test sırasında eş yordamların nasıl yürütüleceği üzerinde daha az kontrole sahip olmanızı sağlar.
Ayrıntılı bilgi için her bir görev dağıtıcının uygulanmasına ilişkin dokümanlara bakın.
Eş yordamları test etmek için runTest
eş yordam oluşturucuyu kullanın. runTest
, testlerdeki gecikmeleri atlamak ve sanal zamanı kontrol etmenizi sağlamak için bir TestCoroutineScheduler
kullanır. Gerektiğinde ek test görev dağıtıcıları oluşturmak için bu planlayıcıyı da kullanabilirsiniz.
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()
}
}
Tüm TestDispatchers
aynı planlayıcıyı paylaşmalıdır. Bu sayede testlerinizi belirleyici hale getirmek için tüm eş yordam kodunuzu tek test iş parçacığında çalıştırabilirsiniz. runTest
, geri dönmeden önce aynı planlayıcıdaki veya test eş yordasının alt öğeleri olan tüm eş yordamlarının tamamlanmasını bekler.
GlobalScope'tan Kaçınma
Bu, Görev dağıtıcıları ekleme en iyi uygulamasına benzer. GlobalScope
sayesinde, bir sınıfın kullandığı CoroutineScope
kodunu ekleyerek bazı dezavantajlar belirlemiş olursunuz:
Sabit kodlama değerlerini teşvik eder.
GlobalScope
kodunu sabitlersenizDispatchers
kodunu da sabitliyor olabilirsiniz.Kodunuz kontrolsüz bir kapsamda yürütüldüğü için testi çok zor hale getirir ve yürütülmesini kontrol edemezsiniz.
Kapsama dahil edilen tüm eş yordamlar için yürütülecek ortak bir
CoroutineContext
sahibi olamaz.
Bunun yerine, mevcut kapsamı aşması gereken işler için bir CoroutineScope
yerleştirmeyi düşünün. Bu konu hakkında daha fazla bilgi edinmek için İş ve veri katmanı bölümünde eş yordam oluşturma bölümüne göz atın.
// 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
}
}
GlobalScope
ve alternatifleri hakkında daha fazla bilgi edinmek için İptal edilmemesi gereken çalışmalara ilişkin koşullar ve kalıplar bölümüne bakın.
Ortak yordanızı iptal edilebilir hale getirin
Eş yordalarda iptal işlemi işbirliğine tabidir. Diğer bir deyişle, bir eş yordasının Job
iptal edildiğinde, eş yordam askıya alınana veya iptal için kontrol edilene kadar iptal edilmez. Bir eş yordasında engelleme işlemleri yapıyorsanız eş yordanın iptal edilebilir olduğundan emin olun.
Örneğin, diskten birden fazla dosya okuyorsanız her dosyayı okumaya başlamadan önce eş yordamın iptal edilip edilmediğini kontrol edin. İptal olup olmadığını kontrol etmenin bir yolu, ensureActive
işlevini çağırmaktır.
someScope.launch {
for(file in files) {
ensureActive() // Check for cancellation
readFile(file)
}
}
kotlinx.coroutines
kapsamındaki tüm askıya alma işlevleri (ör. withContext
ve delay
) iptal edilebilir. Eş yordanız bunları çağırırsa
başka herhangi bir işlem yapmanız gerekmez.
Eş yordamlarda iptal hakkında daha fazla bilgi edinmek için Eşliklerde iptaller blog yayınına göz atın.
İstisnalara dikkat edin
Eş yordamlara eklenen işlenmemiş istisnalar uygulamanızın kilitlenmesine neden olabilir. İstisnaların olması olasıysa bunları viewModelScope
veya lifecycleScope
ile oluşturulan eş yordamların gövdesinde yakalayın.
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
}
}
}
}
Daha fazla bilgi için Kotlin dokümanlarındaki istisnalar veya Kotlin belgelerindeki Coroutine istisnaları işleme konulu blog yayınına göz atın.
Eş yordamlar hakkında daha fazla bilgi edinin
Daha fazla eş yordam kaynağı için Kotlin eş yordamları ve akış için ek kaynaklar sayfasına bakın.