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 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. advanceUntilIdle gibi 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.

taşıma rehberini inceleyin.

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. 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ü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.