Android'de eş yordamlar için en iyi uygulamalar

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 coroutine'ler 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 göndericileri test göndericisiyle değiştirerek testleri daha belirleyici hale getirebileceğiniz için test etmeyi kolaylaştırır.

Askıya alma işlevleri, ana ileti dizisinden 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 güvenle çağrılabilirler. 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şırmakla sorumludur. Bu, 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'yı kullanacaklarını düşünmelerine gerek kalmadığından uygulamanızı daha ölçeklenebilir hale getirir. 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şzamanlı rutinler 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ı yordamı 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şlemler 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 İş 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 merkezileştirilir. 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ıflarda farklı nedenlerle eşzamanlı rutinler oluşturulması 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, arayanın yaşam döngüsünden daha uzun sürmelidir. Bu senaryoda, Coroutines & Patterns for work that shouldn’t be cancelled blog gönderisinde 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 yerleştirilmelidir. 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. Diğer sıralanmış eş yordamların advanceUntilIdle gibi yöntemler kullanılarak ç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üleceği konusunda daha az kontrol sağlar.

Ek ayrıntılar için her bir dağıtıcı uygulamasının dokümanlarına bakın.

Coroutine'leri test etmek için runTest coroutine oluşturucusunu 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 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,
            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 bulunan veya test eşzamanlı yordamının alt öğeleri olan tüm eşzamanlı yordamların tamamlanmasını bekledikten sonra döndürülür.

GlobalScope'tan Kaçının

Bu, Inject Dispatchers en iyi uygulamasına benzer. GlobalScope kullanarak bir sınıfın kullandığı CoroutineScope değerini sabit kodluyorsunuz. Bu durumun bazı dezavantajları vardır:

  • Değerlerin sabit kodlanmasını teşvik eder. GlobalScope değerini sabit kodlarsanız Dispatchers değerini de sabit kodlamış olabilirsiniz.

  • Kodunuz kontrolsüz bir kapsamda yürütüldüğünden test yapmak çok zorlaşır ve yürütülmesini kontrol edemezsiniz.

  • Kapsamın kendisine yerleştirilmiş tüm eş yordamlar için yürütülecek ortak bir CoroutineContext olamaz.

Bunun yerine, mevcut kapsamdan daha uzun süre devam etmesi gereken işler için CoroutineScope eklemeyi düşünebilirsiniz. Bu konu hakkında daha fazla bilgi edinmek için İşletme ve veri katmanında 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 bilgiyi Coroutines & Patterns for work that shouldn’t be cancelled (İptal edilmemesi gereken işler için eş yordamlar ve kalıplar) başlıklı blog yayınında bulabilirsiniz.

Coroutine'inizi iptal edilebilir hale getirme

Coroutine'larda iptal işlemi işbirlikçi bir şekilde yapılır. Bu nedenle, bir coroutine'ın Job iptal edildiğinde, coroutine askıya alınana veya iptal kontrolü yapılana kadar iptal edilmez. Bir coroutine'de engelleme işlemleri yapıyorsanız coroutine'in 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 tarihinden itibaren withContext ve delay gibi tüm askıya alma işlevleri iptal edilebilir. Coroutine'iniz 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'larda oluşturulan işlenmemiş istisnalar, uygulamanızın kilitlenmesine neden olabilir. İstisnaların oluşması muhtemel ise bunları viewModelScope veya lifecycleScope ile oluşturulan tüm 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)
                // 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ını veya Kotlin belgelerindeki Coroutine exceptions handling (Coroutine istisna işleme) bölümünü inceleyin.

Coroutine'ler hakkında daha fazla bilgi

Daha fazla eşzamanlı iş kaynağı için Kotlin eşzamanlı işleri ve akışı için ek kaynaklar sayfasını inceleyin.