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:
ObservableveDisposableObserverkullanarak asenkron işlemleri yönetir. - Coroutines:
suspendfonksiyonlar 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 viewModelScope kullanmak, 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 iseDispatchers.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'in yaşam döngüsü ViewModel'in scope'una bağlı olduğu için, UseCase sınıfı sadece iş mantığına odaklanır.
Güncel not (2026): Bu örnekte iki noktaya dikkat etmek iyi olur. Birincisi,
catch (e: Exception)iptal sırasında fırlatılanCancellationException'ı da yakalar. Buradaki kodda iptal edilmiş coroutine'dewithContexttekrarCancellationExceptionfırlattığı içinonErrorçağrılmaz, ama genel kural olarakCancellationExceptionyutulmamalı, yeniden fırlatılmalıdır. İkincisi,viewModelScopezatenDispatchers.Main.immediateüzerinde çalıştığı için sonuç tarafındakiwithContext(Dispatchers.Main)gerekli değildir. Bugün yaygın olan yaklaşım, use-case'i dispatcher'ını kendi içindewithContextile seçen düz birsuspendfonksiyon olarak tutmak, coroutine'i ViewModel'deviewModelScope.launchile başlatmak ve sonucuStateFlowile UI'a vermektir. Aşağıdaki Best Practice'ler bölümündeki 5. maddenin ilk önerisi de bu yönde. Dispatcher seçimini ViewModel yerine işi yapan katmanda (use-case veya repository) tutmak, o fonksiyonu her yerden güvenle çağrılabilir (main-safe) yapar.
Ö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)
}
}
}
}
Güncel not (2026): Bu implementasyon doğru çalışır ama sadeleştirilebilir.
collectdeğerleri zaten sırayla işlediği içinMutex'e vechannelFlow'a gerek yoktur; düz birflow {}yeterlidir. AyrıcaSystem.currentTimeMillis()cihaz saati değiştiğinde ileri/geri atlayabilir, süre ölçmek içinSystemClock.elapsedRealtime()gibi monoton bir saat daha güvenlidir:fun <T> Flow<T>.throttleFirst(windowDurationMillis: Long): Flow<T> = flow { var lastEmitTime: Long? = null collect { value -> val now = SystemClock.elapsedRealtime() val last = lastEmitTime if (last == null || now - last >= windowDurationMillis) { lastEmitTime = now emit(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.
Güncel not (2026): StateFlow tarafındaki sorunun somut hali şu:
throttleFirstpencere içindeki sonraki değerleri düşürür ve geri getirmez. Pencere içinde gelen son state hiç gösterilmeyebilir, UI eski bir state'te kalır. Bu yüzdenthrottleFirst'ü tıklama, navigasyon gibi tek seferlik olaylarda (SharedFlow/Channel) kullanmak, state akışlarında ise ya hiç throttle uygulamamak ya da son değeri koruyandebounce/samplegibi operatörleri tercih etmek daha güvenli.
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, Flow'un emit ettiği (yayınladığı) her değeri sırayla 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() }
)
}
}
Güncel not (2026): Bu extension'ları Fragment'ta kullanırken
LifecycleOwnerolarak Fragment'ın kendisini değilviewLifecycleOwner'ı verin (viewLifecycleOwner.collectFlow(...)). Aksi halde view yeniden oluşturulduğunda eski collector'lar devam eder ve aynı Flow birden fazla kez toplanır.
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: yeni bir değer geldiğinde önceki değer için çalışan collector bloğu iptal edilir. Flow üzerinde yine 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:
flowOperatorparametresi 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 loglama/hata raporlama aracı (Timber, Crashlytics vb.) 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)
}
}
}
}
}
Güncel not (2026):
CoroutineExceptionHandleryalnızca yakalanmamış istisnalar için ve yalnızca root coroutine'e (scope üzerinde doğrudan başlatılanlaunch'a) ya da scope'un context'ine verildiğinde çalışır. Child coroutine'lere verilen handler etkisizdir,asyncise istisnayıawait()çağrısına bırakır. Handler çalıştığında coroutine zaten bitmiştir, yani bir kurtarma mekanizması değil, son çare loglama/raporlama noktasıdır. Yukarıdaki örnektetry/catchzatenException'ları yakaladığı için handler'a yalnızca bunun dışında kalanlar düşer (örneğinonErroriçinden fırlatılan bir istisna).
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
CoroutineScopeoluş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ı
withContextile 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,
catchoperatö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 dispatcher'ları
kotlinx-coroutines-testiçindekiTestDispatcher'larla, diğer bağımlılıkları ise 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-testkü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.
Dispatcherskullanı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
Sık sorulan sorular
- Android projesinde RxJava'dan Kotlin Coroutines'a neden geçmeli?
- Coroutines daha basit ve okunabilir kod, daha az boilerplate ve native Kotlin desteği sunar. RxJava'da her use-case için tekrarlanan subscribeOn/observeOn ayarları ortadan kalktığı için kod tabanı daha sürdürülebilir ve bakımı kolay hale gelir.
- ViewModel içinde coroutine kullanırken hafıza sızıntısı nasıl önlenir?
- Coroutine'leri viewModelScope ile başlatmak yeterlidir; viewModelScope, ViewModel yok edildiğinde içindeki coroutine'leri otomatik olarak iptal eder. Ayrıca ekstra bir CoroutineScope oluşturmak yerine mevcut scope'ları kullanmak kaynak yönetimini kolaylaştırır.
- Ağ çağrıları ve UI güncellemeleri için hangi Dispatcher kullanılmalı?
- Ağ çağrıları ve disk işlemleri gibi IO yoğun işler Dispatchers.IO üzerinde, UI güncellemeleri ise Dispatchers.Main üzerinde çalıştırılmalıdır. İki dispatcher arasındaki geçiş withContext ile yapılır.
- throttleFirst Flow operatörü ne işe yarar?
- Belirli bir zaman penceresi içinde gelen ilk değeri iletir ve o süre boyunca sonraki değerleri yok sayar. Hızlı art arda butona tıklanınca bir dialog'un veya fragment'ın birden fazla açılması gibi istenmeyen yan etkileri önlemek için uygundur; değer düşürdüğü için StateFlow gibi state akışlarında dikkatli kullanılmalıdır.