• 4.09.2026 09:06:15
  • Admin Admin

Jetpack Compose ekranlarında arka plan yazımları, snapshotFlow geri beslemeleri ve effect iptalleri sessiz durum hataları üretebilir. Bu android eğitimi rehberi, hatayı reducer, test ve sistem iziyle izole eder.

Jetpack Compose'da Snapshot Yarışlarını Test, Trace ve MVVM ile Yönetmek

Jetpack Compose ve android mvvm mimarisi: Tek yazarlı UI durumu kurun

Compose'un MutableState nesneleri snapshot kaydı tutar. Bir IO coroutine'i aynı anda state yazarken composition o kaydı okursa, özellikle birden fazla mutable snapshot birleştirildiğinde apply conflict veya ekranda eski verinin yeniden görünmesi görülebilir. Bunu ViewModel katmanında Compose state saklamak yerine immutable StateFlow ve atomik update ile tek yazarlı bir reducer'a taşıyın. StateFlow'un update işlemi compare-and-set döngüsüyle çalışır; iki dispatcher aynı anda aksiyon gönderdiğinde son yazan diğer alanları ezmez.

data class ProfileUiState(
    val loading: Boolean = false,
    val name: String = "",
    val error: String? = null
)

class ProfileViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(ProfileUiState())
    val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()

    fun refresh() = viewModelScope.launch {
        _uiState.update { it.copy(loading = true, error = null) }
        runCatching { repository.loadProfile() }
            .onSuccess { profile ->
                _uiState.update {
                    it.copy(loading = false, name = profile.name)
                }
            }
            .onFailure { throwable ->
                _uiState.update {
                    it.copy(loading = false, error = throwable.message)
                }
            }
    }
}

@Composable
fun ProfileRoute(viewModel: ProfileViewModel) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()
    ProfileScreen(state = state, onRefresh = viewModel::refresh)
}

Buradaki kritik ayrıntı, repository sonucunu Dispatchers.IO üzerinde almış olsanız bile MutableStateFlow.update çağrısının Compose snapshot sistemine yazmamasıdır. UI tarafında collectAsStateWithLifecycle() sonucu Compose state'e köprülenir ve lifecycle STOPPED iken collector iptal edilir. collectAsState() kullanıp ekran görünmezken ağ akışını toplamaya devam etmek, navigation back stack'te gereksiz state güncellemeleri ve yeniden composition üretir. Bu ayrım, android mvvm mimarisi içinde UI state ile Compose'un render state'ini aynı nesne sanma hatasını engeller.

snapshotFlow geri beslemesini ölçülebilir bir akışa dönüştürün

snapshotFlow, bloğun içinde okunan snapshot state değiştiğinde yeniden hesaplanır ve sonucu eşit değilse Flow öğesi üretir. Yaygın hata, collector içinde tekrar aynı state'i yazmaktır: örneğin scroll indeksini okuyup collector'dan doğrudan listState.scrollToItem(0) çağırmak yeni bir snapshot invalidation zinciri başlatabilir. Okuma ile komutu ayırın; Flow yalnızca niyet üretsin, scroll komutunu ayrı ve sınırlı bir effect uygulasın.

@Composable
fun FeedRoute(viewModel: FeedViewModel) {
    val listState = rememberLazyListState()

    LaunchedEffect(listState) {
        snapshotFlow { listState.firstVisibleItemIndex }
            .map { index -> index >= 8 }
            .distinctUntilChanged()
            .collect { pastThreshold ->
                viewModel.dispatch(FeedAction.ScrollThresholdChanged(pastThreshold))
            }
    }

    val state by viewModel.uiState.collectAsStateWithLifecycle()
    LaunchedEffect(state.pendingScrollTarget) {
        state.pendingScrollTarget?.let { target ->
            listState.animateScrollToItem(target)
            viewModel.dispatch(FeedAction.ScrollConsumed)
        }
    }
}

Bu yapıda LaunchedEffect(state.pendingScrollTarget) anahtarının neden gerekli olduğunu kontrol edin: hedef değiştiğinde eski animasyon coroutine'i iptal edilir, yeni hedef tek coroutine ile uygulanır. Anahtarı Unit yapıp state'i içeriden okumak, yeni hedef geldiğinde eski collector'ın güncel olmayan değeri kullanmasına yol açabilir. Android eğitim içeriklerinde sık atlanan başka ayrıntı da şudur: snapshotFlow { listState.layoutInfo } gibi büyük ve sık değişen bir nesneyi doğrudan emit etmek allocation baskısı yaratır. Bunun yerine yalnızca gereken primitive değeri, örneğin firstVisibleItemIndex veya görünür öğe sayısını okuyun.

Kotlin eğitimi için reducer yarışlarını sanal zamanla test edin

Yarış hatasını gerçek cihazda parmakla tekrar etmeye çalışmak yerine kotlinx-coroutines-test ile dispatcher'ı kontrol edin. Reducer'ı ViewModel'den bağımsız bir sınıfa çıkarmak, iki aksiyonun sırasını testte deterministik hale getirir. Aşağıdaki testte Turbine'in ilk state'i hemen vermesi beklenir; ardından yükleme sonucu geldikten sonra daha eski bir hata aksiyonunun state'i bozup bozmadığı açıkça doğrulanır.

class FeedStore(private val scope: CoroutineScope) {
    private val _state = MutableStateFlow(FeedUiState())
    val state = _state.asStateFlow()

    fun dispatch(action: FeedAction) {
        _state.update { previous ->
            when (action) {
                FeedAction.LoadStarted -> previous.copy(loading = true, error = null)
                is FeedAction.LoadSucceeded -> previous.copy(
                    loading = false, items = action.items, requestId = action.requestId
                )
                is FeedAction.LoadFailed -> if (action.requestId == previous.requestId) {
                    previous.copy(loading = false, error = action.message)
                } else previous
            }
        }
    }
}

@Test
fun stale_failure_does_not_replace_newer_result() = runTest {
    val store = FeedStore(this)

    store.state.test {
        assertEquals(FeedUiState(), awaitItem())
        store.dispatch(FeedAction.LoadSucceeded(requestId = 2, items = listOf("A")))
        assertEquals(listOf("A"), awaitItem().items)
        store.dispatch(FeedAction.LoadFailed(requestId = 1, message = "timeout"))
        expectNoEvents()
    }
}

Buradaki requestId kontrolü sadece test kolaylığı değildir. HTTP isteği iptal edilse bile bazı callback veya cache katmanları sonucu geç teslim edebilir; eski sonucun yeni state'i ezmesini reducer seviyesinde engellersiniz. runTest içinde gecikmeli bir fake kullanıyorsanız advanceUntilIdle() çağırın; Thread.sleep test dispatcher'ını ilerletmez. Bu teknik, kotlin eğitimi veya kotlin kursu kapsamında coroutine anlatımının ötesine geçip gerçek UI state tutarlılığını test etmenin pratik yoludur.

Android Studio eğitimi: Snapshot maliyetini Perfetto ile önce-sonra karşılaştırın

Snapshot kaynaklı sorunun performans etkisini tahmin etmeyin. Android Studio CPU Profiler'da System Trace kaydı alın veya cihazdan Perfetto ile trace toplayın. Uygulama modülüne androidx.compose.runtime:runtime-tracing bağımlılığını ekleyin; ardından kritik reducer ve liste hesaplarını androidx.tracing.trace ile etiketleyin. Etiketler trace üzerinde event ile recomposition zamanını aynı zaman çizelgesinde ilişkilendirmenizi sağlar.

dependencies {
    implementation("androidx.compose.runtime:runtime-tracing")
    implementation("androidx.tracing:tracing-ktx")
}

fun reduce(action: FeedAction) {
    trace("FeedStore.reduce:${action::class.simpleName}") {
        _state.update { old -> reducer(old, action) }
    }
}

// Fiziksel cihazda 10 saniyelik sistem izi:
// adb shell perfetto -o /data/misc/perfetto-traces/feed.pftrace -t 10s sched freq gfx view am wm -a com.example.app
// adb pull /data/misc/perfetto-traces/feed.pftrace .

Önce sorunlu sürümde 30 aynı kaydırma senaryosu çalıştırın ve trace'ten üç sayıyı kaydedin: FeedStore.reduce toplam çağrı sayısı, en uzun tek çağrı süresi ve frame zaman çizelgesinde 16.6 ms'yi aşan frame sayısı. Sonra layoutInfo okumayı primitive scroll eşiğine indiren sürümle aynı senaryoyu çalıştırın. Sadece ortalamayı karşılaştırmayın; kısa bir ortalama, nadir fakat görünür uzun frame'leri gizler. Bu önce-sonra düzeni, android studio eğitimi içinde profiler ekranını gezmekten daha değerlidir çünkü kod değişikliğini doğrudan bir trace metriğine bağlar.

Mobil uygulama geliştirme eğitimi ve play store yayınlama öncesi release doğrulaması

Debug derlemede doğru görünen Compose davranışı release'de farklılaşabilir; R8, farklı timing ve gerçek lifecycle geçişleri effect iptallerini görünür hale getirir. Play Store yayınlama öncesinde imzalı bundle'ı cihazda bundletool ile kurun, ardından aynı instrumented test paketini release varyantına karşı koşturun. Bu işlem, store'a yüklenmeden önce split APK seçimi ve release manifest'iyle gerçek kurulum yolunu sınar.

./gradlew :app:bundleRelease
bundletool build-apks   --bundle app/build/outputs/bundle/release/app-release.aab   --output app-release.apks   --connected-device
bundletool install-apks --apks app-release.apks
./gradlew :app:connectedReleaseAndroidTest

Test senaryosunda ekranı açın, yüklemeyi başlatın, hemen başka ekrana gidin ve geri dönün; ardından fake repository'nin eski ve yeni sonuçlarını ters sırada yayınlayın. Assertion, ekranda yalnızca en yüksek requestId ile gelen listenin görünmesi olmalıdır. Bu kontrollü senaryo, mobil uygulama geliştirme eğitimi alan ekiplerin sadece mutlu yol screenshot'ına güvenmesini önler. Bir android kursu veya android eğitim programında bu testi CI'a eklemek için komutu ayrı bir release smoke job olarak çalıştırın; debug testinin geçmesi release davranışını kanıtlamaz.

Sık Sorulan Sorular

android eğitimi sırasında Jetpack Compose snapshot hatası nasıl ayıklanır?

Önce UI'ya yazan noktaları arayın: ViewModel'de MutableState yerine StateFlow kullanın ve Compose state yazımını composable collector'a sınırlayın. Android Studio System Trace veya Perfetto'da reducer için trace etiketi ekleyin; aynı kullanıcı aksiyonunda kaç reducer çağrısı ve kaç uzun frame oluştuğunu ölçün.

android studio eğitimi için snapshotFlow kullanırken hangi hata sık görülür?

snapshotFlow bloğunda okunan aynı state'i collector içinde yazmak geri besleme döngüsü oluşturabilir. snapshotFlow'dan yalnızca primitive bir niyet, örneğin indeks eşiği üretin; scroll veya state değiştiren komutu ayrı bir LaunchedEffect içinde requestId ya da tüketilmiş event kontrolüyle uygulayın.

kotlin kursu projelerinde eski ağ sonucu yeni UI state'i nasıl ezmez?

Her isteğe artan bir requestId verin ve reducer'da hata veya başarı sonucunu mevcut requestId ile karşılaştırın. kotlinx-coroutines-test ve Turbine ile önce requestId 2 başarısını, sonra requestId 1 hatasını yayınlayın; ikinci olaydan sonra Flow'un yeni state üretmediğini expectNoEvents ile doğrulayın.

play store yayınlama öncesinde Compose state yarışları release derlemede nasıl test edilir?

Release AAB'yi bundletool build-apks ve install-apks ile bağlı cihaza kurun, sonra connectedReleaseAndroidTest çalıştırın. Testte navigation ile effect iptalini tetikleyin ve fake repository sonuçlarını ters sırada döndürerek eski sonucun güncel requestId state'ini değiştirmediğini assertion ile kontrol 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