Sayfalama & Sonsuz Kaydırma
Mobil uygulamalarda büyük veri setlerini verimli şekilde yüklemek ve göstermek için kullanılan temel teknikler.
Sayfalama Mimari Desenleri
Geleneksel Sayfalama
- Offset Tabanlı Sayfalama:
?page=2&limit=20
ile sayfa tabanlı gezinme- Basit uygulama ancak büyük veri setlerinde performans sorunları
- Skip-limit deseni ile veritabanı sorgu optimizasyon zorlukları
- İmleç Tabanlı Sayfalama:
?after=cursor_id&limit=20
ile durum bilgili gezinme- Veri değişikliklerinde bile tutarlı sonuçlar
- Büyük veri setleri için daha iyi performans
Mobil-Optimize Sayfalama Stratejileri
- Aşamalı Yükleme: Küçük başlangıç sayfası, daha büyük sonraki sayfalar
- Uyarlanabilir Sayfa Boyutları: Ağ koşullarına göre dinamik sayfa boyutlandırma
- Öngörülü Yükleme: Kullanıcı davranış kalıpları ile akıllı ön yükleme
Platform-Spesifik Sayfalama Uygulamaları
Android Sayfalama Çözümleri
- Paging 3 Kütüphane Mimarisi:
- PagingSource ile veri yükleme soyutlaması
- RemoteMediator ile ağ + veritabanı koordinasyonu
- PagingDataAdapter ile RecyclerView entegrasyonu
- LoadState yönetimi ile yükleme/hata durumları
- Gelişmiş Özellikler:
- Separators ile gruplandırılmış içerik
- Headers/Footers ile ek içerik
- Retry mekanizmaları ile hata kurtarma
- Placeholder desteği ile akıcı kaydırma
kotlin
// Android Paging 3 Uygulaması
class ItemPagingSource(
private val apiService: ApiService,
private val query: String
) : PagingSource<Int, Item>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Item> {
return try {
val page = params.key ?: 1
val response = apiService.getItems(
query = query,
page = page,
size = params.loadSize
)
LoadResult.Page(
data = response.items,
prevKey = if (page == 1) null else page - 1,
nextKey = if (response.items.isEmpty()) null else page + 1
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, Item>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}
// Repository Uygulaması
class ItemRepository(private val apiService: ApiService) {
fun getItemsPagingFlow(query: String): Flow<PagingData<Item>> {
return Pager(
config = PagingConfig(
pageSize = 20,
prefetchDistance = 5,
enablePlaceholders = false
),
pagingSourceFactory = { ItemPagingSource(apiService, query) }
).flow
}
}
// Adapter Uygulaması
class ItemAdapter : PagingDataAdapter<Item, ItemViewHolder>(ItemDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_layout, parent, false)
return ItemViewHolder(view)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
holder.bind(item)
}
}
}
iOS Sayfalama Desenleri
- UICollectionView Sayfalama:
UICollectionViewDiffableDataSource
ile modern yaklaşımwillDisplay
delegate metodları ile yükleme tetikleyicileriNSFetchedResultsController
ile Core Data entegrasyonu
- SwiftUI Sayfalama:
- LazyVStack ile verimli liste render etme
- @StateObject ile sayfalama durum yönetimi
- Combine yayıncıları ile reaktif veri yükleme
swift
// iOS UICollectionView Sayfalama
class ItemViewController: UIViewController {
@IBOutlet weak var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private let viewModel = ItemViewModel()
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
setupDataSource()
setupBindings()
viewModel.loadInitialData()
}
private func setupDataSource() {
dataSource = UICollectionViewDiffableDataSource<Section, Item>(
collectionView: collectionView
) { collectionView, indexPath, item in
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: "ItemCell",
for: indexPath
) as! ItemCell
cell.configure(with: item)
return cell
}
}
private func setupBindings() {
viewModel.$items
.receive(on: DispatchQueue.main)
.sink { [weak self] items in
self?.updateSnapshot(with: items)
}
.store(in: &cancellables)
}
private func updateSnapshot(with items: [Item]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(items)
dataSource.apply(snapshot, animatingDifferences: true)
}
}
// Collection View Delegate
extension ItemViewController: UICollectionViewDelegate {
func collectionView(
_ collectionView: UICollectionView,
willDisplay cell: UICollectionViewCell,
forItemAt indexPath: IndexPath
) {
let totalItems = dataSource.snapshot().numberOfItems
if indexPath.row == totalItems - 5 {
viewModel.loadNextPage()
}
}
}
// SwiftUI Sayfalama
struct ItemListView: View {
@StateObject private var viewModel = ItemViewModel()
var body: some View {
NavigationView {
List {
ForEach(viewModel.items) { item in
ItemRowView(item: item)
.onAppear {
if item == viewModel.items.last {
viewModel.loadNextPage()
}
}
}
if viewModel.isLoading {
HStack {
Spacer()
ProgressView()
Spacer()
}
}
}
.navigationTitle("Öğeler")
.refreshable {
await viewModel.refresh()
}
}
.onAppear {
viewModel.loadInitialData()
}
}
}
Flutter Sayfalama Çözümleri
- infinite_scroll_pagination Paketi:
- PagingController ile durum yönetimi
- Yerleşik hata yönetimi ile kullanıcı deneyimi
- Özel öğe oluşturucuları ile esnek UI
- Özel Sayfalama Uygulaması:
- ScrollController ile kaydırma pozisyonu tespiti
- FutureBuilder ile asenkron veri yükleme
- Durum yönetimi ile sayfalama koordinasyonu
dart
// Flutter Özel Sayfalama Uygulaması
class PaginatedListView<T> extends StatefulWidget {
final Future<List<T>> Function(int page) onLoadPage;
final Widget Function(BuildContext context, T item) itemBuilder;
final Widget? loadingWidget;
final Widget? errorWidget;
final int pageSize;
const PaginatedListView({
Key? key,
required this.onLoadPage,
required this.itemBuilder,
this.loadingWidget,
this.errorWidget,
this.pageSize = 20,
}) : super(key: key);
@override
_PaginatedListViewState<T> createState() => _PaginatedListViewState<T>();
}
class _PaginatedListViewState<T> extends State<PaginatedListView<T>> {
final List<T> _items = [];
final ScrollController _scrollController = ScrollController();
bool _isLoading = false;
bool _hasReachedEnd = false;
String? _error;
int _currentPage = 1;
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
_loadPage();
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
_loadPage();
}
}
Future<void> _loadPage() async {
if (_isLoading || _hasReachedEnd) return;
setState(() {
_isLoading = true;
_error = null;
});
try {
final newItems = await widget.onLoadPage(_currentPage);
setState(() {
_items.addAll(newItems);
_currentPage++;
_isLoading = false;
if (newItems.length < widget.pageSize) {
_hasReachedEnd = true;
}
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
Future<void> _refresh() async {
setState(() {
_items.clear();
_currentPage = 1;
_hasReachedEnd = false;
_error = null;
});
await _loadPage();
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: _refresh,
child: ListView.builder(
controller: _scrollController,
itemCount: _items.length + (_isLoading ? 1 : 0),
itemBuilder: (context, index) {
if (index < _items.length) {
return widget.itemBuilder(context, _items[index]);
} else {
return _buildLoadingIndicator();
}
},
),
);
}
Widget _buildLoadingIndicator() {
if (_error != null) {
return widget.errorWidget ??
Center(
child: Column(
children: [
Text('Hata: $_error'),
ElevatedButton(
onPressed: _loadPage,
child: Text('Tekrar Dene'),
),
],
),
);
}
return widget.loadingWidget ??
const Center(child: CircularProgressIndicator());
}
}
Sonsuz Kaydırma Uygulaması
Kaydırma Tespit Mekanizmaları
- Eşik Tabanlı Yükleme: %80 kaydırıldığında yükleme
- Öngörülü Yükleme: Kaydırma hızına göre yükleme
- Görünüm Alanı Tabanlı Yükleme: Görünür alana yaklaşırken yükleme
Performans Optimizasyon Teknikleri
- Görünüm Geri Dönüşümü:
- RecyclerView ViewHolder deseni (Android)
- UITableView hücre yeniden kullanımı (iOS)
- ListView.builder verimli render etme (Flutter)
- Bellek Yönetimi:
- Pencere tabanlı veri yönetimi
- Uzak öğeler için otomatik veri temizleme
- Görüntü tembel yükleme ile bellek optimizasyonu
Gelişmiş Sonsuz Kaydırma Özellikleri
- Çift Yönlü Kaydırma: İleri ve geri sayfalama
- Aşağı Çekerek Yenileme Entegrasyonu: Manuel yenileme yeteneği
- Boş Durum Yönetimi: Daha fazla veri olmayan senaryolar
- Hata Durumu Kurtarma: Ağ hatası ile yeniden deneme mekanizmaları
Veri Tutarlılığı Zorlukları
Gerçek Zamanlı Veri Güncellemeleri
- Yeni Öğe Ekleme: Sayfalama sırasında yeni eklenen öğeleri yönetme
- Öğe Değişiklikleri: Sayfalamayı bozmadan mevcut öğeleri güncelleme
- Öğe Silme: Kaydırma pozisyonunu koruyarak öğeleri kaldırma
Eşzamanlı Erişim Yönetimi
- Yarış Durumu Önleme: Çoklu sayfa yüklemelerini koordine etme
- Önbellek Koordinasyonu: Önbelleklenmiş ve taze veri arasında tutarlılık sağlama
- Durum Senkronizasyonu: Çoklu ekran sayfalama durum yönetimi
Ağ Optimizasyonu
Akıllı Ön Yükleme
- Kullanım Kalıbı Analizi: Kullanıcı kaydırma davranışını öğrenme
- Ağ Durumu Farkındalığı: Bağlantıya göre ön yükleme stratejisini ayarlama
- Pil Düşüncesi: Düşük pilde ön yüklemeyi azaltma
İstek Optimizasyonu
- İstek Tekrarlarını Önleme: Yinelenen sayfa isteklerini önleme
- İstek İptali: Kaydırma yönü değiştiğinde gereksiz istekleri iptal etme
- Toplu Yükleme: Uygun olduğunda tek istekte birden fazla sayfa yükleme
Metrikler ve Analitik
- Kaydırma Derinliği Takibi: Kullanıcıların tipik olarak ne kadar kaydırdığı
- Sayfa Yükleme Performansı: Her sayfayı yükleme süresi
- Kullanıcı Etkileşimi: Sayfalama ile kullanıcı tutma arasındaki korelasyon
- Hata Oranı İzleme: Başarısız sayfa yüklemeleri ile kullanıcı deneyimi etkisi