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

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ın advanceUntilIdle 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 sabitlerseniz Dispatchers 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.