Jetpack Compose kullanan büyük listelerde Paging 3 ve RemoteMediator maliyetini Macrobenchmark, Perfetto ve Room sorgu kayıtlarıyla ölçün. Bu android eğitim rehberi, ağ ve veritabanı yüklerini ayrıştırır.
Android Eğitimi: Paging 3 RemoteMediator ile Liste Yükünü Ölçmek
Kotlin eğitimi kapsamında Paging 3 yük sınırlarını tasarlamak
Paging 3 sorunlarının çoğu ağ gecikmesinden değil, tek bir sayfanın Room'a nasıl yazıldığı ve UI'nın kaç öğeyi aynı anda istediğinden çıkar. Önce API sayfa boyutu, Room transaction boyutu ve PagingConfig.pageSize değerini ayrı değişkenler kabul edin. Örneğin sunucu 50 kayıt döndürürken istemcide pageSize=20 kullanmak geçerlidir; ancak RemoteMediator tek ağ cevabını üç ayrı database write dalgasına bölüyorsa disk I/O ve invalidation sayısı artar. Bu ilişkiyi görünür kılmak için Room'da query callback açın ve debug derlemesinde sorgu sayısını Logcat'e yazdırın.
val db = Room.databaseBuilder(context, AppDb::class.java, "catalog.db")
.setQueryCallback(
{ sql, args -> Log.d("RoomSql", "$sql args=$args") },
Executors.newSingleThreadExecutor()
)
.build()
val pagingConfig = PagingConfig(
pageSize = 30,
initialLoadSize = 60,
prefetchDistance = 10,
enablePlaceholders = false,
maxSize = 180
)maxSize için sık yapılan hata, değeri pageSize ile uyumsuz seçmektir. Paging, sayfaları düşürürken prefetchDistance ve görünür pencereyi korumaya çalışır; çok küçük maxSize, kullanıcı iki yönlü kaydırdığında aynı sayfanın tekrar ağdan istenmesine yol açabilir. Gerçek katalog verinizle 30/60/180 ve 50/100/300 gibi iki konfigürasyonu karşılaştırın. Her koşulda 200 satır aşağı, 100 satır yukarı kaydırın; RoomSql logundan SELECT sayısını, ağ interceptor'ından HTTP istek sayısını ve Perfetto'dan main thread frame süresini ayrı kaydedin.
Android MVVM mimarisi içinde atomik RemoteMediator yazımı
android mvvm mimarisi katmanlarında RemoteMediator'ı ViewModel'e koymak yerine repository sınırında üretin: ViewModel yalnızca PagingData akışını tüketmelidir. Kritik nokta, sayfa kayıtları ile o sayfaya ait remote key bilgisinin aynı Room transaction içinde yazılmasıdır. Aksi halde uygulama yazma sırasında kapanırsa kayıtlar var, key yok durumuna düşer; sonraki APPEND çağrısı aynı cursor ile tekrar veri indirir veya yanlış sayfaya atlar.
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, ProductEntity>
): MediatorResult {
val cursor = when (loadType) {
LoadType.REFRESH -> null
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> db.remoteKeyDao().lastCursor() ?: return MediatorResult.Success(true)
}
return try {
val response = api.products(cursor = cursor, limit = state.config.pageSize)
db.withTransaction {
if (loadType == LoadType.REFRESH) {
db.productDao().clearAll()
db.remoteKeyDao().clearAll()
}
db.productDao().upsertAll(response.items.map(ProductDto::toEntity))
db.remoteKeyDao().insert(RemoteKey(id = "products", nextCursor = response.nextCursor))
}
MediatorResult.Success(endOfPaginationReached = response.nextCursor == null)
} catch (e: IOException) {
MediatorResult.Error(e)
}
}Buradaki edge case, boş bir APPEND cevabının her zaman listenin sonu olmamasıdır. Cursor tabanlı API, filtre indeksi gecikmeli güncelleniyorsa geçici olarak boş dizi ve null olmayan nextCursor döndürebilir. API sözleşmesi bunu mümkün kılıyorsa yalnızca nextCursor == null ile sonuca karar verin; items.isEmpty() ile karar vermek yeni kayıtların sonraki cursor'da kalmasına neden olur. Bu davranışı MockWebServer ile iki ardışık boş olmayan cursor cevabı tanımlayarak repository testinde doğrulayın.
Jetpack Compose liste yükünü Android Studio eğitimi araçlarıyla profile etmek
Ölçümden önce UI thread, binder, SQLite ve ağ süresini tek bir sayı altında toplamayın. Android Studio Profiler içindeki System Trace kaydı veya doğrudan Perfetto, kaydırma sırasında hangi dilimin frame bütçesini aştığını gösterir. RemoteMediator etrafına Trace bölümleri koyun; bu bölümler Perfetto zaman çizelgesinde HTTP bekleme süresini Room transaction süresinden ayırır. Release'e yakın bir build kullanın, çünkü debug build'deki ek denetimler ve loglama frame zamanlarını yanıltır.
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, ProductEntity>
): MediatorResult = trace("products_mediator_$loadType") {
val response = trace("products_http") {
api.products(cursor = cursorFor(loadType), limit = state.config.pageSize)
}
trace("products_room_write") {
db.withTransaction {
db.productDao().upsertAll(response.items.map(ProductDto::toEntity))
db.remoteKeyDao().insert(RemoteKey("products", response.nextCursor))
}
}
MediatorResult.Success(response.nextCursor == null)
}Önce-sonra karşılaştırmasında aynı cihaz imajı, aynı seed veri ve aynı kaydırma senaryosu kullanılmalıdır. Örneğin ilk sürümde her ProductEntity için ayrı insert çağrısı varsa, DAO'yu tek parametreli çağrılardan liste alan @Upsert suspend fun upsertAll(items: List<ProductEntity>) imzasına değiştirin. Ardından Perfetto SQL ile products_room_write slice sürelerinin P50 ve P95 değerlerini kıyaslayın. Örnek sorgu:
SELECT name, COUNT(*) AS n,
AVG(dur) / 1e6 AS avg_ms,
MAX(dur) / 1e6 AS max_ms
FROM slice
WHERE name = 'products_room_write'
GROUP BY name; Sadece ortalamayı raporlamayın; kullanıcıya görünen takılmalar genellikle P95 veya maksimum transaction süresinde ortaya çıkar.Jetpack Compose tarafında yük durumunu ve anahtarları doğru bağlamak
Jetpack Compose ile PagingData bağlanırken her satıra indeks tabanlı key vermek, REFRESH sonrası sıralama değiştiğinde item state'inin yanlış ürüne taşınmasına yol açar. Sunucunun değişmez kimliğini key olarak kullanın ve yük durumunu yalnızca append için değil refresh için de görünür yapın. Ayrıca LazyColumn içinde item sayısı sıfırken append loading göstermek anlamsızdır; ilk yükte refresh.loadState kontrol edilmelidir.
@Composable
fun ProductScreen(viewModel: ProductViewModel) {
val products = viewModel.products.collectAsLazyPagingItems()
LazyColumn {
items(
count = products.itemCount,
key = products.itemKey { it.id },
contentType = products.itemContentType { "product" }
) { index ->
products[index]?.let { ProductRow(it) }
}
when (val append = products.loadState.append) {
is LoadState.Loading -> item(key = "append_spinner") { LoadingRow() }
is LoadState.Error -> item(key = "append_retry") {
RetryRow { products.retry() }
}
else -> Unit
}
}
}Bu ekran için Compose UI testinde yalnızca spinner'in göründüğünü doğrulamak yeterli değildir. Fake PagingSource ile ilk REFRESH'i hata, retry sonrası başarı olarak kurgulayın ve retry'nin yeni bir Pager yaratmadığını kontrol edin. Pager'ı her retry tıklamasında ViewModel'de yeniden oluşturmak, önceki Flow koleksiyonunu açık bırakıp aynı liste için paralel RemoteMediator çağrıları üretebilir. Pager instance'ını ViewModel ömründe bir kez oluşturup cachedIn(viewModelScope) uygulayın.
Android kursu projelerinde yayın öncesi regresyon ve Play Store yayınlama kontrolü
Bir android kursu veya kotlin kursu projesinde liste akışını gerçek kullanıcıya açmadan önce, benchmark modülünde sabit bir MockWebServer gecikmesiyle regresyon eşiği belirleyin. Örneğin 500 ms ağ gecikmesi ve 50 öğelik cevap altında ilk 100 satır kaydırmasında jank frame sayısını JankStats veya Perfetto üzerinden kaydedin. Bu, mobil uygulama geliştirme eğitimi sırasında 'hızlı hissettiriyor' yorumundan daha denetlenebilir bir kabul kriteridir.
play store yayınlama aşamasında App Bundle'ın minify edilmiş sürümünü internal test kanalına gönderin ve test cihazında aynı trace'i yeniden alın. Yerel debug sonucu ile dağıtılan paket sonucu farklı olabilir: R8, ağ güvenlik yapılandırması, backend base URL'si ve logging interceptor'ları çalışma yolunu değiştirir. Paket içeriğini doğrulamak için CI'da şu komutu çalıştırın:
bundletool build-apks --bundle app-release.aab --output app-release.apks --mode universal
unzip -l app-release.apks | grep 'base.apk' Bu kontrol, ölçtüğünüz artefact ile yayınladığınız artefact'ın aynı build pipeline'dan geldiğini kanıtlamaz ama bundle'ın kurulabilir APK üretip üretmediğini erken yakalar. CI'a ayrıca benchmark P95 eşiği aşılırsa merge'i engelleyen bir görev ekleyin.İlgili Eğitim
YTÜSEM İlgili Eğitim
Sık Sorulan Sorular
Android eğitimi için Paging 3 pageSize değeri nasıl ölçülerek seçilir?
Sabit bir API cevabı ve cihazla en az iki PagingConfig çalıştırın. Perfetto'da Room write slice P95 süresini, OkHttp EventListener ile istek sayısını ve JankStats ile jank frame sayısını kaydedin. Daha büyük pageSize daha az HTTP isteği üretirken transaction süresini artırabilir; seçim bu üç ölçümün birlikte değerlendirilmesine dayanmalıdır.
Kotlin eğitimi sırasında RemoteMediator neden Room transaction kullanmalı?
Ürün tablosu ve remote key ayrı yazılırsa uygulama process death yaşadığında yarım durum kalabilir. db.withTransaction içinde upsertAll ve key insert yapmak, SQLite'ın atomiklik garantisiyle ikisini birlikte commit veya rollback eder. Bu sayede sonraki APPEND doğru cursor'dan devam eder.
Jetpack Compose Paging listesinde retry neden yeni Pager oluşturmamalı?
Yeni Pager, önceki Flow koleksiyonu iptal edilmediyse ikinci bir RemoteMediator zinciri başlatabilir. ViewModel'de Pager'ı bir kez oluşturun, flow.cachedIn(viewModelScope) kullanın ve UI'dan LazyPagingItems.retry() çağırın. Bu çağrı mevcut yükleme zincirinin hata durumunu yeniden dener.
Android Studio eğitimi kapsamında Paging performansı hangi araçla incelenir?
System Trace veya Perfetto kullanın. RemoteMediator HTTP ve Room write aşamalarını androidx.tracing.trace ile işaretleyin, ardından slice tablosundan ortalama, P95 ve maksimum süreleri çıkarın. CPU Profiler tek başına ağ bekleme ve SQLite transaction sınırlarını aynı netlikte göstermez.
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.


