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

Bu sayfada, iyi bir örneklemi birçok yönden olumlu yönde etkileyen eş yordamlar kullanırken uygulamanızın daha ölçeklenebilir ve test edilebilir olmasını sağlayın.

Sevk Görevlileri Ekleyin

Yeni eş yordamlar oluştururken veya çağrı yaparken Dispatchers kodunu sabit kod yazmayın. withContext.

// 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 yerleştirme kalıbı, bunları yenileriyle değiştirebileceğiniz için testi kolaylaştırır birim ve enstrümantasyon testlerinde görev dağıtıcı test görev dağıtıcısı testlerinizi daha deterministik hale getirin.

Askıya alma işlevlerinin ana iş parçacığından çağrılması güvenli olmalıdır

Askıya alma işlevleri ana güvenli olmalıdır; diğer bir deyişle, ana ileti dizisidir. Bir sınıf, yürütmenin ana iş parçacığının dışına taşınmasından sorumludur. withContext Bu, hangi bölüme olursa olsun, uygulamanızdaki tüm sınıflar için iç içe geçmiş olabilir.

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)
    }
}

Bu kalıp, askıya alma işlevini çağıran sınıflar nedeniyle uygulamanızı daha ölçeklenebilir hale getirir ve ne tür bir iş için Dispatcher kullanacağınız konusunda endişelenmenize gerek yoktur. Bu işi yapan sınıfa aittir.

ViewModel, eş yordamlar oluşturmalıdır

ViewModel sınıf tercih etmelidir iş gerçekleştirmek için askıya alma işlevlerini açığa çıkarmak yerine eş yordamlar oluşturmak mantığıyla başlayalım. ViewModel içindeki askıya alma işlevleri şunun yerine şu durumlarda yararlı olabilir: tek bir değer kullanılması yeterlidir.

// 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ığı gerçekleştirmek için eş yordamları doğrudan tetiklememelidir. Bunun yerine bu sorumluluğu ViewModel hesabına erteleyin. Böylece, işletmeniz ViewModel nesneleri gerekli araç testleri.

Ayrıca, eş yordamlarınız yapılandırma değişikliklerinden viewModelScope içinde başlarsa otomatik olarak güncellenir. Oluşturduğunuz lifecycleScope kullanılan eş yordamları manuel olarak işlemeniz gerekirdi. Eş yordamın ViewModel'ın kapsamını aşması gerekiyorsa şuna göz atın: İşletme ve veri katmanı bölümünde eş yordamlar oluşturma.

Değişebilir türleri açığa çıkarma

Sabit türleri diğer sınıflara göstermeyi tercih edin. Bu şekilde projeyle ilgili değişken tür, tek bir sınıfta toplandığından, hataları ayıklamayı kolaylaştırır. bir şeyler ters gidebilir.

// 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şletme katmanlarındaki sınıflar genellikle bazı işlevleri veya zaman içindeki veri değişiklikleriyle ilgili bildirim almak için tek seferlik aramalar yapabilirsiniz. İlgili sınıflar katmanlarının tek seferlik aramalar için askıya alma ve Akış Veri değişiklikleri hakkında bildirim gönderme

// 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, arayanı (genellikle sunum katmanını), bu katmanlarda gerçekleştirilen işin yürütülmesini ve yaşam döngüsünü kontrol etmek ve iptal edebilir.

İş ve veri katmanında eş yordamlar oluşturma

Veri veya işletme katmanında, şunun için eş yordam oluşturulması gereken sınıflar için farklı seçenekler vardır.

Bu eş yordamlarda yapılacak çalışma yalnızca kullanıcı arayanın yaşam döngüsünü izlemelidir. Çoğu zaman durumlarda, arayan ViewModel olacaktır ve Kullanıcı ekrandan ayrılır ve ViewModel temizlenir. Böyle durumlarda 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 ve yapılan işin bağlı değilse görüşme, arayanın gözünden daha uzun yaşam döngüsü boyunca geçerlidir. Bu senaryoda, harici CoroutineScope kullanılmalıdır. Corutinler ve İptal edilmemesi gereken çalışma kalıplarıyla ilgili blog yayını.

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, şundan daha uzun süren bir sınıf tarafından oluşturulmalı ve yönetilmelidir: Application sınıfı veya bir ViewModel kapsamı bir gezinme grafiğine ayarlandı.

Testlere TestDispatcher öğeleri ekle

Örnek: TestDispatcher eklemeniz gerekir. İki tür kullanılabilir proje yönetimindeki kotlinx-coroutines-test kitaplığı:

  • StandardTestDispatcher: Eş yordamlar bir planlayıcı ile başlatıldı ve yürütülür test iş parçacığı meşgul olmadığında test edin. Test ileti dizisini askıya alarak koordinatlar, koordinatlar, koordinasyon, advanceUntilIdle.

  • UnconfinedTestDispatcher: Yeni eş yordamları istekli bir şekilde, engelleyici bir şekilde çalıştırır. Bu işlem genellikle yazı yazmanın eş yordamların testlerini kolaylaştırır ancak eş yordamların yürütülmesidir.

Ek ayrıntılar için her sevk görevlisi uygulamasının dokümanlarına bakın.

Eş yordamları test etmek için runTest eş yordam oluşturucu. runTest, bir TestCoroutineScheduler testlerde gecikmeleri atlatmayı ve sanal süreyi kontrol etmenizi sağlar. Ayrıca transkriptinizi bu planlayıcıyı kullanarak gerektiğinde ek test sevk görevlileri oluşturun.

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 şekilde şunları yapabilirsiniz: testlerinizi yapmak için tüm eş yordam kodunuzu tek test iş parçacığında çalıştırın deterministik. runTest, aynı olan tüm eş yordamları bekler geri dönmeden önce tamamlaması gereken test korutinin alt öğeleridir.

GlobalKapsamdan Kaçınma

Bu, Sevk Görevlerini Ekle en iyi uygulamasına benzer. Şunu kullanarak: GlobalScope bir sınıfın kullandığı CoroutineScope kodunu kodlayarak bazı dezavantajlar oluşturuyorsunuz girin:

  • Sabit kodlama değerlerini teşvik eder. GlobalScope kodunu sabit olarak kodlarsanız Dispatchers sabit olarak kodlayabilirsiniz.

  • Kodunuz kontrolsüz bir kapsamda yürütüldüğü için testi çok zor hale getirir. yürütülmesini kontrol edemezsiniz.

  • Tüm eş yordamlar için yürütülecek ortak bir CoroutineContext olamaz bir araç haline geldi.

Bunun yerine, hayatta kalması gereken işler için bir CoroutineScope yerleştirmeyi düşünebilirsiniz belirleneceğini konuşacağız. Şu bölüme göz atın: İşletme ve veri katmanı bölümünde eş yordamlar oluşturma ziyaret edin.

// 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
    }
}

Şurada GlobalScope ve alternatifleri hakkında daha fazla bilgi edinebilirsiniz: Koordinler ve İptal edilmemesi gereken çalışma kalıplarıyla ilgili blog yayını.

Eş yordamınızı iptal edilebilir hale getirin

Eş yordamlarda iptal, iş birliğine dayalıdır, yani bir eş yordamın iptal edilmesi Job iptal edildi. Eş yordam, askıya alınana veya kontrol edene kadar iptal edilmez gönderin. Eş yordamdaki işlemleri engellerseniz eş yordamın iptal edilebilir olduğunu doğrulayın.

Örneğin, diskten birden fazla dosya okuyorsanız, her dosyayı okuyarak eş yordamın iptal edilip edilmediğini kontrol edin. Paydaşların ihtiyaçlarını iptal durumunu kontrol etmek için ensureActive işlevini kullanın.

someScope.launch {
    for(file in files) {
        ensureActive() // Check for cancellation
        readFile(file)
    }
}

kotlinx.coroutines tarafından sağlanan tüm askıya alma işlevleri, ör. withContext ve delay iptal edilebilir. Eş yordamınız onları çağırıyorsa devam edebilir.

Eş yordamlarda iptal hakkında daha fazla bilgi için Eşzamanlı blog yayınında iptal işlemi.

İstisnalara dikkat edin

Eş yordamlara atılan işlenmemiş istisnalar uygulamanızın kilitlenmesine neden olabilir. İstisnalar bunları, üst düzey bir çizimle oluşturulmuş eş yordamların gövdesinde viewModelScope veya lifecycleScope.

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 bu blog yayınına göz atın: Eş yordamlarda istisnalar, veya Koordin istisna işleme hakkındaki makalemizi incelemenizi öneririz.

Eş yordamlar hakkında daha fazla bilgi

Daha fazla eş yordam kaynağı için bkz. Kotlin eş yordamları ve akışı için ek kaynaklar sayfasını ziyaret edin.