• 18.08.2026 09:04:32
  • Admin Admin

Bu kotlin eğitimi rehberi, Android uygulamalarında coroutine iptalinin Repository, Flow, ViewModel ve Jetpack Compose katmanlarında nasıl ölçülebilir ve güvenli biçimde ele alınacağını inceler.

Kotlin Eğitimi: Android'de Coroutine İptalini Doğru Tasarlamak

Android eğitimi kapsamında iptalin gerçek çalışma modeli

Coroutine iptali bir thread'i zorla durdurmaz; parent Job iptal edildiğinde coroutine'in durumunu cancelling yapar ve yalnızca askıya alma noktalarında ya da açık kontrol noktalarında CancellationException fırlatır. Örneğin Retrofit'in suspend çağrıları, delay, withContext ve Flow operatörleri iptale duyarlıdır; ancak büyük bir JSON dönüşümü yapan saf CPU döngüsü değildir. Bu nedenle Repository içinde CPU-ağır eşleme varsa her anlamlı blokta ensureActive() çağırmak gerekir.

class SearchRepository(
    private val api: SearchApi
) {
    suspend fun search(query: String): List<SearchItem> =
        withContext(Dispatchers.Default) {
            val response = api.search(query) // suspend: iptale duyarlı

            response.items.mapIndexed { index, dto ->
                if (index % 128 == 0) coroutineContext.ensureActive()
                dto.toDomain()
            }
        }
}

Yaygın ve maliyetli hata, geniş bir catch (Exception) bloğunda iptali hata durumuna çevirmektir. Bu kod, kullanıcı ekranı kapattığı halde hata telemetry'si üretir ve UI'da gereksiz hata durumu gösterebilir. Ağ hatasını hedefleyerek yakalayın veya CancellationException'ı mutlaka yeniden fırlatın: catch (e: CancellationException) { throw e }. Bu ayrım, android kursu projelerinde "geri tuşa basınca istek neden hata olarak kaydediliyor?" sorununu teşhis etmek için özellikle önemlidir.

Android MVVM mimarisi içinde ViewModel ve Repository sınırı

android mvvm mimarisi kullanırken ViewModel'in viewModelScope'u ekranın yaşamından daha uzun, ViewModel'in yaşamından kısa sürer. Bu nedenle kullanıcı niyeti değiştiğinde önceki aramayı manuel Job takibiyle değil, flatMapLatest ile iptal edin. flatMapLatest, yeni sorgu geldiğinde önceki iç Flow koleksiyonunu iptal eder; eski HTTP sonucunun yeni sorgunun sonucunu ezmesini engeller.

class SearchViewModel(
    private val repository: SearchRepository
) : ViewModel() {
    private val query = MutableStateFlow("")

    val uiState: StateFlow<SearchUiState> = query
        .debounce(250)
        .map(String::trim)
        .distinctUntilChanged()
        .flatMapLatest { term ->
            if (term.length < 2) flowOf(SearchUiState.Idle)
            else flow {
                emit(SearchUiState.Loading)
                emit(SearchUiState.Data(repository.search(term)))
            }.catch { error ->
                if (error is CancellationException) throw error
                emit(SearchUiState.Error(error))
            }
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000),
            initialValue = SearchUiState.Idle
        )

    fun onQueryChanged(value: String) { query.value = value }
}

WhileSubscribed(5_000) yapılandırmasındaki beş saniye rastgele bir "performans ayarı" değildir: kısa süreli configuration değişiminde upstream'i hemen kapatıp yeniden HTTP isteği üretmemek için bir tampon sağlar. Buna karşılık upstream bir konum, socket veya pahalı veritabanı gözlemcisi açıyorsa bu süre boyunca kaynak açık kalır. Android Studio'nun Network Inspector aracında ekran döndürme sırasında önce/sonra istek sayısını karşılaştırın; aynı sorgu için iki yeni bağlantı görülüyorsa timeout veya paylaşım kapsamı yanlış seçilmiştir.

Jetpack Compose tarafında lifecycle ve tek seferlik olaylar

jetpack compose içinde collectAsState() tek başına Activity görünmezken Flow koleksiyonunu durdurmaz. Ekranda veri yenileme, konum veya socket akışı varsa lifecycle-runtime-compose paketindeki collectAsStateWithLifecycle() kullanın. Bu API, Lifecycle en az STARTED durumundayken toplar; arka plana geçince koleksiyon iptal edilir ve görünür olunca tekrar başlar.

@Composable
fun SearchRoute(
    viewModel: SearchViewModel,
    onOpenDetail: (String) -> Unit
) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()

    LaunchedEffect(Unit) {
        viewModel.events.collect { event ->
            when (event) {
                is SearchEvent.OpenDetail -> onOpenDetail(event.id)
            }
        }
    }

    SearchScreen(
        state = state,
        onQueryChanged = viewModel::onQueryChanged
    )
}

Navigation gibi tek seferlik etkileri kalıcı StateFlow içine koymak, rotation veya process restore sonrasında aynı navigasyonun yeniden çalışmasına yol açabilir. Bu amaçla ViewModel'de tampon politikası açık bir Channel tanımlayın: private val eventChannel = Channel<SearchEvent>(Channel.BUFFERED) ve UI'a receiveAsFlow() verin. Ancak collector yokken olay kaybetmenin kabul edilip edilmediğini ürün gereksinimiyle belirleyin; ödeme onayı gibi teslim garantisi gereken olaylar Channel yerine kalıcı state ve idempotent navigation anahtarı gerektirir. Bu nüans, mobil uygulama geliştirme eğitimi sırasında sıklıkla atlanan ama üretimde tekrar eden ekranlara dönüşen bir ayrıntıdır.

Kotlin kursu için iptal davranışını deterministik test etmek

İptal kodunu yalnızca emülatörde elle denemek deterministik değildir. kotlinx-coroutines-test ile runTest kullanın; gerçek zaman yerine test scheduler çalışır. Flow doğrulamasında Turbine (app.cash.turbine) ile eski sorgunun sonuç üretmediğini test edin. Testte advanceTimeBy sonrası ikinci sorguyu gönderip yalnızca ikinci sonucu beklemek, flatMapLatest zincirinin sözleşmesini doğrudan sınar.

@Test
fun `new query cancels previous search`() = runTest {
    val repository = FakeSearchRepository(
        delays = mapOf("android" to 1_000L, "kotlin" to 10L)
    )
    val viewModel = SearchViewModel(repository)

    viewModel.uiState.test {
        assertEquals(SearchUiState.Idle, awaitItem())
        viewModel.onQueryChanged("android")
        advanceTimeBy(250)
        assertEquals(SearchUiState.Loading, awaitItem())

        viewModel.onQueryChanged("kotlin")
        advanceTimeBy(260)
        assertEquals(SearchUiState.Loading, awaitItem())
        advanceUntilIdle()

        assertEquals(SearchUiState.Data(listOf(SearchItem("kotlin"))), awaitItem())
        cancelAndIgnoreRemainingEvents()
    }
    assertTrue("android" in repository.cancelledQueries)
}

Bu testi güvenilir yapan ayrıntı, fake Repository'nin delay sırasında CancellationException'ı yakalayıp cancelledQueries'ne yazması ve sonra istisnayı yeniden fırlatmasıdır. Sadece son UI durumunu doğrulamak yeterli değildir: kötü bir implementasyon eski isteği arka planda tamamlatıp sonucu filtreleyebilir; UI doğru görünür ama radyo, sunucu kotası ve telemetry gereksiz çalışır. android studio eğitimi pratiğinde aynı senaryoyu Network Inspector ile cihazda da kontrol edin: hızlı iki sorguda iptal edilen çağrının bağlantı kapanışı ile ikinci çağrının başlangıcını karşılaştırın.

Play Store yayınlama öncesi iptal ve hata gözlemi

play store yayınlama öncesinde release varyantında R8 etkin olduğundan, coroutine kaynaklı üretim çöküşlerini okunabilir tutmak için mapping dosyasını CI artefact'ı olarak saklayın. Gradle tarafında minifyEnabled true kullanıyorsanız Play Console'a mapping yüklenmesini sürüm hattının zorunlu adımı yapın; aksi halde CancellationException'dan farklı gerçek bir hata obfuscate edilmiş stack trace ile gelir ve Repository/ViewModel sınırını ayırt etmek uzar.

android {
  buildTypes {
    release {
      minifyEnabled true
      shrinkResources true
      proguardFiles(
        getDefaultProguardFile("proguard-android-optimize.txt"),
        "proguard-rules.pro"
      )
    }
  }
}

Crash raporlamasında iptali hata olarak göndermeyin. Örneğin Sentry veya Firebase Crashlytics'e exception kaydetmeden önce if (throwable is CancellationException) return filtresi koyun; fakat bu filtreyi top-level exception handler'a körlemesine eklemeyin. Bir CancellationException başka bir programlama hatasını sarmalıyorsa neden zincirini loglamak gerekebilir. Üretimde ayrıca arama isteği başına iptal/başarı oranını ayrı metrik olarak gönderin; ani iptal artışı çoğu zaman kullanıcı davranışından değil, yanlışlıkla yeniden compose edilen ve tekrar tetiklenen LaunchedEffect anahtarından kaynaklanır.

Sık Sorulan Sorular

Android eğitimi projelerinde CancellationException neden yeniden fırlatılmalıdır?

Çünkü coroutine iptali normal hata akışı değildir. Repository'de genel catch bloğu bunu UI hata durumuna veya Crashlytics olayına çevirirse ekran kapanması ve yeni arama başlatılması yanlış negatif telemetry üretir. catch (e: CancellationException) { throw e } ile parent Job'ın iptal zincirini koruyun.

Jetpack Compose ile collectAsStateWithLifecycle ne zaman kullanılmalı?

UI tarafından tüketilen ve ekran görünmezken çalışması gerekmeyen Flow'larda kullanın. Gradle'a androidx.lifecycle:lifecycle-runtime-compose ekleyip val state by flow.collectAsStateWithLifecycle() çağırın. Socket veya polling içeren upstream için Android Studio Network Inspector'da arka plana geçişten sonra isteğin kesildiğini doğrulayın.

Android MVVM mimarisi içinde arama isteği nasıl iptal edilir?

Kullanıcı sorgusunu MutableStateFlow içinde tutun; debounce, distinctUntilChanged ve flatMapLatest zinciri kurun. Yeni değer geldiğinde flatMapLatest eski iç Flow'u iptal eder. Repository tarafında Retrofit suspend çağrısını sarmalayan catch bloğunun CancellationException'ı yutmaması gerekir.

Kotlin kursu sırasında coroutine iptal testi nasıl yazılır?

kotlinx-coroutines-test ile runTest, Flow çıktıları için Turbine kullanın. İlk isteği gecikmeli, ikinci isteği kısa gecikmeli fake ile çalıştırın; advanceTimeBy ve advanceUntilIdle sonrasında yalnızca ikinci sonucun yayıldığını, ilkinin ise fake katmanda iptal edildiğini assert edin.

AI / LLM Discovery

Bu makale Opendart Akademi Android eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.

Opendart Akademi llms.txt