Merhaba Android geliştiricileri! Projede RxJava’dan Kotlin Coroutines’a geçiş yaparken yaşadığım deneyimleri paylaşmak istiyorum. Asenkron programlamada bu iki güçlü aracın sunduğu avantajları ve karşılaştığım zorlukları adım adım ele alacağız.
İçindekiler
- Neden Geçiş Yapmalıyız?
- RxJava ile Asenkron İşlemler
- Coroutines’a Geçiş Temelleri
- CoroutineScope ve Dispatcher Yönetimi
- ViewModel’de Coroutine Kullanımı
- Özel Extension Fonksiyonları: throttleFirst ve collectFlow
- Hata Yönetimi
- Best Practice’ler
- Kaynaklar

Neden Geçiş Yapmalıyız?
Asenkron programlama, modern mobil uygulamaların vazgeçilmez bir parçasıdır. Ağ istekleri, veri tabanı işlemleri ve kullanıcı etkileşimleri gibi birçok işlem, arka planda yürütülürken, kullanıcıya kesintisiz ve akıcı bir deneyim sunmak için doğru yönetilmelidir. RxJava, bu alanda uzun süredir güçlü bir araç olarak kullanılıyor. Ancak, karmaşık yapısı ve öğrenme eğrisi bazı geliştiricileri alternatif çözümlere yönlendirmiştir. Kotlin Coroutines ise daha basit ve okunabilir bir yapı sunarak bu ihtiyacı karşılıyor.
RxJava’nın Avantajları:
- Güçlü Reaktif Programlama Yetenekleri:
- Veri akışlarını yönetmek için esnek bir yapı sunar.
- Geniş Operatör Yelpazesi:
- Farklı senaryolar için birçok operatör seçeneği sağlar.
- Çoklu Platform Desteği:
- Android dışında da kullanılabilir, bu da projeler arasında tutarlılığı artırır.
Coroutines’ın Avantajları:
- Daha basit ve okunabilir kod
- Daha az boilerplate kod
- Native Kotlin desteği
Bu avantajlar göz önüne alındığında, Kotlin Coroutines’a geçiş yapmak, daha sürdürülebilir ve bakımı kolay bir kod tabanı oluşturmanıza yardımcı olabilir.
RxJava ile Asenkron İşlemler
RxJava ile asenkron işlemleri yönetmek oldukça güçlüdür, ancak bazen karmaşık ve tekrarlı kod yapıları oluşturabilir. Geçtiğimiz projede, RxJava kullanarak bir ağ isteği gerçekleştirdiğimde aşağıdaki gibi bir yapı kullanıyordum:
abstract class RxUseCase<Response, Params> {
abstract fun execute(params: Params?): Observable<Response>
fun run(observer: DisposableObserver<Response>, params: Params?) {
execute(params)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(observer)
}
}
Bu sınıf, belirli parametrelerle ağ istekleri yapmamı sağlıyordu. Ancak, her use-case için benzer dispatcher ayarlamaları yapmak kodun tekrarlanmasına neden oluyor ve bakımını zorlaştırıyordu. Bu durum, özellikle projede çok sayıda use-case olduğunda daha belirgin hale geliyor.
Coroutines’a Geçiş Temelleri
İlk adım olarak, RxJava tabanlı yapıyı Kotlin Coroutines ile değiştirmeyi hedefliyoruz. Bunun için öncelikle temel bir coroutine tabanlı use-case sınıfı oluşturacağız.
abstract class CoroutineUseCase<Response, Params> {
abstract suspend fun execute(params: Params?): Response
suspend fun run(params: Params?): Response {
return execute(params)
}
}
Bu sınıf, suspend anahtar kelimesi ile tanımlanmış bir fonksiyon içerir. Bu sayede, coroutine içinde çağrılabilir ve asenkron işlemleri daha basit bir şekilde yönetebiliriz.
Temel Farklar
- RxJava: Observable ve DisposableObserver kullanarak asenkron işlemleri yönetir.
- Coroutines: suspend fonksiyonlar ve coroutine scope kullanarak daha basit ve okunabilir bir yapı sunar.
Bu basit adımla, RxJava’nın karmaşıklığından kurtularak, daha temiz ve anlaşılır bir kod yapısına sahip olmaya başladık.
CoroutineScope ve Dispatcher Yönetimi
Kotlin Coroutines ile çalışırken, coroutine’lerin yaşam döngüsünü ve hangi thread üzerinde çalışacağını belirlemek önemlidir. Bu nedenle, CoroutineScope ve Dispatcher kullanımı kritik bir rol oynar.
CoroutineScope Kullanımı
CoroutineScope, coroutine’lerin yaşam döngüsünü yönetmek için kullanılır. Özellikle viewModel’ler içinde viewModelScopekullanmak, coroutine’lerin ViewModel’in yaşam döngüsüne bağlı olarak otomatik olarak iptal edilmesini sağlar. Bu, hafıza sızıntılarını önlemek ve kaynakları verimli kullanmak için kritik öneme sahiptir.
abstract class CoroutineRequestUseCase<Response, Params> {
abstract suspend fun execute(params: Params?): Response
fun run(
scope: CoroutineScope,
observer: CoroutineObserver<Response>,
params: Params?
) {
scope.launch {
try {
val response = withContext(Dispatchers.IO) {
execute(params)
}
withContext(Dispatchers.Main) {
observer.onSuccess(response)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
observer.onError(e)
}
}
}
}
}
Dispatcher Kullanımı
- Dispatchers.IO: Ağ çağrıları ve disk işlemleri gibi IO yoğun işlemler için kullanılır.
- Dispatchers.Main: UI güncellemeleri için kullanılır.
- withContext: Belirli bir dispatcher üzerinde çalışması gereken kod parçalarını belirtmek için kullanılır.
Bu yapı, coroutine’lerin doğru thread üzerinde çalışmasını sağlayarak performansı optimize eder.
ViewModel’de Coroutine Kullanımı
ViewModel’ler, Android’in MVVM mimarisinde veri yönetimi ve UI ile etkileşimde bulunan önemli bileşenlerdir. Coroutine’lerin yaşam döngüsünü ViewModel’e bağlı olarak yönetmek için viewModelScope kullanmak en iyi yaklaşımlardan biridir.
UseCase Sınıfının Düzenlenmesi
UseCase sınıfımızı, CoroutineScope parametresi alacak şekilde düzenleyelim. Bu sayede, coroutine’lerin yaşam döngüsünü ViewModel’e bağlı olarak yönetebiliriz.
class MyViewModel : ViewModel() {
private val useCase = MyUseCase()
fun performNetworkRequest(params: Params) {
useCase.run(
scope = viewModelScope,
observer = object : CoroutineObserver<Response> {
override fun onSuccess(response: Response) {
// Başarılı sonuç işleme
}
override fun onError(e: Exception) {
// Hata işleme
}
},
params = params
)
}
}
Bu yaklaşımla:
- Dispatcher Kullanımı:
- Ağ çağrıları Dispatchers.IO üzerinde çalışır, UI güncellemeleri ise Dispatchers.Main üzerinde yapılır.
- Scope Yönetimi:
- viewModelScope, ViewModel’in yaşam döngüsüne bağlı olarak coroutine’leri otomatik olarak iptal eder, böylece hafıza sızıntıları önlenir.
- Clean Code:
- Coroutine yönetimi ViewModel içinde yapıldığı için, UseCase sınıfı sadece iş mantığına odaklanır.
Özel Extension Fonksiyonlar: throttleFirst ve collectFlow
Projelerimde, RxJava’dan Coroutines’a geçiş sürecinde karşılaştığım bazı zorlukları aşmak için özel extension fonksiyonlar geliştirdim. Bu fonksiyonlar, coroutine ve Flow kullanımını daha da kolaylaştırmak ve kod tekrarını azaltmak amacıyla oluşturuldu.
throttleFirst Operatörü
throttleFirst, yüksek frekansta tetiklenen olaylarda sadece ilkini ileten bir Flow operatörüdür. Bu operatör, belirli bir zaman penceresi (windowDurationMillis) içerisinde gelen ilk değeri emit eder (yayınlar) ve sonraki değerleri bu süre boyunca yoksayar. Bu, hızlı tekrar eden olayları (örneğin, buton tıklamaları) yönetmek için idealdir.
fun <T> Flow<T>.throttleFirst(windowDurationMillis: Long): Flow<T> = channelFlow {
val mutex = Mutex()
var lastEmitTime = 0L
collect { value ->
val currentTime = System.currentTimeMillis()
mutex.withLock {
if (currentTime - lastEmitTime >= windowDurationMillis) {
lastEmitTime = currentTime
send(value)
}
}
}
}
Kullanım Örneği
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.eventFlow
.throttleFirst(1000)
.collectLatest { event ->
handleEvent(event)
}
}
}
Avantajları
- Event Flooding’i Önler: Belirli bir zaman penceresi içerisinde sadece ilk olayı işleyerek, hızlı tekrar eden olayların (örn. çoklu buton tıklamaları) işlenmesini engeller.
- Kullanıcı Deneyimini İyileştirir: Kullanıcının hızlıca butona birkaç kez tıklaması durumunda istenmeyen yan etkileri (örn. fragment’ın veya dialog’ların birden fazla açılması) önler.
- Kullanım Kolaylığı: Manuel olarak tekrarları yönetmek yerine, throttleFirst operatörünü kullanarak UI sorunları engellenebilir.
Dikkat Edilmesi Gerekenler
- Gecikmeler Kullanıcı Deneyimini Olumsuz Etkileyebilir: Çok kısa throttle süresi vermek de çok uzun bir süre vermek de istenen deneyimi sağlamayabilir o yüzden 300L gibi bir standart belirlemek iyi olabilir.
- StateFlow ile Kullanım: StateFlow’un sürekli güncellenen yapısı nedeniyle, throttle süresine dikkat edilmezse sorun çıkabilir.
collectFlow Operatörü
collectFlow, Flow’ları yaşam döngüsüne duyarlı (Lifecycle Aware) bir şekilde toplamak için geliştirdiğim özel bir extension fonksiyonudur. Bu fonksiyon, en son emit edilen (yayınlanan) değeri toplar ve Flow üzerinde belirli (throttle, debounce vb) operatörler uygulanabilir.
fun <T> LifecycleOwner.collectFlow(
flow: Flow<T>,
state: Lifecycle.State = Lifecycle.State.STARTED,
flowOperator: (Flow<T>) -> Flow<T> = { it },
collector: suspend (T) -> Unit
) {
val collectedFlow = flowOperator(flow)
lifecycleScope.launch {
repeatOnLifecycle(state) {
collectedFlow.collect { collector(it) }
}
}
}
Kullanım Örneği
private fun observeFlow() {
viewModel.run {
collectFlow(
flow = formDataFlow,
collector = { formData ->
binding.formListView.submitFormData(formData, listOf(0, formData.size - 1))
}
)
collectFlow(
flow = navigateBackSharedFlow,
flowOperator = { it.throttleFirst(DEFAULT_THROTTLE_TIME) },
collector = { finish() }
)
}
}
collectLatestFlow Operatörü
collectLatestFlow, Flow’ları yaşam döngüsüne duyarlı (Lifecycle Aware) bir şekilde toplamak için geliştirdiğim başka bir extension fonksiyonudur. Bu fonksiyon, en son emit edilen (yayınlanan) değeri toplar ve Flow üzerinde belirli operatörler uygulanabilir.
fun <T> LifecycleOwner.collectLatestFlow(
flow: Flow<T>,
state: Lifecycle.State = Lifecycle.State.STARTED,
flowOperator: (Flow<T>) -> Flow<T> = { it },
collector: suspend (T) -> Unit
) {
val collectedFlow = flowOperator(flow)
lifecycleScope.launch {
repeatOnLifecycle(state) {
collectedFlow.collectLatest { collector(it) }
}
}
}
Kullanım Örneği
private fun observeFlow() {
viewModel.run {
collectLatestFlow(
flow = formDataFlow,
collector = { formData ->
binding.formListView.submitFormData(formData, listOf(0, formData.size - 1))
}
)
collectLatestFlow(
flow = navigateBackSharedFlow,
flowOperator = { it.throttleFirst(DEFAULT_THROTTLE_TIME) },
collector = { finish() }
)
}
}
Avantajları
- Lifecycle Duyarlılığı: Flow’ların yaşam döngüsüne duyarlı olarak toplanmasını sağlar, böylece gereksiz kaynak tüketimini önler.
- Esneklik: flowOperator parametresi sayesinde, Flow üzerinde ek operatörler uygulanabilir.
- Kod Tekrarını Azaltır: Yaşam döngüsü yönetimini ve Flow işlemlerini tek bir fonksiyon içinde birleştirir. Bu sayede her Activity veya Fragment’ta aynı kod yapısını tekrarlamak zorunda kalmazsınız.
Kullanım Senaryoları
- Buton Tıklamaları: Yüksek frekansta/hızda tetiklenen buton tıklamalarını yönetmek.
- Navigasyon Olayları: Kullanıcının çok hızlı tepkimelerle navigasyon yapılarını birden fazla kez tetiklemesini engellemek.
- Form Girişleri: Kullanıcının çok hızlı tepkimelerle form yapılarını birden fazla kez tetiklemesini engellemek.
Hata Yönetimi
Asenkron işlemlerde hataların doğru yönetimi, uygulamanızın güvenilirliği açısından kritik öneme sahiptir. Kotlin Coroutines, CoroutineExceptionHandler aracılığıyla merkezi hata yönetimi sağlar.
CoroutineExceptionHandler Kullanımı
CoroutineExceptionHandler, coroutine içinde oluşabilecek beklenmedik hataları merkezi olarak yönetmenizi sağlar. Bu sayede, her coroutine için ayrı hata yönetimi yapmak zorunda kalmazsınız.
UseCase Sınıfında Exception Handler’ın Entegrasyonu
abstract class CoroutineRequestUseCase<Response, Params> {
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
// Merkezi hata yönetimi
Log.e("CoroutineException", "Unhandled exception: ${throwable.localizedMessage}", throwable)
// Gerekirse, hata bildirim sistemi (timber vs) entegre edilebilir
}
abstract suspend fun execute(params: Params?): Response
fun run(
scope: CoroutineScope,
observer: CoroutineObserver<Response>,
params: Params?
) {
scope.launch(exceptionHandler) {
try {
val response = withContext(Dispatchers.IO) {
execute(params)
}
withContext(Dispatchers.Main) {
observer.onSuccess(response)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
observer.onError(e)
}
}
}
}
}
Hata Yönetiminin Önemi
- Merkezi Yönetim: Hataları merkezi bir yerde yönetmek, her coroutine için ayrı hata yönetimi yapma ihtiyacını ortadan kaldırır.
- Kod Temizliği: Hata yönetimini tek bir yerde toplayarak kodun okunabilirliğini artırır.
- Güvenilirlik: Uygulamanızın beklenmedik çökmesini engelleyebilir ve hataları kullanıcıya veya loglara iletebilirsiniz.
Best Practice’ler
Kotlin Coroutines ile çalışırken, kodunuzun temiz, sürdürülebilir ve performanslı olmasını sağlamak için bazı en iyi uygulamaları takip etmek önemlidir.
1. CoroutineScope Yönetimi
- viewModelScope Kullanımı:
- Coroutine’lerin yaşam döngüsünü ViewModel’e bağlayın.
- viewModelScope, ViewModel yok edildiğinde coroutine’leri otomatik olarak iptal eder.
- Ekstra Scope Oluşturmayın:
- Özellikle, ek bir CoroutineScope oluşturmak yerine mevcut scope’ları kullanmak kodunuzu sadeleştirir ve kaynak yönetimini kolaylaştırır.
2. Dispatcher Kullanımı
- Dispatchers.IO: Ağ çağrıları ve IO işlemleri için kullanın.
- Dispatchers.Main: UI güncellemeleri için kullanın.
- withContext Kullanımı: Belirli bir dispatcher üzerinde çalışması gereken kod parçalarını withContext ile belirleyin.
3. Hata Yönetimi
- CoroutineExceptionHandler: Yakalanmamış istisnaları merkezi olarak yönetmek için kullanın.
- try-catch Blokları: Beklenen hataları yakalayarak kullanıcıya anlamlı geri bildirimler verin.
- Flow İçindeki Hatalar: Flow kullanıyorsanız, catch operatörünü kullanarak akış içindeki hataları yönetin.
4. Dependency Injection (DI)
- DI Araçları Kullanın: CoroutineScope ve Dispatcher gibi bağımlılıkları DI araçları (örneğin Hilt veya Dagger) ile enjekte edin.
- Test Edilebilirlik: DI kullanarak, testlerde coroutine’leri ve dispatcher’ları mock veya stub ile değiştirebilirsiniz.
5. Clean Architecture Uygulamaları
- UseCase’leri Suspend Fonksiyonlar Olarak Tasarlayın: İş mantığını temiz ve bağımsız hale getirin.
- Coroutine Yönetimini Üst Katmanlarda Yapın: Coroutine ve dispatcher yönetimini ViewModel gibi üst katmanlarda yaparak, kodun sorumluluklarını netleştirin.
6. Test Edilebilirlik
- Coroutine Test Kütüphaneleri: kotlinx-coroutines-test kütüphanesini kullanarak coroutine’lerinizi test edin.
- Mock ve Stub Kullanımı: Bağımlılıkları mock nesnelerle değiştirerek farklı senaryoları test edin.
Sonuç
Kotlin Coroutines, modern Android geliştirme için güçlü ve esnek bir asenkron programlama aracı sunar. RxJava’dan Coroutines’a geçiş yaparken, coroutine’lerin sunduğu basitlik ve okunabilirlik avantajlarından faydalanabilirsiniz. Doğru scope yönetimi, dispatcher kullanımı ve hata yönetimi ile kodunuzu daha temiz, sürdürülebilir ve test edilebilir hale getirebilirsiniz.

Bu yazıda, RxJava’dan Coroutines’a geçiş yaparken karşılaştığım zorlukları ve bunları nasıl aştığımı adım adım paylaştım. Projenizde bu yaklaşımları uygulayarak, daha modern ve performanslı bir kod tabanına sahip olabilirsiniz. Unutmayın, her proje ve ekip farklıdır; bu nedenle, en iyi sonucu almak için yaklaşımlarınızı kendi ihtiyaçlarınıza göre uyarlayın.
İyi Kodlamalar!
Ek Notlar ve İpuçları
- Kod İncelemesi ve Refaktörizasyon:
- Geçiş sürecinde kodunuzu adım adım refaktörize edin.
- Her değişiklikten sonra kod incelemeleri yaparak hataları erken aşamada tespit edin.
- Dokümantasyon ve Standartlar:
- Proje içinde coroutine kullanımı ile ilgili dokümantasyon oluşturun.
- Kod standartları belirleyerek, tüm ekip üyelerinin aynı yaklaşımı benimsemesini sağlayın.
- Performans İzleme:
- Coroutine’lerin performansını izleyerek, potansiyel darboğazları tespit edin.
- Dispatchers kullanımı ve scope yönetimi ile ilgili optimizasyonlar yapın.
Bu öneriler ve adımlar, RxJava’dan Kotlin Coroutines’a geçiş sürecinde karşılaşabileceğiniz zorlukları aşmanıza ve başarılı bir dönüşüm gerçekleştirmenize yardımcı olacaktır. Her adımı dikkatlice uygulayarak, asenkron programlamanın sunduğu tüm avantajlardan faydalanabilirsiniz.
Kaynaklar
- Kotlin Coroutines Resmi Dokümantasyonu
- Android Jetpack — ViewModel ve viewModelScope
- Flow API Guide
- StateFlow ve SharedFlow
- CoroutineExceptionHandler