Android MVVM mimarisi içinde Paging 3 ve RemoteMediator kullanarak Room onbelleği ile API sayfalamasını tutarlı hale getirin. Jetpack Compose listelerinde yükleme, hata ve yenileme durumlarını ölçülebilir biçimde yönetin.
Android MVVM Mimarisi: Paging 3 RemoteMediator ile Akıcı Listeler
Android MVVM Mimarisi ile Paging 3 veri akışını kurmak
Sayfalı bir akışta ViewModel'in doğrudan Retrofit çağırması, ekran döndüğünde veya aynı sorgu iki kez istendiğinde ağ sonucu ile yerel veri arasında yarış durumu üretir. Android MVVM mimarisi için sağlam sınır şudur: UI sadece Room'dan gelen PagingSource akışını gözlemler, Repository ise Retrofit ve Room işlemlerini RemoteMediator içinde birleştirir. Room sorgusuna sabit bir sıralama ekleyin; sadece createdAt ile sıralama yapmak, aynı zaman damgasına sahip kayıtlarda satırların sayfalar arasında yer değiştirmesine yol açabilir. Örneğin ORDER BY createdAt DESC, id DESC kullanın.
@OptIn(ExperimentalPagingApi::class)
fun feedPager(): Flow<PagingData<ArticleEntity>> = Pager(
config = PagingConfig(
pageSize = 30,
initialLoadSize = 30,
prefetchDistance = 8,
enablePlaceholders = false,
maxSize = 180
),
remoteMediator = ArticleRemoteMediator(api, db),
pagingSourceFactory = { db.articleDao().pagingSource() }
).flow
class FeedViewModel(repository: ArticleRepository) : ViewModel() {
val articles = repository.feedPager()
.cachedIn(viewModelScope)
}Buradaki cachedIn kritik bir ayrıntıdır: aynı ViewModel içinde yeniden composition veya ikinci bir collector oluştuğunda upstream Pager'ı paylaşır. Buna karşılık cachedIn'i Repository singleton'ına taşımayın; farklı kullanıcı oturumlarının veya filtrelerin PagingData nesnesini paylaşması, eski kullanıcının Room satırlarını yeni ekranda gösterebilir. Oturum değişiminde Room veritabanını kapatıp kullanıcıya özgü dosya adıyla yeniden açmak ya da userId alanını her DAO sorgusuna zorunlu filtre olarak eklemek gerekir.
RemoteMediator transaction sınırları ve yenileme hataları
RemoteMediator içinde ağ cevabını aldıktan sonra hem içerik satırlarını hem de sayfa anahtarını tek Room transaction'ında yazın. Bunun nedeni, process'in transaction ortasında ölmesi durumunda ArticleEntity yazılıp RemoteKey yazılmaması halinde bir sonraki APPEND'in yanlış sayfayı istemesidir. REFRESH sırasında yerel tabloyu ağ isteğinden önce silmek de yaygın bir hatadır: istek 500 dönerse kullanıcı elindeki kullanılabilir önbelleği kaybeder.
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, ArticleEntity>
): MediatorResult {
val page = when (loadType) {
LoadType.REFRESH -> 1
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> db.remoteKeyDao().nextPage("feed")
?: return MediatorResult.Success(endOfPaginationReached = true)
}
return try {
val response = api.getArticles(page = page, size = state.config.pageSize)
db.withTransaction {
if (loadType == LoadType.REFRESH) {
db.articleDao().clearFeed()
db.remoteKeyDao().delete("feed")
}
db.articleDao().upsertAll(response.items.map { it.toEntity() })
db.remoteKeyDao().upsert(RemoteKey("feed", response.nextPage))
}
MediatorResult.Success(endOfPaginationReached = response.nextPage == null)
} catch (e: IOException) {
MediatorResult.Error(e)
} catch (e: HttpException) {
MediatorResult.Error(e)
}
}Sunucu yalnızca ileri yönlü cursor veriyorsa PREPEND'i başarıyla bitirmek doğrudur. Ancak API hem before hem after cursor destekliyorsa PREPEND'i koşulsuz bitirmek eski öğelere erişimi engeller; bu durumda ilk ve son görünür öğenin RemoteKey kayıtlarını ayrı tutun. Ayrıca filtre değişiminde "feed" gibi tek sabit remote key kullanmayın. Key'i "feed:$categoryId:$sort" biçiminde üretin, aksi halde bir kategorinin nextPage değeri diğer kategorinin APPEND isteğine taşınır.
Jetpack Compose listesinde yükleme durumları ve stabil anahtarlar
Jetpack Compose tarafında PagingData'yı collectAsLazyPagingItems ile tüketin ve LazyColumn için backend kimliğini key olarak verin. Liste indeksi key olduğunda REFRESH sonrası ilk sayfaya yeni kayıt eklendiğinde Compose mevcut slotları farklı makaleler için yeniden kullanabilir; örneğin satır içindeki remember edilmiş expand state yanlış makaleye taşınır. contentType vermek de farklı satır tiplerinde ölçüm ve composition yapısının daha doğru yeniden kullanılmasını sağlar.
@Composable
fun FeedScreen(viewModel: FeedViewModel) {
val items = viewModel.articles.collectAsLazyPagingItems()
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(
count = items.itemCount,
key = { index -> items[index]?.id ?: "placeholder-$index" },
contentType = { index -> items[index]?.type ?: "placeholder" }
) { index ->
items[index]?.let { article -> ArticleRow(article) }
}
when (val append = items.loadState.append) {
is LoadState.Loading -> item { CircularProgressIndicator() }
is LoadState.Error -> item {
RetryRow(message = append.error.message ?: "Yukleme hatasi") {
items.retry()
}
}
else -> Unit
}
}
}refresh ve append hatalarını aynı UI mesajında birleştirmeyin. refresh Error boş bir Room tablosunda tam ekran hata sunabilir, fakat append Error mevcut listeyi geçersiz kılmaz ve listenin sonunda retry satırı gerektirir. Paging'in LoadStateFlow değerini loglamak için Firebase Crashlytics breadcrumb veya uygulamanın kendi event altyapısında loadType, HTTP kodu ve itemCount alanlarını kaydedin. Bu kayıtlar, örneğin yalnızca düşük bağlantıda görülen APPEND tekrar döngüsünü UI ekran görüntüsünden ayırmayı sağlar.
Paging kaydırma maliyetini Perfetto ve Macrobenchmark ile ölçmek
Kaydırma sorununun ağdan mı, Room sorgusundan mı, yoksa Compose ölçümünden mi geldiğini tahmin etmeyin. Önce aynı fiziksel veya emülatör cihazda release benzeri bir build ile Macrobenchmark çalıştırın. Android Studio Profiler CPU örneklemesi anlık inceleme için yararlıdır; kare düşüşü ile uygulama trace'ini ilişkilendirmek için Perfetto sistem izi daha uygundur. Önce PagingConfig maxSize verilmemiş ve key kullanılmamış sürümün median frame süresini kaydedin, sonra yalnızca bir değişiklik yapıp aynı benchmark'ı en az 10 iterasyonla tekrar edin.
@RunWith(AndroidJUnit4::class)
class FeedScrollBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun scrollFeed() = benchmarkRule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(FrameTimingMetric()),
compilationMode = CompilationMode.Full(),
iterations = 10,
setupBlock = { pressHome(); startActivityAndWait() }
) {
val list = device.findObject(By.desc("feed_list"))
list.setGestureMargin(device.displayWidth / 5)
list.fling(Direction.DOWN)
list.fling(Direction.DOWN)
}
}Benchmark hedefini bulmak için üretim semantiğini bozmadan yalnızca debug veya benchmark build type'ında LazyColumn'a contentDescription="feed_list" ekleyin. Perfetto kaydında Choreographer frame'lerinin yanında uzun Room query, OkHttp callback veya compose:recompose dilimlerini arayın. Ağ isteğinin zamanını uygulama trace'ine bağlamak için Repository'de Trace.beginSection("paging_fetch") ve finally bloğunda Trace.endSection() kullanın. Eğer frame süresi yalnızca yeni sayfa eklendiğinde yükseliyorsa önce ArticleRow içindeki Bitmap decode veya tarih formatlama gibi ana thread işlerini düzeltin; pageSize'ı rastgele küçültmek yalnızca daha fazla ağ isteği üretir.
Android Studio eğitimi kapsamında test, paketleme ve Play Store yayınlama
RemoteMediator davranışını MockWebServer ile test edin: ilk REFRESH için 200, APPEND için 500 döndürün ve hata sonrası Room'daki ilk 30 kaydın korunduğunu doğrulayın. Bu senaryo, sadece UI testiyle yakalanması zor olan erken clearFeed hatasını yakalar. Ayrıca Room in-memory veritabanında aynı id'li kayıt için upsert sonrası sıralamanın id ikincil anahtarıyla kararlı kaldığını test edin. Testi CI'da ./gradlew connectedCheck ile API 21 yerine uygulamanızın gerçek minSdk emülatöründe koşturun; SQLite davranışı ve desugaring farkları burada görünür olabilir.
Bir android eğitimi veya android eğitim programında Paging'i sadece sonsuz liste demosu olarak anlatmak yetersizdir; android kursu içeriğinde ağ hatası, cursor çakışması ve benchmark ölçümü birlikte ele alınmalıdır. Aynı şekilde kotlin eğitimi ve kotlin kursu örneklerinde Flow'un cachedIn sınırı ve structured concurrency gösterilmelidir. mobil uygulama geliştirme eğitimi sürecinde Android Studio'nun Database Inspector aracıyla RemoteKey tablosunu kontrol edin, ardından play store yayınlama öncesi ./gradlew bundleRelease üretin ve bundletool ile paketin gerçek split APK içeriğini inceleyin:
bundletool build-apks --bundle=app-release.aab --output=app.apks --mode=universal
unzip -l app.apks | grep -E "classes|resources" Bu kontrol, yanlışlıkla debug endpoint veya test sertifikası içeren bir release artifact'in dağıtılmasını CI logunda görünür hale getirir.İlgili Eğitim
YTÜSEM İlgili Eğitim
Sık Sorulan Sorular
Android MVVM mimarisi ile Paging 3 kullanırken cachedIn nereye yazılmalı?
cachedIn(viewModelScope) genellikle ViewModel'de olmalıdır. Böylece aynı ekranın birden fazla collector'ı tek Pager akışını paylaşır, fakat farklı kullanıcı veya filtre ekranları Repository singleton'ında yanlış PagingData paylaşmaz. Filtre değişimini flatMapLatest ile yeni Pager üretimi olarak modelleyin.
Jetpack Compose Paging listesinde append hatası nasıl tekrar denenir?
lazyPagingItems.loadState.append değerini LoadState.Error olarak kontrol edin ve listenin sonunda lazyPagingItems.retry() çağıran bir item gösterin. refresh hatasını append ile birleştirmeyin; önbellekte satır varken append hatası tüm ekranı kaplayan hata görünümü olmamalıdır.
Android Studio eğitimi sırasında Paging performansı hangi araçla ölçülür?
Tekrarlanabilir karşılaştırma için Macrobenchmark FrameTimingMetric kullanın; aynı cihaz, aynı compilation mode ve en az 10 iterasyonla önce-sonra median değerlerini karşılaştırın. Ardından Perfetto'da uzun frame'leri Room sorgusu, OkHttp callback ve Compose recomposition trace dilimleriyle eşleştirin.
Play Store yayınlama öncesi Paging uygulamasında hangi testler çalıştırılmalı?
MockWebServer ile REFRESH 200, APPEND 500 ve yeniden deneme senaryolarını çalıştırın; hata sırasında Room önbelleğinin silinmediğini doğrulayın. Release paketi için ./gradlew bundleRelease çalıştırın, bundletool build-apks ile AAB içeriğini inceleyin ve Macrobenchmark sonuçlarını sürüm artefaktına ekleyin.
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.



