Bu sayfada, eş yordamlar kullanılırken uygulamanızı daha ölçeklenebilir ve test edilebilir hale getirerek olumlu etki yaratan çeşitli en iyi uygulamalar sunulmaktadır.
Inject Dispatchers
Yeni eş yordamlar oluştururken veya withContext çağırırken Dispatchers değerini sabit kodlamayın.
// 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ı, birim ve enstrümantasyon testlerindeki bu dağıtıcıları test dağıtıcısı ile değiştirerek testleri daha belirleyici hale getirebileceğiniz için test etmeyi kolaylaştırır.
Askıya alma işlevleri, ana iş parçacığından güvenli bir şekilde çağrılabilir.
Askıya alma işlevleri ana iş parçacığı açısından güvenli olmalıdır. Yani ana iş parçacığından çağrılmaları güvenlidir. Bir sınıf, eş yordamda uzun süren engelleme işlemleri yapıyorsa withContext kullanarak yürütmeyi ana iş parçacığından taşımakla sorumludur. Bu durum, uygulamanızdaki tüm sınıflar için geçerlidir. Sınıfın mimarinin hangi bölümünde olduğu önemli değildir.
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) } }
Bu kalıp, askıya alma işlevlerini çağıran sınıfların hangi iş türü için hangi Dispatcher öğesini kullanacaklarını düşünmelerine gerek kalmadığından uygulamanızın daha ölçeklenebilir olmasını sağlar. Bu sorumluluk, işi yapan sınıfa aittir.
ViewModel, coroutine oluşturmalıdır.
ViewModel sınıfları, iş mantığını yürütmek için askıya alma işlevlerini kullanmak yerine eş yordam oluşturmayı tercih etmelidir. ViewModel içindeki askıya alma işlevleri, durumu bir veri akışı kullanarak göstermek yerine yalnızca tek bir değerin yayınlanması gerektiğinde 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ı yürütmek için doğrudan herhangi bir eşzamanlı işlevi tetiklememelidir.
Bunun yerine, bu sorumluluğu ViewModel'ya devredin. Bu sayede, görünümleri test etmek için gereken enstrümantasyon testlerini kullanmak yerine ViewModel nesneleri birim testine tabi tutulabildiğinden işletme mantığınızın test edilmesi kolaylaşır.
Ayrıca, iş viewModelScope içinde başlatılırsa eş yordamlarınız yapılandırma değişikliklerinden otomatik olarak etkilenmez. Bunun yerine lifecycleScope kullanarak eşzamanlı işlevler oluşturursanız bunu manuel olarak yapmanız gerekir.
Coroutine'in ViewModel kapsamından daha uzun süre çalışması gerekiyorsa İşletme ve veri katmanında coroutine oluşturma bölümüne göz atın.
Değiştirilebilir türleri kullanıma sunmayın
Değiştirilemez türlerin diğer sınıflara gösterilmesini tercih edin. Bu şekilde, değiştirilebilir türdeki tüm değişiklikler tek bir sınıfta toplanır. Bu da bir sorun olduğunda hata ayıklamayı kolaylaştırı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şletme katmanı, askıya alma işlevlerini ve akışlarını kullanıma sunmalıdır.
Veri ve işletme katmanlarındaki sınıflar genellikle tek seferlik çağrıları gerçekleştirmek veya zaman içindeki veri değişikliklerinden haberdar olmak için işlevler sunar. Bu katmanlardaki sınıflar, tek seferlik çağrılar için askıya alma işlevlerini ve veri değişiklikleri hakkında bildirim göndermek için Flow'u kullanmalı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, genellikle sunum katmanı olan arayanın, bu katmanlarda gerçekleşen işin yürütülmesini ve yaşam döngüsünü kontrol etmesini ve gerektiğinde iptal etmesini sağlar.
İş ve veri katmanında eş yordam oluşturma
Veri veya işletme katmanındaki sınıflar için farklı nedenlerle eş yordam oluşturmak gerektiğinde farklı seçenekler kullanılabilir.
Bu eş yordamlarda yapılacak iş yalnızca kullanıcı mevcut ekrandayken alakalıysa arayanın yaşam döngüsünü takip etmelidir. Çoğu durumda, arayan ViewModel olur ve kullanıcı ekrandan ayrılıp 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()) } } }
Yapılacak iş, uygulama açık olduğu sürece geçerliyse ve belirli bir ekrana bağlı değilse iş, arayanın yaşam döngüsünden daha uzun sürmelidir. Bu senaryoda, Coroutines & Patterns for work that shouldn’t be cancelled blog yayınında 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üre yaşayan bir sınıf tarafından oluşturulup yönetilmelidir. Bu sınıf, Application sınıfı veya bir gezinme grafiği kapsamına alınmış bir ViewModel olabilir.
Testlere TestDispatcher'ları ekleme
Testlerde sınıflarınıza TestDispatcher örneği eklenmelidir. kotlinx-coroutines-test kitaplığında iki uygulama mevcuttur:
StandardTestDispatcher: Üzerinde başlatılan eş yordamları bir planlayıcıyla sıraya alır ve test iş parçacığı meşgul olmadığında bunları yürütür.advanceUntilIdlegibi yöntemleri kullanarak diğer sıralanmış eş yordamların çalışmasına izin vermek için test iş parçacığını askıya alabilirsiniz.UnconfinedTestDispatcher: Yeni eşzamanlı rutinleri engelleme şeklinde hemen çalıştırır. Bu genellikle test yazmayı kolaylaştırır ancak test sırasında eş yordamların nasıl yürütüldüğü üzerinde daha az kontrol sahibi olursunuz.
Daha fazla bilgi için her dağıtıcı uygulamasının belgelerine bakın.
Coroutine'leri test etmek için
runTest
Coroutine oluşturucuyu kullanın. runTest, testlerdeki gecikmeleri atlamak ve sanal zamanı kontrol etmenize olanak tanımak için TestCoroutineScheduler kullanır. Gerekirse ek test dağıtıcıları oluşturmak için de bu planlayıcıyı 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, defaultDispatcher = 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 deterministik hale getirmek için tüm coroutine kodunuzu tek test iş parçacığında çalıştırabilirsiniz. runTest, aynı planlayıcıda olan veya test eş yordamının alt öğeleri olan tüm eş yordamların tamamlanmasını bekledikten sonra geri döner.
GlobalScope'tan Kaçının
Bu, Inject Dispatchers (Göndericileri Ekle) en iyi uygulamasına benzer. GlobalScope kullanarak bir sınıfın kullandığı CoroutineScope değerini sabit kodluyorsunuz. Bu durum bazı dezavantajlara yol açar:
Değerlerin gömülü kodlanmasını teşvik eder.
GlobalScopedeğerini sabit kodlarsanızDispatchersdeğerini de sabit kodlamış olabilirsiniz.Kodunuz kontrolsüz bir kapsamda yürütüldüğünden test yapmak çok zorlaşır ve yürütmeyi kontrol edemezsiniz.
Kapsamın kendisinde yer alan tüm eş yordamlar için ortak bir
CoroutineContextçalıştıramazsınız.
Bunun yerine, mevcut kapsamdan daha uzun süre geçerli olması gereken çalışmalar için CoroutineScope eklemeyi düşünebilirsiniz. Bu konu hakkında daha fazla bilgi edinmek için İş ve veri katmanında eş yordam oluşturma bölümünü inceleyin.
// 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 bilgiyi Coroutines & Patterns for work that shouldn’t be cancelled blog yayınında bulabilirsiniz.
Eş yordamınızı iptal edilebilir hale getirme
Coroutine'larda iptal işlemi işbirlikçi bir şekilde yapılır. Bu nedenle, bir coroutine'un Job iptal edildiğinde coroutine askıya alınana veya iptal kontrolü yapılana kadar iptal edilmez. Bir eş yordamda engelleme işlemleri yapıyorsanız eş yordamı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 durumunu kontrol etmenin bir yolu ensureActive işlevini çağırmaktır.
someScope.launch { for (file in files) { ensureActive() // Check for cancellation readFile(file) } }
kotlinx.coroutines'daki withContext ve delay gibi tüm askıya alma işlevleri iptal edilebilir. Eş yordamınız bunları çağırıyorsa herhangi bir ek işlem yapmanız gerekmez.
Coroutine'lerde iptal hakkında daha fazla bilgi için Coroutine'lerde iptal blog yayınını inceleyin.
İstisnalara dikkat edin
Coroutine'lerde işlenmeyen istisnalar, uygulamanızın kilitlenmesine neden olabilir. İstisnaların oluşması muhtemel ise viewModelScope veya lifecycleScope ile oluşturulan tüm eş yordamların gövdesinde bunları yakalayın.
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 } } } }
Daha fazla bilgi için Exceptions in coroutines (Coroutine'lerde istisnalar) başlıklı blog yayınına veya Kotlin belgelerindeki Coroutine exceptions handling (Coroutine istisna işleme) bölümüne göz atın.
Coroutine'ler hakkında daha fazla bilgi
Daha fazla coroutine kaynağı için Kotlin belgelerindeki Coroutines rehberine bakın.