Android uygulamanızda Kotlin eş yordamlarını kullanma

Bu codelab'de, Android uygulamasında Kotlin Coroutines'i kullanmayı öğreneceksiniz. Bu, geri çağırma ihtiyacını azaltarak kodu basitleştirebilen yeni bir arka plan iş parçacığı yönetimi yöntemidir. Coroutine'ler, veritabanı veya ağ erişimi gibi uzun süren görevler için asenkron geri çağırmaları sıralı koda dönüştüren bir Kotlin özelliğidir.

Ne yapacağınız hakkında fikir edinmek için aşağıdaki kod snippet'ini inceleyin.

// Async callbacks
networkRequest { result ->
   // Successful network request
   databaseSave(result) { rows ->
     // Result saved
   }
}

Geri çağırmaya dayalı kod, eş yordamlar kullanılarak sıralı koda dönüştürülür.

// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved

Uzun süren görevler için geri çağırma stili kullanan ve Architecture Components ile oluşturulmuş mevcut bir uygulamayla başlayacaksınız.

Bu codelab'i tamamladığınızda, ağdan veri yüklemek için uygulamanızda coroutine'leri kullanacak kadar deneyim kazanacak ve coroutine'leri bir uygulamaya entegre edebileceksiniz. Ayrıca, coroutine'lerle ilgili en iyi uygulamaları ve coroutine'leri kullanan koda karşı nasıl test yazılacağını da öğreneceksiniz.

Ön koşullar

  • Mimari Bileşenler ViewModel, LiveData, Repository ve Room hakkında bilgi sahibi olmak.
  • Uzantı işlevleri ve lambda'lar dahil olmak üzere Kotlin söz dizimiyle ilgili deneyim
  • Ana iş parçacığı, arka plan iş parçacıkları ve geri çağırmalar dahil olmak üzere Android'de iş parçacıklarını kullanma konusunda temel bilgi sahibi olmanız gerekir.

Yapacaklarınız

  • Coroutine'larla yazılmış çağrı kodunu çalıştırın ve sonuçları alın.
  • Asenkron kodu sıralı hale getirmek için askıya alma işlevlerini kullanın.
  • Kodun nasıl yürütüleceğini kontrol etmek için launch ve runBlocking politikalarını kullanın.
  • suspendCoroutine kullanarak mevcut API'leri eş yordamlara dönüştürme tekniklerini öğrenin.
  • Architecture Components ile coroutine'leri kullanma
  • Coroutine'leri test etmeyle ilgili en iyi uygulamaları öğrenin.

İhtiyacınız olanlar

  • Android Studio 3.5 (Codelab, diğer sürümlerle de çalışabilir ancak bazı şeyler eksik olabilir veya farklı görünebilir).

Bu codelab'i uygularken herhangi bir sorunla (kod hataları, dilbilgisi hataları, net olmayan ifadeler vb.) karşılaşırsanız lütfen codelab'in sol alt köşesindeki Hata bildir bağlantısını kullanarak sorunu bildirin.

Kodu indirme

Bu codelab'deki tüm kodları indirmek için aşağıdaki bağlantıyı tıklayın:

Zip dosyasını indir

... veya aşağıdaki komutu kullanarak GitHub deposunu komut satırından klonlayın:

$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git

Sık sorulan sorular

Öncelikle başlangıçtaki örnek uygulamanın nasıl göründüğüne bakalım. Örnek uygulamayı Android Studio'da açmak için bu talimatları uygulayın.

  1. kotlin-coroutines ZIP dosyasını indirdiyseniz dosyayı açın.
  2. Android Studio'da coroutines-codelab projesini açın.
  3. start uygulama modülünü seçin.
  4. execute.pngÇalıştır düğmesini tıklayın ve bir emülatör seçin ya da Android Lollipop'u (desteklenen minimum SDK 21'dir) çalıştırabilen Android cihazınızı bağlayın. Kotlin eş yordamları ekranı görünmelidir:

Bu başlangıç uygulaması, ekrana bastıktan kısa bir süre sonra sayıyı artırmak için iş parçacıklarını kullanır. Ayrıca, ağdan yeni bir başlık getirip ekranda gösterir. Şimdi deneyin. Kısa bir süre sonra sayı ve mesajın değiştiğini göreceksiniz. Bu codelab'de, bu uygulamayı eş yordamları kullanacak şekilde dönüştüreceksiniz.

Bu uygulama, MainActivity içindeki kullanıcı arayüzü kodunu MainViewModel içindeki uygulama mantığından ayırmak için Architecture Components'ı kullanır. Projenin yapısı hakkında bilgi edinmek için zaman ayırın.

  1. MainActivity, kullanıcı arayüzünü görüntüler, tıklama işleyicilerini kaydeder ve Snackbar gösterebilir. Etkinlikleri MainViewModel'a iletir ve ekranı MainViewModel'daki LiveData'a göre günceller.
  2. MainViewModel, onMainViewClicked içindeki etkinlikleri işler ve LiveData. kullanarak MainActivity ile iletişim kurar.
  3. Executors, arka plan iş parçacığında hangi işlemlerin çalıştırılabileceğini tanımlar BACKGROUND,.
  4. TitleRepository, sonuçları ağdan getirir ve veritabanına kaydeder.

Projeye eş yordam ekleme

Kotlin'de eş yordamları kullanmak için projenizin build.gradle (Module: app) dosyasında coroutines-core kitaplığını eklemeniz gerekir. Codelab projelerinde bu işlem sizin için yapılmıştır. Bu nedenle, codelab'i tamamlamak için bu işlemi yapmanız gerekmez.

Android'de eş yordamlar, temel kitaplık ve Android'e özel uzantılar olarak kullanılabilir:

  • kotlinx-corountines-core : Kotlin'de eş yordamları kullanmak için ana arayüz
  • kotlinx-coroutines-android : Coroutine'lerde Android ana iş parçacığı için destek

Başlangıç uygulaması, build.gradle. bölümündeki bağımlılıkları zaten içerir. Yeni bir uygulama projesi oluştururken build.gradle (Module: app) dosyasını açıp eşzamanlı rutin bağımlılıklarını projeye eklemeniz gerekir.

dependencies {
  ...
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
}

Android'de ana iş parçacığının engellenmemesi gerekir. Ana iş parçacığı, kullanıcı arayüzündeki tüm güncellemeleri işleyen tek bir iş parçacığıdır. Ayrıca, tüm tıklama işleyicilerini ve diğer kullanıcı arayüzü geri çağırmalarını çağıran iş parçacığıdır. Bu nedenle, mükemmel bir kullanıcı deneyimi sağlamak için sorunsuz çalışması gerekir.

Uygulamanızın kullanıcıya görünür bir duraklama olmadan gösterilmesi için ana iş parçacığının ekranı 16 ms veya daha uzun aralıklarla güncellemesi gerekir. Bu, saniyede yaklaşık 60 kareye karşılık gelir. Büyük JSON veri kümelerini ayrıştırma, veritabanına veri yazma veya ağdan veri getirme gibi birçok yaygın görev bu süreden daha uzun sürer. Bu nedenle, ana iş parçacığından bu tür kodların çağrılması uygulamanın duraklamasına, takılmasına veya hatta donmasına neden olabilir. Ana ileti dizisini çok uzun süre engellerseniz uygulama kilitlenebilir ve Uygulama yanıt vermiyor iletişim kutusu gösterilebilir.

Android'de ana iş parçacığı güvenliğini sağlayarak bu sorunu nasıl çözdüğünü öğrenmek için aşağıdaki videoyu izleyin.

Geri arama kalıbı

Ana iş parçacığını engellemeden uzun süren görevleri gerçekleştirmenin bir yolu geri çağırmalardır. Geri çağırmaları kullanarak arka plan iş parçacığında uzun süren görevler başlatabilirsiniz. Görev tamamlandığında, ana iş parçacığında sonucu size bildirmek için geri çağırma işlemi yapılır.

Geri çağırma kalıbı örneğine göz atın.

// Slow request with callbacks
@UiThread
fun makeNetworkRequest() {
    // The slow network request runs on another thread
    slowFetch { result ->
        // When the result is ready, this callback will get the result
        show(result)
    }
    // makeNetworkRequest() exits after calling slowFetch without waiting for the result
}

Bu kod @UiThread ile açıklama eklenmiş olduğundan ana iş parçacığında yürütülecek kadar hızlı çalışmalıdır. Bu nedenle, bir sonraki ekran güncellemesinin gecikmemesi için çok hızlı bir şekilde geri dönmesi gerekir. Ancak slowFetch işleminin tamamlanması saniyeler hatta dakikalar süreceğinden ana iş parçacığı sonucu bekleyemez. show(result) geri çağırma işlevi, slowFetch öğesinin arka plan iş parçacığında çalışmasına ve sonuç hazır olduğunda döndürmesine olanak tanır.

Geri çağırmaları kaldırmak için eş yordamları kullanma

Geri çağırmalar harika bir yöntem olsa da bazı dezavantajları vardır. Geri çağırmaları yoğun bir şekilde kullanan kodların okunması ve üzerinde düşünülmesi zorlaşabilir. Ayrıca geri çağırmalar, istisnalar gibi bazı dil özelliklerinin kullanılmasına izin vermez.

Kotlin coroutines, geri çağırmaya dayalı kodu sıralı koda dönüştürmenize olanak tanır. Sırayla yazılan kodlar genellikle daha kolay okunur ve hatta istisnalar gibi dil özelliklerini kullanabilir.

Sonuç olarak, her ikisi de aynı şeyi yapar: uzun süren bir görevden sonuç alınana kadar bekler ve yürütmeye devam eder. Ancak kodda çok farklı görünürler.

suspend anahtar kelimesi, Kotlin'in bir işlevi veya işlev türünü coroutine'ler için kullanılabilir olarak işaretleme yöntemidir. Bir coroutine, suspend olarak işaretlenmiş bir işlevi çağırdığında, normal bir işlev çağrısı gibi bu işlevin döndürülmesini beklemek yerine sonuç hazır olana kadar yürütmeyi askıya alır ve sonuçla birlikte kaldığı yerden devam eder. Sonuç beklenirken askıya alındığında, üzerinde çalıştığı iş parçacığının engellemesini kaldırır . Böylece diğer işlevler veya eş yordamlar çalışabilir.

Örneğin, aşağıdaki kodda hem makeNetworkRequest() hem de slowFetch(), suspend işlevidir.

// Slow request with coroutines
@UiThread
suspend fun makeNetworkRequest() {
    // slowFetch is another suspend function so instead of 
    // blocking the main thread  makeNetworkRequest will `suspend` until the result is 
    // ready
    val result = slowFetch()
    // continue to execute after the result is ready
    show(result)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }

Geri çağırma sürümünde olduğu gibi, makeNetworkRequest @UiThread olarak işaretlendiği için ana ileti dizisinden hemen döndürülmelidir. Bu nedenle, genellikle slowFetch gibi engelleme yöntemlerini çağıramaz. suspend anahtar kelimesi burada marifetini gösterir.

Coroutine kodu, geri çağırmaya dayalı koda kıyasla daha az kodla mevcut iş parçacığının engellemesini kaldırma konusunda aynı sonucu elde eder. Sıralı stili sayesinde, birden fazla geri çağırma oluşturmadan uzun süren görevleri kolayca zincirleyebilirsiniz. Örneğin, iki ağ uç noktasından sonuç getiren ve veritabanına kaydeden kod, geri çağırma olmadan eş yordamlar içinde bir işlev olarak yazılabilir. Örneğin:

// Request data from network and save it to database with coroutines

// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
    // slowFetch and anotherFetch are suspend functions
    val slow = slowFetch()
    val another = anotherFetch()
    // save is a regular function and will block this thread
    database.save(slow, another)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }

Bir sonraki bölümde örnek uygulamaya eş yordamları tanıtacaksınız.

Bu alıştırmada, gecikmeden sonra mesaj görüntülemek için bir eş yordam yazacaksınız. Başlamak için Android Studio'da start modülünün açık olduğundan emin olun.

CoroutineScope'u anlama

Kotlin'de tüm eş yordamlar CoroutineScope içinde çalışır. Bir kapsam, işi aracılığıyla eşzamanlı rutinlerin yaşam süresini kontrol eder. Bir kapsamın işini iptal ettiğinizde, bu kapsamda başlatılan tüm eş yordamlar iptal edilir. Android'de, örneğin kullanıcı bir Activity veya Fragment'den uzaklaştığında çalışan tüm eş yordamları iptal etmek için bir kapsam kullanabilirsiniz. Kapsamlar, varsayılan bir dağıtıcı belirtmenize de olanak tanır. Bir dağıtıcı, hangi iş parçacığının bir eşrutini çalıştıracağını kontrol eder.

Kullanıcı arayüzü tarafından başlatılan ortak rutinler genellikle Android'deki ana iş parçacığı olan Dispatchers.Main üzerinde başlatılır. Dispatchers.Main üzerinde başlatılan bir eş yordam, askıya alınmış durumdayken ana iş parçacığını engellemez. ViewModel coroutine'ler neredeyse her zaman kullanıcı arayüzünü ana iş parçacığında güncellediğinden, coroutine'leri ana iş parçacığında başlatmak fazladan iş parçacığı geçişi yapmanızı önler. Ana iş parçacığında başlatılan bir ortak yordam, başlatıldıktan sonra istediği zaman göndericileri değiştirebilir. Örneğin, ana iş parçacığı dışında büyük bir JSON sonucunu ayrıştırmak için başka bir dağıtıcı kullanabilir.

viewModelScope'u kullanma

AndroidX lifecycle-viewmodel-ktx kitaplığı, ViewModel'lere kullanıcı arayüzüyle ilgili eşzamanlı rutinleri başlatmak üzere yapılandırılmış bir CoroutineScope ekler. Bu kitaplığı kullanmak için projenizin build.gradle (Module: start) dosyasına eklemeniz gerekir. Bu adım, codelab projelerinde zaten yapılmıştır.

dependencies {
  ...
  implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x"
}

Kitaplık, ViewModel sınıfının uzantı işlevi olarak viewModelScope ekler. Bu kapsam Dispatchers.Main ile sınırlıdır ve ViewModel temizlendiğinde otomatik olarak iptal edilir.

İş parçacıklarından eş yordamlara geçiş

MainViewModel.kt içinde bu kodla birlikte bir sonraki YAPILACAKLAR'ı bulun:

MainViewModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   BACKGROUND.submit {
       Thread.sleep(1_000)
       _taps.postValue("$tapCount taps")
   }
}

Bu kod, arka plan iş parçacığında çalışmak için BACKGROUND ExecutorService (util/Executor.kt içinde tanımlanır) kullanır. sleep, mevcut iş parçacığını engellediğinden ana iş parçacığında çağrılırsa kullanıcı arayüzünü dondurur. Kullanıcı ana görünümü tıkladıktan bir saniye sonra snackbar istenir.

KOD'dan BACKGROUND'u kaldırıp kodu tekrar çalıştırarak bunun nasıl gerçekleştiğini görebilirsiniz. Yükleme animasyonu gösterilmez ve her şey bir saniye sonra son duruma "atlar".

MainViewModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   Thread.sleep(1_000)
   _taps.postValue("$tapCount taps")
}

updateTaps değerini aynı işi yapan bu eş yordama dayalı kodla değiştirin. launch ve delay dosyalarını içe aktarmanız gerekir.

MainViewModel.kt

/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
   // launch a coroutine in viewModelScope
   viewModelScope.launch {
       tapCount++
       // suspend this coroutine for one second
       delay(1_000)
       // resume in the main dispatcher
       // _snackbar.value can be called directly from main thread
       _taps.postValue("$tapCount taps")
   }
}

Bu kod da aynı şeyi yapar ve bir snackbar göstermeden önce bir saniye bekler. Ancak bazı önemli farklılıklar vardır:

  1. viewModelScope.launch, viewModelScope içinde bir eşzamanlı rutin başlatır. Bu, viewModelScope'ya ilettiğimiz iş iptal edildiğinde bu iş/kapsamdaki tüm eşzamanlı rutinlerin iptal edileceği anlamına gelir. Kullanıcı, delay döndürülmeden önce Etkinlikten ayrılırsa bu coroutine, ViewModel yok edildiğinde onCleared çağrıldığında otomatik olarak iptal edilir.
  2. viewModelScope, Dispatchers.Main varsayılan dağıtıcıya sahip olduğundan bu eş yordam ana ileti dizisinde başlatılır. Farklı ileti dizilerinin nasıl kullanılacağını daha sonra göreceğiz.
  3. delay işlevi, suspend işlevidir. Bu, Android Studio'da sol oluktaki simgesiyle gösterilir. Bu coroutine ana iş parçacığında çalışsa da delay iş parçacığını bir saniye boyunca engellemez. Bunun yerine, gönderici, bir sonraki ifadede bir saniye içinde devam ettirilecek şekilde eş yordamı planlar.

Çalıştırın. Ana görünümü tıkladığınızda bir saniye sonra bir snackbar görmeniz gerekir.

Bir sonraki bölümde bu işlevi nasıl test edeceğinizi ele alacağız.

Bu alıştırmada, az önce yazdığınız kod için bir test yazacaksınız. Bu alıştırmada, Dispatchers.Main üzerinde çalışan eş yordamları kotlinx-coroutines-test kitaplığını kullanarak nasıl test edeceğiniz gösterilmektedir. Bu codelab'in ilerleyen bölümlerinde, doğrudan coroutine'lerle etkileşime giren bir test uygulayacaksınız.

Mevcut kodu inceleyin

MainViewModelTest.kt dosyasını androidTest klasöründe açın.

MainViewModelTest.kt

class MainViewModelTest {
   @get:Rule
   val coroutineScope =  MainCoroutineScopeRule()
   @get:Rule
   val instantTaskExecutorRule = InstantTaskExecutorRule()

   lateinit var subject: MainViewModel

   @Before
   fun setup() {
       subject = MainViewModel(
           TitleRepository(
                   MainNetworkFake("OK"),
                   TitleDaoFake("initial")
           ))
   }
}

Kural, JUnit'te bir testin yürütülmesinden önce ve sonra kod çalıştırmanın bir yoludur. MainViewModel'ı cihaz dışı bir testte test etmemize olanak tanıyan iki kural kullanılır:

  1. InstantTaskExecutorRule, her görevi eşzamanlı olarak yürütecek şekilde LiveData'ü yapılandıran bir JUnit kuralıdır.
  2. MainCoroutineScopeRule, bu kod tabanında Dispatchers.Main'nin kotlinx-coroutines-test'tan TestCoroutineDispatcher kullanacak şekilde yapılandırıldığı özel bir kuraldır. Bu, testlerin test için sanal saati ilerletmesine ve kodun birim testlerinde Dispatchers.Main kullanmasına olanak tanır.

setup yönteminde, test sahteleri kullanılarak MainViewModel öğesinin yeni bir örneği oluşturulur. Bunlar, gerçek ağı veya veritabanını kullanmadan test yazmaya yardımcı olmak için başlangıç kodunda sağlanan ağın ve veritabanının sahte uygulamalarıdır.

Bu testte, sahteler yalnızca MainViewModel bağımlılıklarını karşılamak için gereklidir. Bu kod laboratuvarının ilerleyen bölümlerinde, sahte verileri eş yordamları destekleyecek şekilde güncelleyeceksiniz.

Coroutine'ları kontrol eden bir test yazın

Ana görünüm tıklandıktan bir saniye sonra dokunma işlemlerinin güncellenmesini sağlayan yeni bir test ekleyin:

MainViewModelTest.kt

@Test
fun whenMainClicked_updatesTaps() {
   subject.onMainViewClicked()
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
   coroutineScope.advanceTimeBy(1000)
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}

onMainViewClicked işlevi çağrıldığında, az önce oluşturduğumuz eşzamanlı rutin başlatılır. Bu test, onMainViewClicked çağrıldıktan hemen sonra dokunma sayısının "0 dokunma" olarak kalmasını, 1 saniye sonra ise "1 dokunma" olarak güncellenmesini kontrol eder.

Bu test, onMainViewClicked tarafından başlatılan eş yordamın yürütülmesini kontrol etmek için sanal zamanı kullanır. MainCoroutineScopeRule, Dispatchers.Main üzerinde başlatılan eş yordamların yürütülmesini duraklatmanıza, devam ettirmenize veya kontrol etmenize olanak tanır. Burada advanceTimeBy(1_000) işlevini çağırıyoruz. Bu, ana dağıtıcının 1 saniye sonra devam ettirilmesi planlanan eş yordamları hemen yürütmesine neden olur.

Bu test tamamen deterministtir. Yani her zaman aynı şekilde yürütülür. Ayrıca, Dispatchers.Main üzerinde başlatılan eş yordamların yürütülmesi üzerinde tam kontrol sahibi olduğundan değerin ayarlanması için bir saniye beklemesi gerekmez.

Mevcut testi çalıştırma

  1. İçerik menüsünü açmak için düzenleyicinizde sınıf adını MainViewModelTest sağ tıklayın.
  2. İçerik menüsünde execute.pngRun 'MainViewModelTest''i (MainViewModelTest'i Çalıştır) seçin.
  3. Gelecekteki çalıştırmalar için bu test yapılandırmasını araç çubuğundaki execute.png düğmesinin yanındaki yapılandırmalardan seçebilirsiniz. Yapılandırma varsayılan olarak MainViewModelTest olarak adlandırılır.

Testin başarılı olduğunu görmeniz gerekir. Çalıştırmak için bir saniyeden çok daha kısa bir süre gerekir.

Sonraki alıştırmada, mevcut geri çağırma API'lerinden eş yordamları kullanmaya nasıl geçeceğinizi öğreneceksiniz.

Bu adımda, bir depoyu coroutine kullanacak şekilde dönüştürmeye başlayacaksınız. Bunu yapmak için ViewModel, Repository, Room ve Retrofit'ye eş yordamlar ekleyeceğiz.

Her bir mimari bölümün neyden sorumlu olduğunu anlamak, bunları eş yordam kullanmaya geçirmeden önce iyi bir fikirdir.

  1. MainDatabase, Title kaydeden ve yükleyen Room'u kullanarak bir veritabanı uygular.
  2. MainNetwork yeni bir başlık getiren bir ağ API'si uygular. Başlıkları getirmek için Retrofit kullanılır. Retrofit, rastgele hata döndürecek veya sahte veriler sağlayacak şekilde yapılandırılmış ancak bunun dışında gerçek ağ istekleri yapıyormuş gibi davranıyor.
  3. TitleRepository, ağ ve veritabanından gelen verileri birleştirerek başlığı getirmek veya yenilemek için tek bir API uygular.
  4. MainViewModel ekranın durumunu temsil eder ve etkinlikleri işler. Kullanıcı ekrana dokunduğunda başlığı yenilemesi için depoya talimat verir.

Ağ isteği kullanıcı arayüzü etkinlikleriyle yönlendirildiğinden ve bu etkinliklere dayalı bir eşzamanlılık rutini başlatmak istediğimizden, eşzamanlılık rutinlerini kullanmaya başlamak için doğal yer ViewModel'dır.

Geri arama sürümü

MainViewModel.kt simgesini açarak refreshTitle beyanını görün.

MainViewModel.kt

/**
* Update title text via this LiveData
*/
val title = repository.title


// ... other code ...


/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   // TODO: Convert refreshTitle to use coroutines
   _spinner.value = true
   repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
       override fun onCompleted() {
           _spinner.postValue(false)
       }

       override fun onError(cause: Throwable) {
           _snackBar.postValue(cause.message)
           _spinner.postValue(false)
       }
   })
}

Bu işlev, kullanıcı ekrana her tıkladığında çağrılır ve depoda başlığın yenilenmesine ve yeni başlığın veritabanına yazılmasına neden olur.

Bu uygulama, birkaç işlem yapmak için geri çağırma kullanır:

  • Sorgu başlatmadan önce _spinner.value = true ile yükleniyor dönen işareti gösterir.
  • Sonuç aldığında _spinner.value = false ile yükleme animasyonunu temizler.
  • Hata alırsa bir snackbar'ın gösterilmesini ister ve spinner'ı temizler.

onCompleted geri çağırmasının title iletilmediğini unutmayın. Tüm başlıkları Room veritabanına yazdığımız için kullanıcı arayüzü, Room tarafından güncellenen bir LiveData gözlemlenerek mevcut başlığa güncellenir.

Coroutines'in güncellenmesinde davranış tam olarak aynı kalacak. Kullanıcı arayüzünü otomatik olarak güncel tutmak için Room veritabanı gibi gözlemlenebilir bir veri kaynağı kullanmak iyi bir yöntemdir.

Coroutine sürümü

refreshTitle işlevini eş yordamlarla yeniden yazalım.

Hemen ihtiyacımız olacağı için depomuzda boş bir askıya alma işlevi oluşturalım (TitleRespository.kt). Kotlin'e eşzamanlı rutinlerle çalıştığını bildirmek için suspend operatörünü kullanan yeni bir işlev tanımlayın.

TitleRepository.kt

suspend fun refreshTitle() {
    // TODO: Refresh from network and write to database
    delay(500)
}

Bu codelab'i tamamladığınızda, yeni bir başlık getirmek ve bunu coroutine'leri kullanarak veritabanına yazmak için Retrofit ve Room'u kullanacak şekilde güncelleyeceksiniz. Şimdilik sadece 500 milisaniye boyunca çalışıyormuş gibi yapıp devam edecek.

MainViewModel içinde, refreshTitle geri çağırma sürümünü yeni bir eşzamanlı rutin başlatan sürümle değiştirin:

MainViewModel.kt

/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           repository.refreshTitle()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Bu işlevi adım adım inceleyelim:

viewModelScope.launch {

Dokunma sayısını güncelleyen eş yordamda olduğu gibi, viewModelScope içinde yeni bir eş yordam başlatarak başlayın. Bu işlem için Dispatchers.Main kullanılacak. refreshTitle bir ağ isteği ve veritabanı sorgusu oluştursa da ana işleme güvenli bir arayüz sunmak için eş yordamları kullanabilir. Bu, ana ileti dizisinden güvenli bir şekilde çağrılabileceği anlamına gelir.

viewModelScope kullandığımız için kullanıcı bu ekrandan uzaklaştığında bu coroutine tarafından başlatılan çalışma otomatik olarak iptal edilir. Bu nedenle, ek ağ istekleri veya veritabanı sorguları oluşturmaz.

Sonraki birkaç kod satırı, repository içinde refreshTitle işlevini çağırır.

try {
    _spinner.value = true
    repository.refreshTitle()
}

Bu eşzamanlı yordam herhangi bir işlem yapmadan önce yükleme döngüsünü başlatır ve ardından refreshTitle işlevini normal bir işlev gibi çağırır. Ancak refreshTitle, askıya alma işlevi olduğundan normal bir işlevden farklı şekilde yürütülür.

Geri arama iletmemiz gerekmez. Coroutine, refreshTitle tarafından devam ettirilene kadar askıya alınır. Normal bir engelleme işlevi çağrısı gibi görünse de ana iş parçacığını engellemeden devam etmeden önce ağ ve veritabanı sorgusu tamamlanana kadar otomatik olarak bekler.

} catch (error: TitleRefreshError) {
    _snackBar.value = error.message
} finally {
    _spinner.value = false
}

Askıya alma işlevlerindeki istisnalar, normal işlevlerdeki hatalar gibi çalışır. Bir askıya alma işlevinde hata oluşturursanız bu hata, arayana iletilir. Bu nedenle, oldukça farklı şekilde yürütülseler de bunları işlemek için normal try/catch bloklarını kullanabilirsiniz. Bu, her geri çağırma için özel hata işleme oluşturmak yerine hata işleme için yerleşik dil desteğine güvenmenize olanak tanıdığından kullanışlıdır.

Ayrıca, bir coroutine'den istisna atarsanız bu coroutine, varsayılan olarak üst öğesini iptal eder. Bu sayede, birbiriyle ilişkili birkaç görevi kolayca birlikte iptal edebilirsiniz.

Ardından, bir finally bloğunda, sorgu çalıştıktan sonra döndürücünün her zaman kapatılmasını sağlayabiliriz.

Başlangıç yapılandırmasını seçip execute.png tuşuna basarak uygulamayı tekrar çalıştırın. Herhangi bir yere dokunduğunuzda yükleme animasyonu görmeniz gerekir. Ağımızı veya veritabanımızı henüz bağlamadığımız için başlık aynı kalır.

Bir sonraki alıştırmada, gerçekten çalışmak için depoyu güncelleyeceksiniz.

Bu alıştırmada, TitleRepository işlevinin çalışan bir sürümünü uygulamak için bir eş yordamın üzerinde çalıştığı iş parçacığını nasıl değiştireceğinizi öğreneceksiniz.

refreshTitle'daki mevcut geri çağırma kodunu inceleyin

TitleRepository.kt simgesini açın ve mevcut geri aramaya dayalı uygulamayı inceleyin.

TitleRepository.kt

// TitleRepository.kt

fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
   // This request will be run on a background thread by retrofit
   BACKGROUND.submit {
       try {
           // Make network request using a blocking call
           val result = network.fetchNextTitle().execute()
           if (result.isSuccessful) {
               // Save it to database
               titleDao.insertTitle(Title(result.body()!!))
               // Inform the caller the refresh is completed
               titleRefreshCallback.onCompleted()
           } else {
               // If it's not successful, inform the callback of the error
               titleRefreshCallback.onError(
                       TitleRefreshError("Unable to refresh title", null))
           }
       } catch (cause: Throwable) {
           // If anything throws an exception, inform the caller
           titleRefreshCallback.onError(
                   TitleRefreshError("Unable to refresh title", cause))
       }
   }
}

TitleRepository.kt içinde refreshTitleWithCallbacks yöntemi, yükleme ve hata durumunu arayana bildirmek için geri çağırma ile uygulanır.

Bu işlev, yenilemeyi uygulamak için birkaç işlem yapar.

  1. BACKGROUND ExecutorService tuşlarına basarak başka bir ileti dizisine geçin.
  2. Engelleme execute() yöntemini kullanarak fetchNextTitle ağ isteğini çalıştırın. Bu, ağ isteğini geçerli iş parçacığında (bu örnekte BACKGROUND içindeki iş parçacıklarından biri) çalıştırır.
  3. Sonuç başarılıysa insertTitle ile veritabanına kaydedin ve onCompleted() yöntemini çağırın.
  4. Sonuç başarılı değilse veya bir istisna varsa arayan kişiye başarısız yenileme hakkında bilgi vermek için onError yöntemini çağırın.

Bu geri çağırmaya dayalı uygulama, ana iş parçacığını engellemediği için ana iş parçacığı için güvenlidir. Ancak, arayan kişiyi iş tamamlandığında bilgilendirmek için geri çağırma kullanması gerekir. Ayrıca, geçiş yaptığı BACKGROUND iş parçacığında geri çağırmaları da çağırır.

Coroutine'lardan gelen aramaları engelleme

Ağa veya veritabanına eş yordamlar eklemeden, eş yordamları kullanarak bu kodu ana işleme güvenli hale getirebiliriz. Bu, geri çağırma işleminden kurtulmamızı ve sonucu başlangıçta çağıran işleme geri iletmemizi sağlar.

Bu kalıbı, büyük bir listeyi sıralama ve filtreleme veya diskten okuma gibi, eş yordamın içinden engelleme veya CPU yoğun işlemler yapmanız gerektiğinde kullanabilirsiniz.

Coroutines, herhangi bir dağıtıcı arasında geçiş yapmak için withContext kullanır. Calling withContext, yalnızca lambda için diğer dağıtıcıya geçer ve ardından bu lambda'nın sonucuyla birlikte kendisini çağıran dağıtıcıya geri döner.

Kotlin eş yordamları varsayılan olarak üç Dispatcher sağlar: Main, IO ve Default. IO Dispatcher, ağdan veya diskten okuma gibi G/Ç işlemleri için optimize edilirken Default Dispatcher, CPU yoğun görevler için optimize edilir.

TitleRepository.kt

suspend fun refreshTitle() {
   // interact with *blocking* network and IO calls from a coroutine
   withContext(Dispatchers.IO) {
       val result = try {
           // Make network request using a blocking call
           network.fetchNextTitle().execute()
       } catch (cause: Throwable) {
           // If the network throws an exception, inform the caller
           throw TitleRefreshError("Unable to refresh title", cause)
       }
      
       if (result.isSuccessful) {
           // Save it to database
           titleDao.insertTitle(Title(result.body()!!))
       } else {
           // If it's not successful, inform the callback of the error
           throw TitleRefreshError("Unable to refresh title", null)
       }
   }
}

Bu uygulamada ağ ve veritabanı için engelleme çağrıları kullanılır ancak geri çağırma sürümünden biraz daha basittir.

Bu kodda hâlâ engelleme çağrıları kullanılıyor. execute() ve insertTitle(...) işlevlerinin çağrılması, bu eşzamanlı yordamın çalıştığı iş parçacığını engeller. Ancak Dispatchers.IO'ya withContext kullanarak geçtiğimizde IO dağıtıcısındaki iş parçacıklarından birini engelliyoruz. Bunu çağıran ve muhtemelen Dispatchers.Main üzerinde çalışan ortak yordam, withContext lambda'sı tamamlanana kadar askıya alınır.

Geri arama sürümüne kıyasla iki önemli fark vardır:

  1. withContext, sonucu kendisini çağıran Dispatcher'a (bu örnekte Dispatchers.Main) geri döndürür. Geri çağırma sürümü, BACKGROUND yürütme hizmetindeki bir iş parçacığında geri çağırmaları çağırdı.
  2. Arayan kullanıcının bu işleve geri çağırma iletmesi gerekmez. Sonucu veya hatayı almak için askıya alma ve devam ettirme işlemlerini kullanabilirler.

Uygulamayı tekrar çalıştırın.

Uygulamayı tekrar çalıştırırsanız yeni coroutine tabanlı uygulamanın sonuçları ağdan yüklediğini görürsünüz.

Sonraki adımda, eş yordamları Room ve Retrofit'e entegre edeceksiniz.

Coroutine entegrasyonuna devam etmek için Room ve Retrofit'in kararlı sürümündeki askıya alma işlevleri desteğini kullanacağız. Ardından, askıya alma işlevlerini kullanarak az önce yazdığımız kodu önemli ölçüde basitleştireceğiz.

Room'da eş yordamlar

İlk olarak MainDatabase.kt işlevini açın ve insertTitle işlevini askıya alma işlevi yapın:

MainDatabase.kt

// add the suspend modifier to the existing insertTitle

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)

Bunu yaptığınızda Room, sorgunuzu ana iş parçacığına güvenli hale getirir ve arka plan iş parçacığında otomatik olarak yürütür. Ancak bu, sorguyu yalnızca bir coroutine'in içinden çağırabileceğiniz anlamına da gelir.

Room'da coroutine'leri kullanmak için yapmanız gerekenler bu kadar. Çok kullanışlı.

Retrofit'te Coroutine'ler

Şimdi de eş yordamları Retrofit ile nasıl entegre edeceğimize bakalım. MainNetwork.kt dosyasını açın ve fetchNextTitle işlevini askıya alma işlevi olarak değiştirin.

MainNetwork.kt

// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String

interface MainNetwork {
   @GET("next_title.json")
   suspend fun fetchNextTitle(): String
}

Retrofit ile askıya alma işlevlerini kullanmak için iki şey yapmanız gerekir:

  1. İşleve bir askıya alma değiştiricisi ekleyin
  2. Dönüş türünden Call sarmalayıcısını kaldırın. Burada String döndürüyoruz ancak karmaşık JSON destekli türü de döndürebilirsiniz. Yine de retrofit'in tam Result erişimini sağlamak istiyorsanız askıya alma işlevinden String yerine Result<String> döndürebilirsiniz.

Retrofit, askıya alma işlevlerini otomatik olarak ana güvenli hale getirir. Böylece bunları doğrudan Dispatchers.Main'den çağırabilirsiniz.

Oda ve Retrofit'i kullanma

Room ve Retrofit artık askıya alma işlevlerini desteklediğinden bunları depomuzdan kullanabiliriz. TitleRepository.kt dosyasını açın ve askıya alma işlevlerini kullanmanın, engelleme sürümüne kıyasla bile mantığı nasıl büyük ölçüde basitleştirdiğini görün:

BaşlıkRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Vay be, bu çok daha kısa. Ne oldu? Askıya alma ve devam ettirme işlevlerini kullanmak, kodun çok daha kısa olmasını sağlıyor. Retrofit, burada String gibi dönüş türlerini veya Call yerine User nesnesini kullanmamıza olanak tanır. Bu işlem güvenlidir. Çünkü askıya alma işlevi içinde Retrofit, ağ isteğini arka plan iş parçacığında çalıştırabilir ve çağrı tamamlandığında eş yordamı devam ettirebilir.

Daha da iyisi, withContext simgesini kaldırdık. Hem Room hem de Retrofit, main-safe askıya alma işlevleri sağladığından bu asenkron çalışmayı Dispatchers.Main'dan düzenlemek güvenlidir.

Derleyici hatalarını düzeltme

Askıya alma işlevini normal bir işlevden çağıramadığınız için, eş yordamlara geçiş yaparken işlevlerin imzasını değiştirmeniz gerekir. Bu adımda suspend değiştiricisini eklediğinizde, gerçek bir projede bir işlevi askıya alacak şekilde değiştirirseniz ne olacağını gösteren birkaç derleyici hatası oluşturuldu.

Projeyi inceleyin ve oluşturulan işlevi askıya alacak şekilde değiştirerek derleyici hatalarını düzeltin. Her biri için hızlı çözümler aşağıda verilmiştir:

TestingFakes.kt

Yeni askıya alma değiştiricilerini desteklemek için sahte testleri güncelleyin.

TitleDaoFake

  1. Hiyerarşideki tüm işlevlere askıya alma değiştiricileri eklemek için alt+enter tuşlarına basın.

MainNetworkFake

  1. Hiyerarşideki tüm işlevlere askıya alma değiştiricileri eklemek için alt+enter tuşlarına basın.
  2. fetchNextTitle yerine bu işlevi kullanın
override suspend fun fetchNextTitle() = result

MainNetworkCompletableFake

  1. Hiyerarşideki tüm işlevlere askıya alma değiştiricileri eklemek için alt+enter tuşlarına basın.
  2. fetchNextTitle yerine bu işlevi kullanın
override suspend fun fetchNextTitle() = completable.await()

TitleRepository.kt

  • Artık kullanılmadığı için refreshTitleWithCallbacks işlevini silin.

Uygulamayı çalıştırma

Uygulamayı tekrar çalıştırın. Derlendikten sonra, ViewModel'den Room ve Retrofit'e kadar her yerde eş yordamlar kullanılarak verilerin yüklendiğini göreceksiniz.

Tebrikler, bu uygulamayı tamamen eşyordam kullanacak şekilde değiştirdiniz. Son olarak, yaptığımız işlemleri nasıl test edeceğimizden bahsedeceğiz.

Bu alıştırmada, doğrudan bir suspend işlevini çağıran bir test yazacaksınız.

refreshTitle herkese açık bir API olarak kullanıma sunulduğundan doğrudan test edilecek ve testlerden nasıl eşzamanlı yordam işlevlerinin çağrılacağı gösterilecek.

Son alıştırmada uyguladığınız refreshTitle işlevini aşağıda görebilirsiniz:

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Askıya alma işlevini çağıran bir test yazma

İki YAPILACAKLAR öğesi içeren test klasöründe TitleRepositoryTest.kt öğesini aç.

İlk test cihazından refreshTitle numaralı telefonu aramayı deneyin.whenRefreshTitleSuccess_insertsRows

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   subject.refreshTitle()
}

refreshTitle, suspend işlevi olduğundan Kotlin, bu işlevi bir coroutine veya başka bir askıya alma işlevi dışında nasıl çağıracağını bilmez ve "Suspend function refreshTitle should be called only from a coroutine or another suspend function." (Askıya alma işlevi olan refreshTitle yalnızca bir coroutine veya başka bir askıya alma işlevi içinden çağrılmalıdır.) gibi bir derleyici hatası alırsınız.

Test çalıştırıcı, eş yordamlar hakkında hiçbir şey bilmediğinden bu testi askıya alma işlevi haline getiremeyiz. launch, ViewModel içindeki CoroutineScope gibi bir kullanarak eş yordam oluşturabiliriz. Ancak testlerin döndürülmeden önce eş yordamları tamamlaması gerekir. Bir test işlevi döndüğünde test sona erer. launch ile başlatılan eş yordamlar, gelecekte bir noktada tamamlanabilecek eşzamansız kodlardır. Bu nedenle, eşzamansız kodu test etmek için testin, eş yordamınız tamamlanana kadar beklemesini sağlayacak bir yöntem kullanmanız gerekir. launch, engellemeyen bir çağrı olduğundan hemen geri döner ve işlev döndükten sonra bir eş yordam çalıştırmaya devam edebilir. Bu nedenle, testlerde kullanılamaz. Örneğin:

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   // launch starts a coroutine then immediately returns
   GlobalScope.launch {
       // since this is asynchronous code, this may be called *after* the test completes
       subject.refreshTitle()
   }
   // test function returns immediately, and
   // doesn't see the results of refreshTitle
}

Bu test bazen başarısız olur. launch çağrısı hemen döndürülür ve test senaryosunun geri kalanıyla aynı anda yürütülür. Test, refreshTitle'nın henüz çalışıp çalışmadığını bilemez ve veritabanının güncellendiğini kontrol etmek gibi tüm onaylamalar güvenilmez olur. Ayrıca, refreshTitle bir istisna oluşturduysa bu istisna test çağrı yığınında oluşturulmaz. Bunun yerine, GlobalScope'ın yakalanmayan istisna işleyicisine gönderilir.

Kitaplık kotlinx-coroutines-test, askıya alma işlevlerini çağırırken engelleme yapan runBlockingTest işlevine sahiptir. runBlockingTest bir askıya alma işlevini veya launches yeni bir ortak yordamı çağırdığında varsayılan olarak hemen yürütür. Bunu, askıya alma işlevlerini ve eş yordamlarını normal işlev çağrılarına dönüştürmenin bir yolu olarak düşünebilirsiniz.

Ayrıca, runBlockingTest yakalanmamış istisnaları sizin için yeniden oluşturur. Bu sayede, bir coroutine'in istisna oluşturduğu durumları test etmek kolaylaşır.

Tek bir coroutine ile test uygulama

refreshTitle adlı görüşmeyi runBlockingTest ile sarmalayın ve GlobalScope.launch sarmalayıcısını subject.refreshTitle() öğesinden kaldırın.

TitleRepositoryTest.kt

@Test
fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest {
   val titleDao = TitleDaoFake("title")
   val subject = TitleRepository(
           MainNetworkFake("OK"),
           titleDao
   )

   subject.refreshTitle()
   Truth.assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK")
}

Bu test, refreshTitle tarafından veritabanına "Tamam" ifadesinin eklenip eklenmediğini kontrol etmek için sağlanan sahte verileri kullanır.

Test runBlockingTest işlevini çağırdığında, runBlockingTest tarafından başlatılan eşzamanlı rutin tamamlanana kadar engellenir. Ardından, refreshTitle işlevi çağrıldığında, veritabanı satırının sahte veritabanımıza eklenmesini beklemek için normal askıya alma ve devam ettirme mekanizması kullanılır.

Test coroutine'i tamamlandıktan sonra runBlockingTest döndürülür.

Zaman aşımı testi yazma

Ağ isteğine kısa bir zaman aşımı eklemek istiyoruz. Önce testi yazalım, ardından zaman aşımını uygulayalım. Yeni bir test oluşturma:

TitleRepositoryTest.kt

@Test(expected = TitleRefreshError::class)
fun whenRefreshTitleTimeout_throws() = runBlockingTest {
   val network = MainNetworkCompletableFake()
   val subject = TitleRepository(
           network,
           TitleDaoFake("title")
   )

   launch {
       subject.refreshTitle()
   }

   advanceTimeBy(5_000)
}

Bu test, sağlanan sahte MainNetworkCompletableFake'yi kullanır. Bu, test devam edene kadar arayanları bekletmek için tasarlanmış bir ağ sahtesidir. refreshTitle, ağ isteği yapmaya çalıştığında zaman aşımlarını test etmek istediğimiz için sonsuza kadar askıda kalır.

Ardından, refreshTitle işlevini çağırmak için ayrı bir eşzamanlı iş parçacığı başlatır. Bu, zaman aşımlarını test etmenin önemli bir parçasıdır. Zaman aşımı, runBlockingTest tarafından oluşturulanlardan farklı bir ortak yordamda gerçekleşmelidir. Bunu yaptığımızda, advanceTimeBy(5_000) bir sonraki satırı çağırabiliriz. Bu işlem, süreyi 5 saniye ileri sarar ve diğer eş yordamın zaman aşımına uğramasına neden olur.

Bu, tam bir zaman aşımı testidir ve zaman aşımı uygulandığında başarılı olur.

Şimdi çalıştırıp neler olduğuna bakalım:

Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]

runBlockingTest'ın özelliklerinden biri, test tamamlandıktan sonra eş yordamların sızmasına izin vermemesidir. Testin sonunda, başlatma ortak yordamımız gibi tamamlanmamış ortak yordamlar varsa test başarısız olur.

Zaman aşımı ekleme

TitleRepository uygulamasını açın ve ağ getirme işlemine beş saniyelik bir zaman aşımı ekleyin. Bu işlemi withTimeout işlevini kullanarak yapabilirsiniz:

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = withTimeout(5_000) {
           network.fetchNextTitle()
       }
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Testi çalıştırın. Testleri çalıştırdığınızda tüm testlerin başarılı olduğunu görmeniz gerekir.

Bir sonraki alıştırmada, eş yordamları kullanarak üst düzey işlevleri nasıl yazacağınızı öğreneceksiniz.

Bu alıştırmada, genel bir veri yükleme işlevi kullanmak için MainViewModel içindeki refreshTitle öğesini yeniden düzenleyeceksiniz. Bu kurs, eş yordamları kullanan yüksek sıralı işlevler oluşturmayı öğretir.

refreshTitle şu anki uygulaması çalışıyor ancak her zaman yükleme animasyonunu gösteren genel bir veri yükleme eş yordamı oluşturabiliriz. Bu, verileri çeşitli etkinliklere yanıt olarak yükleyen ve yükleme animasyonunun tutarlı bir şekilde gösterilmesini sağlamak isteyen bir kod tabanında yararlı olabilir.

Mevcut uygulamayı inceleme. repository.refreshTitle() hariç her satır, yükleme simgesini göstermek ve hataları görüntülemek için kullanılan standart kod.

// MainViewModel.kt

fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           // this is the only part that changes between sources
           repository.refreshTitle() 
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Yüksek sıralı işlevlerde eş yordamları kullanma

Bu kodu MainViewModel.kt dosyasına ekleyin.

MainViewModel.kt

private fun launchDataLoad(block: suspend () -> Unit): Job {
   return viewModelScope.launch {
       try {
           _spinner.value = true
           block()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Şimdi refreshTitle() işlevini bu yüksek sıralı işlevi kullanacak şekilde yeniden düzenleyin.

MainViewModel.kt

fun refreshTitle() {
   launchDataLoad {
       repository.refreshTitle()
   }
}

Yükleme animasyonu gösterme ve hataları gösterme ile ilgili mantığı soyutlayarak verileri yüklemek için gereken gerçek kodumuzu basitleştirdik. Yükleme animasyonu göstermek veya hata görüntülemek, herhangi bir veri yükleme işleminde kolayca genellenebilir. Ancak gerçek veri kaynağı ve hedefi her seferinde belirtilmelidir.

Bu soyutlamayı oluşturmak için launchDataLoad, askıya alma lambda'sı olan bir block bağımsız değişkenini alır. Askıya alma lambda'sı, askıya alma işlevlerini çağırmanızı sağlar. Kotlin, bu codelab'de kullandığımız launch ve runBlocking adlı coroutine oluşturucuları bu şekilde uygular.

// suspend lambda

block: suspend () -> Unit

Askıya alma lambda'sı oluşturmak için suspend anahtar kelimesiyle başlayın. İşlev oku ve dönüş türü Unit bildirimi tamamlar.

Kendi askıya alma lambda'larınızı genellikle bildirmeniz gerekmez ancak tekrarlanan mantığı kapsayan bu tür soyutlamalar oluşturmak için yararlı olabilirler.

Bu alıştırmada, WorkManager'daki ortak rutin tabanlı kodu nasıl kullanacağınızı öğreneceksiniz.

WorkManager nedir?

Android'de ertelenebilir arka plan görevleri için birçok seçenek vardır. Bu alıştırmada, WorkManager'ı coroutine'lerle nasıl entegre edeceğiniz gösterilmektedir. WorkManager, ertelenebilir arka plan çalışmaları için uyumlu, esnek ve basit bir kitaplıktır. WorkManager, Android'deki bu kullanım alanları için önerilen çözümdür.

WorkManager, Android Jetpack'in bir parçasıdır ve fırsatçı ve garantili yürütmenin bir kombinasyonunu gerektiren arka plan çalışmaları için bir Mimari Bileşen'dir. Fırsatçı yürütme, WorkManager'ın arka plan çalışmanızı mümkün olan en kısa sürede yapacağı anlamına gelir. Çalışmanın garanti edilmesi, uygulamanızdan ayrılsanız bile WorkManager'ın çeşitli durumlarda çalışmanızı başlatma mantığını ele alacağı anlamına gelir.

Bu nedenle WorkManager, sonunda tamamlanması gereken görevler için iyi bir seçimdir.

WorkManager'ın iyi bir şekilde kullanılabileceği görevlere bazı örnekler:

  • Günlükleri yükleme
  • Resimlere filtre uygulama ve resmi kaydetme
  • Yerel verileri ağla düzenli olarak senkronize etme

WorkManager ile coroutine'leri kullanma

WorkManager, farklı kullanım alanları için temel ListanableWorker sınıfının farklı uygulamalarını sağlar.

En basit Worker sınıfı, WorkManager tarafından bazı senkron işlemlerin yürütülmesine olanak tanır. Ancak, kod tabanımızı eş yordamları ve askıya alma işlevlerini kullanacak şekilde dönüştürmek için şimdiye kadar yaptığımız çalışmalar göz önüne alındığında, WorkManager'ı kullanmanın en iyi yolu, CoroutineWorker sınıfı aracılığıyla doWork()işlevimizi askıya alma işlevi olarak tanımlamaktır.

Başlamak için RefreshMainDataWork adresini açın. Bu özellik zaten CoroutineWorker'ı genişletir ve doWork'ı uygulamanız gerekir.

suspend doWork işlevinin içinde, depodan refreshTitle() işlevini çağırın ve uygun sonucu döndürün.

YAPILACAKLAR listesini tamamladıktan sonra kod aşağıdaki gibi görünür:

override suspend fun doWork(): Result {
   val database = getDatabase(applicationContext)
   val repository = TitleRepository(network, database.titleDao)

   return try {
       repository.refreshTitle()
       Result.success()
   } catch (error: TitleRefreshError) {
       Result.failure()
   }
}

CoroutineWorker.doWork() işlevinin askıya alma işlevi olduğunu unutmayın. Daha basit olan Worker sınıfının aksine, bu kod WorkManager yapılandırmanızda belirtilen Executor üzerinde ÇALIŞMAZ. Bunun yerine, coroutineContext üyesindeki göndericiyi (varsayılan olarak Dispatchers.Default) kullanır.

CoroutineWorker'ımızı test etme

Hiçbir kod tabanı test edilmeden tamamlanmamalıdır.

WorkManager, Worker sınıflarınızı test etmek için birkaç farklı yöntem sunar. Orijinal test altyapısı hakkında daha fazla bilgi edinmek için dokümanları okuyabilirsiniz.

WorkManager v2.1, ListenableWorker sınıflarını ve dolayısıyla CoroutineWorker'ı test etmenin daha basit bir yolunu desteklemek için yeni bir API grubu sunar. Kodumuzda şu yeni API'lerden birini kullanacağız: TestListenableWorkerBuilder.

Yeni testimizi eklemek için androidTest klasöründeki RefreshMainDataWorkTest dosyasını güncelleyin.

Dosyanın içeriği:

package com.example.android.kotlincoroutines.main

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.example.android.kotlincoroutines.fakes.MainNetworkFake
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4


@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {

@Test
fun testRefreshMainDataWork() {
   val fakeNetwork = MainNetworkFake("OK")

   val context = ApplicationProvider.getApplicationContext<Context>()
   val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context)
           .setWorkerFactory(RefreshMainDataWork.Factory(fakeNetwork))
           .build()

   // Start the work synchronously
   val result = worker.startWork().get()

   assertThat(result).isEqualTo(Result.success())
}

}

Teste başlamadan önce, sahte ağı yerleştirebilmemiz için WorkManager'ya fabrika hakkında bilgi veriyoruz.

Testin kendisi, TestListenableWorkerBuilder yöntemini çağırarak çalıştırabileceğimiz işçimizi oluşturmak için startWork() yöntemini kullanır.

WorkManager, API tasarımını basitleştirmek için nasıl kullanılacağını gösteren örneklerden yalnızca biridir.

Bu codelab'de, uygulamanızda coroutine'leri kullanmaya başlamak için ihtiyacınız olan temel bilgileri ele aldık.

Ele aldığımız konular:

  • Asenkron programlamayı basitleştirmek için hem kullanıcı arayüzünden hem de WorkManager işlerinden Android uygulamalarına nasıl eş yordam entegre edileceği,
  • Ana iş parçacığını engellemeden ağdan veri getirmek ve verileri bir veritabanına kaydetmek için ViewModel içinde eş yordamları kullanma
  • Ayrıca ViewModel tamamlandığında tüm eş yordamların nasıl iptal edileceğini de öğrenin.

Coroutine tabanlı kodu test etmek için hem davranış testi yaptık hem de suspend işlevlerini doğrudan testlerden çağırdık.

Daha fazla bilgi

Android'de daha gelişmiş eş yordam kullanımı hakkında bilgi edinmek için "Kotlin Flow ve LiveData ile Gelişmiş Eş Yordamlar" codelab'ine göz atın.

Kotlin coroutine'lerinin bu codelab'de ele alınmayan birçok özelliği vardır. Kotlin coroutines hakkında daha fazla bilgi edinmek istiyorsanız JetBrains tarafından yayınlanan coroutines kılavuzlarını okuyun. Android'de coroutine'lerin daha fazla kullanım şekli için "Kotlin coroutine'leri ile uygulama performansını artırma" başlıklı makaleyi de inceleyin.