• 23.08.2026 09:11:54
  • Admin Admin

Kotlin Flow akışlarını ViewModel içinde doğru paylaşmayı, tekrar eden ağ çağrılarını Perfetto ile ölçmeyi ve Jetpack Compose UI durumunu lifecycle güvenli toplamayı öğrenin.

Kotlin Eğitimi: Android鈥檇e Flow Paylaşımı ve UI Durum Hataları

Android eğitimi için problem: Cold Flow birden fazla kez çalışır

Bir android eğitimi veya android kursu kapsamında Flow genellikle yalnızca "asenkron veri akışı" olarak anlatılır; üretimde kritik ayrıntı ise Flow'un varsayılan olarak cold olmasıdır. Aşağıdaki repository fonksiyonunu iki ayrı Compose bileşeni toplarsa, her collector yeni bir upstream koleksiyonu başlatır. Bu örnekte iki ayrı REST isteği, iki JSON parse işlemi ve Room gözlemi oluşur.

class ProductRepository(
    private val api: ProductApi,
    private val dao: ProductDao
) {
    fun observeProducts(): Flow<List<Product>> = flow {
        val remote = api.getProducts() // Her collect çağrısında tekrar çalışır
        dao.replaceAll(remote.map(ProductDto::toEntity))
        emitAll(dao.observeAll().map { entities ->
            entities.map(ProductEntity::toDomain)
        })
    }
}

Bu davranışı tahmin etmek yerine doğrulayın. Debug varyantında OkHttp'ye bir interceptor ekleyip istek sayısını kaydedin; aynı ekranda iki collector varken, ekranı arka plana alıp geri getirdikten sonra URL başına çağrı sayısını karşılaştırın. Ağ çağrısının iki kez görünmesi, Compose'un hatası değil, cold upstream'in iki kez collect edilmesidir.

class RequestCountingInterceptor : Interceptor {
    private val counts = ConcurrentHashMap<String, AtomicInteger>()

    override fun intercept(chain: Interceptor.Chain): Response {
        val url = chain.request().url.encodedPath
        counts.getOrPut(url) { AtomicInteger() }.incrementAndGet()
        return chain.proceed(chain.request())
    }

    fun count(path: String): Int = counts[path]?.get() ?: 0
}

Özellikle `flatMapLatest`, `combine` ve `map` zincirlerinin sonunda cold bir Flow döndürmek masum değildir: her collector bu zincirin tamamını yeniden kurar. Room'un `Flow` dönüşü de cold'dur; SQL sorgusu observer başına tekrar kaydedilir. Bu nedenle paylaşım kararı, yalnızca ağ katmanı için değil, pahalı dönüşüm ve birden fazla veritabanı gözlemcisi içeren akışlar için de verilmelidir.

Kotlin kursu pratiği: stateIn ile tek upstream, güvenli UI state

kotlin eğitimi içeriğinde uygulanabilir varsayılan, ekranın kalıcı durumunu ViewModel'de `StateFlow` olarak üretmektir. `stateIn`, birden fazla UI collector'ını tek upstream aboneliğinde birleştirir ve `initialValue` sayesinde yükleme durumunu UI'a deterministik verir. Hata yakalamayı `stateIn` öncesinde yapmak önemlidir; aksi h芒lde upstream exception paylaşım coroutine'ini iptal eder ve yeni subscriber'lar güncel durum alamaz.

data class ProductsUiState(
    val loading: Boolean = true,
    val items: List<Product> = emptyList(),
    val error: String? = null
)

class ProductsViewModel(
    repository: ProductRepository
) : ViewModel() {
    val uiState: StateFlow<ProductsUiState> = repository.observeProducts()
        .map<List<Product>, ProductsUiState> { ProductsUiState(items = it, loading = false) }
        .catch { error ->
            emit(ProductsUiState(loading = false, error = error.message))
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000),
            initialValue = ProductsUiState()
        )
}

`SharingStarted.WhileSubscribed(5_000)` ekran rotasyonu, kısa navigation geçişi veya split-screen odak değişimi sırasında upstream'i hemen kapatmaz. Ancak bu değer veri tazeliği politikası değildir: kullanıcı ekrandan ayrıldıktan sonra beş saniye boyunca WebSocket, konum güncellemesi ya da pahalı polling devam edebilir. Kamera preview veya sürekli GPS gibi kaynaklarda `stopTimeoutMillis = 0` seçin; Room sorgusu ve ekranlar arası kısa geçişlerde ise ölçerek küçük bir gecikme kullanın.

Bir başka edge case `replayExpirationMillis` değeridir. Varsayılan olarak son değer bellek içinde korunur. Kullanıcı hesabı değiştiğinde eski kullanıcının ürün listesinin kısa süreliğine görünmesi kabul edilemezse, kullanıcı değişiminde ViewModel'i temizlemek veya upstream'i `flatMapLatest` ile aktif kullanıcı kimliğine bağlamak gerekir. Sadece `replayExpirationMillis = 0` vermek, aktif subscriber varken eski verinin yayılmasını çözmez.

Jetpack Compose鈥檇a lifecycle güvenli Flow toplama

jetpack compose tarafında `collectAsState()` yerine Android UI için `collectAsStateWithLifecycle()` kullanın. Bu API, collector'ı varsayılan `Lifecycle.State.STARTED` eşiğinde durdurur; arka plandaki bir Activity görünür UI üretmediği h芒lde animasyon, mapping veya ağ sonucu render maliyeti oluşturmaz. Gerekli bağımlılık `androidx.lifecycle:lifecycle-runtime-compose` paketidir.

@Composable
fun ProductsRoute(
    viewModel: ProductsViewModel = hiltViewModel()
) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()

    when {
        state.loading -> CircularProgressIndicator()
        state.error != null -> ErrorPanel(message = state.error)
        else -> ProductsList(products = state.items)
    }
}

Composable içinde doğrudan `repository.observeProducts().collectAsState(...)` çağırmak iki nedenle yanlıştır: repository'nin cold akışını composition'a taşır ve ekranın iki alt bileşeni aynı veriyi istediğinde upstream paylaşımını kaybedersiniz. Ayrıca parametre değişiminde yeniden başlatılması gereken işleri `LaunchedEffect(productId)` ile sınırlayın; anahtarsız `LaunchedEffect(Unit)` farklı ürün kimliği geldiğinde eski işi taşımaya devam eder.

Tek seferlik snackbar veya navigation olaylarını `StateFlow` içine koymayın. State yeniden collect edildiğinde replay edilebilir; process recreation sonrası beklenmeyen navigation üretir. Bunun yerine `Channel` ve `receiveAsFlow()` kullanın. `Channel.BUFFERED` kullanıcı aksiyonu için uygundur, fakat göndericinin sonucu garanti etmez: ViewModel kapanırken `trySend` başarısız olabilir; kritik ödeme veya form gönderme sonucunu kalıcı state ve idempotency anahtarıyla modelleyin.

private val _events = Channel<ProductsEvent>(Channel.BUFFERED)
val events = _events.receiveAsFlow()

fun retry() {
    viewModelScope.launch {
        _events.send(ProductsEvent.ScrollToTop)
    }
}

@Composable
fun ProductsEvents(viewModel: ProductsViewModel) {
    val snackbarHostState = remember { SnackbarHostState() }
    LaunchedEffect(viewModel) {
        viewModel.events.collect { event ->
            if (event is ProductsEvent.Message) {
                snackbarHostState.showSnackbar(event.text)
            }
        }
    }
}

Android MVVM mimarisi akışlarını Perfetto ile önce-sonra ölçmek

android mvvm mimarisi içinde `stateIn` eklemenin etkisini "daha akıcı hissettiriyor" diye değerlendirmeyin. Önce iki ayrı UI collector ile debug build çalıştırın, aynı navigation senaryosunu en az 10 kez tekrarlayın ve `RequestCountingInterceptor` ile `/products` çağrı sayısını kaydedin. Ardından repository Flow'unu ViewModel'de `stateIn` ile paylaşın, aynı cihazda aynı senaryoyu tekrar çalıştırın. Beklenen fark, collector başına istek yerine görünür ekran oturumu başına tek upstream isteğidir.

CPU tarafını görmek için Android Studio'dan Profiler > System Trace ile Perfetto kaydı alın. Kayıtta `sched`, `freq`, `binder_driver`, `view` ve uygulama atrace kategorilerini seçin; mapping bölümünü `Trace` ile işaretleyin. Önce-sonra kaydında `products_map` slice sayısını, ana iş parçacığındaki slice süresini ve aynı zaman aralığındaki OkHttp callback sayısını karşılaştırın. Sadece frame süresine bakmak, ağ tekrarını maskeleyebilir.

fun List<ProductEntity>.toDomainTraced(): List<Product> =
    Trace.beginSection("products_map").let {
        try {
            map { entity ->
                Product(id = entity.id, title = entity.title, price = entity.price)
            }
        } finally {
            Trace.endSection()
        }
    }

Bu ölçümde yaygın hata release benzeri R8 yapılandırması olmadan sonuç çıkarmaktır. Debug build'de debugger, loglama interceptor'ı ve doğrulama kontrolleri CPU süresini değiştirir. Davranış hatasını debug'da doğrulayın; süre kıyasını minify etkin, aynı cihaz ve ısıl olarak benzer koşullardaki bir benchmark varyantında yapın. UI açılışını ölçmeniz gerekiyorsa Macrobenchmark'ın `StartupMode.COLD` modu kullanılabilir; Flow paylaşımını izole etmek için ise navigation sonrası sabit bir kullanıcı senaryosu daha anlamlıdır.

Android Studio eğitimi: Flow sözleşmesini test edin ve yayın öncesi koruyun

Bir android studio eğitimi laboratuvarında bu davranışı test edilebilir h芒le getirmek için `kotlinx-coroutines-test` ile sanal zaman kullanın. `WhileSubscribed(5_000)` gerçek zamanla test edilirse CI'da yavaş ve kararsız olur. `runTest`, `advanceTimeBy` ve sahte repository ile collector ayrıldıktan sonra upstream'in ne zaman iptal edildiğini doğrulayın.

@Test
fun upstream_stops_after_timeout_when_no_subscribers_remain() = runTest {
    val upstream = MutableSharedFlow<Int>()
    val state = upstream.stateIn(
        scope = backgroundScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = -1
    )

    val job = launch { state.collect() }
    runCurrent()
    job.cancel()

    advanceTimeBy(4_999)
    // Upstream'in kapanmadığını fake source üzerindeki cancel sayısıyla doğrulayın.
    advanceTimeBy(1)
    // Bu noktada fake source cancellation beklenir.
}

mobil uygulama geliştirme eğitimi veren ekiplerde bu kuralı kod inceleme kontrol listesine eklemek faydalıdır: UI'a giden kalıcı veri `StateFlow`, tek seferlik efekt `Channel` veya dikkatle yapılandırılmış `SharedFlow`, repository'den çıkan pahalı cold akış ise ViewModel sınırında paylaşılmış olmalıdır. Bu ayrım, testte state assertion ile event assertion'ın birbirine karışmasını da engeller.

play store yayınlama öncesi internal test kanalında Firebase Performance Monitoring veya kendi ağ metriklerinizle endpoint başına istek sayısını sürüm bazında izleyin. `http.client.duration` benzeri bir süre metriği tek başına yeterli değildir; `/products` çağrı adedini kullanıcı oturumu ve ekran görüntülenmesiyle normalize edin. Yeni bir Compose alt bileşeni ikinci collector eklediğinde, p95 süre değişmese bile istek oranındaki artış erken uyarı verir.

Sık Sorulan Sorular

Kotlin eğitimi kapsamında stateIn mi shareIn mi kullanmalıyım?

Ekranın her anda çizilebilir bir değere ihtiyacı varsa `stateIn` kullanın; `initialValue` zorunluluğu UI loading/error/empty durumunu açıkça modelletir. Değeri olmayan olay akışlarında `shareIn` kullanılabilir, ancak `replay`, buffer kapasitesi ve subscriber yokken olay kaybı davranışını açıkça seçmeniz gerekir.

Jetpack Compose collectAsStateWithLifecycle ağ çağrılarını tek başına azaltır mı?

Hayır. Bu API collector'ı lifecycle STARTED altında durdurur, fakat aynı cold Flow'u iki composable topluyorsa iki upstream koleksiyonu oluşur. Tekilleştirme için Flow'u ViewModel içinde `stateIn(viewModelScope, SharingStarted.WhileSubscribed(...), initialValue)` ile paylaşın ve OkHttp interceptor sayacıyla çağrı adedini doğrulayın.

Android MVVM mimarisi içinde SharedFlow ile navigation event göndermek güvenli mi?

Yapılandırmaya bağlıdır. `MutableSharedFlow(replay = 0)` collector yokken olayı kaybedebilir; `replay = 1` ise yeni collector'ın eski navigation olayını tekrar işlemesine yol açabilir. Tek tüketicili UI efektleri için `Channel.BUFFERED` ve `receiveAsFlow()` daha açık bir sözleşmedir; kritik iş sonuçlarını ise kalıcı UI state ile modelleyin.

Android kursu projelerinde WhileSubscribed timeout kaç milisaniye olmalı?

Sabit bir evrensel değer yoktur. Önce Perfetto ve request counter ile rotasyon, bottom navigation ve kısa geri dönüş senaryolarını ölçün. Room tabanlı liste ekranında 5 saniye kısa geçişlerde yeniden aboneliği azaltabilir; GPS, kamera veya WebSocket gibi kaynaklarda 0 ms ile anında kapatma daha güvenlidir.

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