↑↓ seç · Enter aç · Esc kapat

Mutlu Tekin
Mutlu Tekin
← Yazılar

Bölüm 10 — Konu 49: Lazy Loading, Pagination, Büyük Liste Performansı

5 dk okuma #flutter
Dizi · 49/61 Flutter Türkçe Tutorial
  1. Bölüm 1 — Konu 1: Flutter Nedir, Mimarisi (Widget → Element → RenderObject, Skia/Impeller)
  2. Bölüm 1 — Konu 2: Ortam Kurulumu (Flutter SDK, Android Studio/VS Code, Emulator/Simulator, DevTools'a İlk Bakış)
  3. Bölüm 1 — Konu 3: İlk Proje Yapısı (`pubspec.yaml`, `lib/`, Klasör Mimarisi, `flutter create` Anatomisi)
  4. Bölüm 1 — Konu 4: Hot Reload / Hot Restart Ne Yapıyor, Neden Önemli
  5. Bölüm 2 — Konu 5: "Her Şey Widget'tır" Felsefesi, Widget Ağacı
  6. Bölüm 2 — Konu 6: StatelessWidget vs StatefulWidget + `setState()` Derinlemesine (Rebuild Mekanizması)
  7. Bölüm 2 — Konu 7: Temel Layout — Container, Row, Column, Stack, Padding, Align, Center
  8. Bölüm 2 — Konu 8: Constraint Sistemi — "Constraints Go Down, Sizes Go Up, Parent Sets Position"
  9. Bölüm 2 — Konu 9: `Expanded`, `Flexible`, `Spacer`, Intrinsic Widget'lar
  10. Bölüm 3 — Konu 10: MaterialApp/CupertinoApp, Scaffold, AppBar
  11. Bölüm 3 — Konu 11: `ListView`, `GridView`, `SingleChildScrollView` (+ Builder Pattern)
  12. Bölüm 3 — Konu 12: Text, TextStyle, Icon, Image, Asset Yönetimi
  13. Bölüm 3 — Konu 13: Custom Widget Yazma Prensipleri (Composition Over Inheritance)
  14. Bölüm 3 — Konu 14: Navigator 1.0 — Push/Pop, Named Routes
  15. Bölüm 4 — Konu 15: Form, TextField/TextFormField, GlobalKey<FormState>, Validasyon
  16. Bölüm 4 — Konu 16: Theme Sistemi (ThemeData, ColorScheme)
  17. Bölüm 4 — Konu 17: Responsive & Adaptive Tasarım (MediaQuery, LayoutBuilder, OrientationBuilder, Breakpoint Stratejileri)
  18. Bölüm 5 — Konu 18: Future/Async-Await Flutter Bağlamında, FutureBuilder
  19. Bölüm 5 — Konu 19: `Stream`, `StreamBuilder`
  20. Bölüm 5 — Konu 20: HTTP İstekleri (`http` / `dio` Paketleri)
  21. Bölüm 5 — Konu 21: JSON Serialization (Manuel → `json_serializable`/`freezed`)
  22. Bölüm 5 — Konu 22: Local Storage — `shared_preferences` → `Hive` → `sqflite`/Drift
  23. Bölüm 6 — Konu 23: InheritedWidget ve InheritedModel — "Neden setState Yetmiyor" Sorusunun Cevabı
  24. Bölüm 6 — Konu 24: Provider Paketi
  25. Bölüm 6 — Konu 25: Riverpod (Modern Yaklaşım, Provider'ın Halefi)
  26. Bölüm 6 — Konu 26: BLoC/Cubit Pattern (`flutter_bloc`)
  27. Bölüm 6 — Konu 27: GetX (Tartışmalı Ama Yaygın)
  28. Bölüm 6 — Konu 28: Karşılaştırma — Hangi Projede Hangisi?
  29. Bölüm 7 — Konu 29: Navigator 2.0 (Router, RouteInformationParser, RouterDelegate)
  30. Bölüm 7 — Konu 30: `go_router` Paketi (Pratik ve Modern Çözüm)
  31. Bölüm 7 — Konu 31: Deep Linking
  32. Bölüm 7 — Konu 32: Repository Pattern, Katmanlı Mimari (Data/Domain/Presentation)
  33. Bölüm 7 — Konu 33: Dependency Injection (`get_it`, `injectable`)
  34. Bölüm 7 — Konu 34: Clean Architecture Uyarlaması, SOLID Prensipleri
  35. Bölüm 8 — Konu 35: Implicit Animasyonlar
  36. Bölüm 8 — Konu 36: Explicit Animasyonlar (`AnimationController`, `Tween`, `Curve`)
  37. Bölüm 8 — Konu 37: Hero Animasyonları
  38. Bölüm 8 — Konu 38: `CustomPainter` / `Canvas`
  39. Bölüm 8 — Konu 39: Rive / Lottie Entegrasyonu
  40. Bölüm 9 — Konu 40: Platform Channels
  41. Bölüm 9 — Konu 41: Permission Yönetimi (`permission_handler`)
  42. Bölüm 9 — Konu 42: Kamera, Konum, Sensörler
  43. Bölüm 9 — Konu 43: Push Notification (Firebase Cloud Messaging)
  44. Bölüm 9 — Konu 44: Android/iOS Build Sistemleri
  45. Bölüm 10 — Konu 45: Test Yazımı (Unit, Widget, Integration, Golden)
  46. Bölüm 10 — Konu 46: CI/CD
  47. Bölüm 10 — Konu 47: Rebuild Optimizasyonu (`const`, `key` Kullanımı)
  48. Bölüm 10 — Konu 48: DevTools Profiling
  49. Bölüm 10 — Konu 49: Lazy Loading, Pagination, Büyük Liste Performansı
  50. Bölüm 11 — Konu 50: Firebase Ekosistemi (Auth, Firestore, Storage, Functions)
  51. Bölüm 11 — Konu 51: Supabase Alternatifi
  52. Bölüm 11 — Konu 52: GraphQL (Opsiyonel)
  53. Bölüm 11 — Konu 53: Store Yayınlama Süreci (İmzalama, Listing, Versiyonlama)
  54. Bölüm 11 — Konu 54: App Size, Obfuscation, Flavor Yönetimi
  55. Bölüm 12 — Konu 55: Flutter Web / Desktop
  56. Bölüm 12 — Konu 56: Custom `RenderObject` Yazımı
  57. Bölüm 12 — Konu 57: Engine Mimarisi Derinlemesine (Impeller vs Skia)
  58. Bölüm 12 — Konu 58: Plugin Geliştirme, pub.dev'e Paket Yayınlama
  59. Bölüm 12 — Konu 59: Monorepo Mimarisi (Melos)
  60. Bölüm 12 — Konu 60: Erişilebilirlik (Accessibility) Derinlemesine
  61. Bölüm 12 — Konu 61: Yerelleştirme (Localization / i18n)
Dizinin sayfası →
İçindekiler 10 başlık
  1. ListView.builder Zaten "Lazy" — Hatırlatma
  2. Pagination — Veriyi de "Lazy" Yükleme
  3. RepaintBoundary — Çizim İşini İzole Etme
  4. AutomaticKeepAliveClientMixin — Kaydırınca Kaybolan State'i Koruma
  5. Resim Önbellekleme — cached_network_image
  6. İleri Bir Teknik — Sliver'lar (Kısa Bir Önizleme)
  7. Özet — Büyük Liste Performansı İçin Kontrol Listesi
  8. 🎯 Bu Dersten Çıkarılması Gerekenler
  9. 📝 Ödevler
  10. 🎮 Mini Uygulama — Bölüm 10 Checkpoint: Performanslı Ürün Kataloğu

Bölüm 10'un son konusu. Konu 47-48'de genel rebuild optimizasyonunu ve ölçüm araçlarını öğrendik — bu derste, gerçek uygulamalarda en sık karşılaşılan performans senaryosuna odaklanıyoruz: binlerce elemanlı bir listeyi akıcı şekilde göstermek.

ListView.builder Zaten "Lazy" — Hatırlatma

Bölüm 3 Konu 11'de öğrendiğimiz ListView.builder'ı hatırla: tüm listeyi baştan oluşturmaz, sadece ekranda görünen (+ biraz fazlası) widget'ları oluşturur. Bu, "lazy loading"in widget oluşturma seviyesindeki hâli — ama bu derste göreceğimiz gibi, büyük listelerde yetmeyen iki farklı sorun daha var: verinin tamamının bellekte olması, ve her elemanın kendi maliyetinin (özellikle resimler) toplam performansa etkisi.

Pagination — Veriyi de "Lazy" Yükleme

ListView.builder, elindeki List<T>'nin sadece görünen kısmını çizer — ama ya List<T>'nin kendisi 10.000 elemanlıysa ve bunların hepsini API'den tek seferde çekmen gerekiyorsa? Bu hem yavaş bir ilk yükleme, hem gereksiz bir veri transferi olur. Pagination (sayfalama), veriyi API'den de parça parça çekmeyi sağlar.

dart
class UrunListesiEkrani extends StatefulWidget {
  const UrunListesiEkrani({super.key});

  @override
  State<UrunListesiEkrani> createState() => _UrunListesiEkraniState();
}

class _UrunListesiEkraniState extends State<UrunListesiEkrani> {
  final List<Urun> _urunler = [];
  final ScrollController _scrollController = ScrollController();
  int _sayfa = 1;
  bool _yukleniyorMu = false;
  bool _dahaFazlaVarMi = true;

  @override
  void initState() {
    super.initState();
    _sayfaYukle();
    _scrollController.addListener(_scrollDinleyici);
  }

  void _scrollDinleyici() {
    // kullanıcı listenin SONUNA 200 piksel kala, bir sonraki sayfayı yükle
    if (_scrollController.position.pixels >=
        _scrollController.position.maxScrollExtent - 200) {
      _sayfaYukle();
    }
  }

  Future<void> _sayfaYukle() async {
    if (_yukleniyorMu || !_dahaFazlaVarMi) return; // Dart Bölüm 3 — erken çıkış deseni

    setState(() => _yukleniyorMu = true);

    final yeniUrunler = await UrunServisi.getir(sayfa: _sayfa); // Bölüm 5 Konu 20'yi hatırla

    setState(() {
      _urunler.addAll(yeniUrunler);
      _sayfa++;
      _yukleniyorMu = false;
      _dahaFazlaVarMi = yeniUrunler.isNotEmpty;
    });
  }

  @override
  void dispose() {
    _scrollController.dispose(); // Bölüm 8 Konu 36'daki AYNI disiplin
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _scrollController,
      itemCount: _urunler.length + (_dahaFazlaVarMi ? 1 : 0), // +1: yükleniyor göstergesi
      itemBuilder: (context, index) {
        if (index == _urunler.length) {
          return const Center(
            child: Padding(
              padding: EdgeInsets.all(16),
              child: CircularProgressIndicator(),
            ),
          );
        }
        return UrunSatiri(urun: _urunler[index]);
      },
    );
  }
}

Parça parça inceleyelim:

ScrollController — Bölüm 3 Konu 11'de ListView.builder'ı kullanırken görmediğimiz bir parametre; kaydırma konumunu izlemeni sağlar (Bölüm 8 Konu 36'daki AnimationController gibi, bu da dispose() edilmesi gereken bir kaynak).

_scrollController.position.pixels >= maxScrollExtent - 200 — "kullanıcı sona 200 piksel kala" demenin yolu; tam sona gelmeden önce tetiklemek, kullanıcı boş bir alanla karşılaşmadan yeni veri hazır olsun diye.

itemCount: _urunler.length + (_dahaFazlaVarMi ? 1 : 0) — Dart Bölüm 3'te öğrendiğimiz ternary operatör; listenin sonuna bir "yükleniyor" göstergesi ekleyip eklemeyeceğimize karar veriyor.

_yukleniyorMu kontrolü — Bölüm 5'te asenkron kod yazarken çok kritik bir detay: kullanıcı hızlı kaydırırsa, _scrollDinleyici birden fazla kez tetiklenebilir — bu bayrak olmadan, aynı sayfayı birden fazla kez isteyebilirsin.

RepaintBoundary — Çizim İşini İzole Etme

dart
ListView.builder(
  itemBuilder: (context, index) {
    return RepaintBoundary(
      child: UrunSatiri(urun: urunler[index]),
    );
  },
)

Bölüm 10 Konu 48'de öğrendiğimiz raster thread kavramını hatırla — bir widget değiştiğinde, Flutter normalde onu ve komşularını yeniden çizim katmanında işlemek zorunda kalabilir. RepaintBoundary, Flutter'a "bu widget'ı kendi ayrı çizim katmanına koy" der — bu sayede, bir satır değiştiğinde (örn. içindeki bir animasyon), komşu satırların yeniden çizilmesi gerekmez, sadece o tek katman güncellenir.

Ne zaman kullanılır? Her widget'ı RepaintBoundary'ye sarmak gerekmez (her katman, kendi belleğini tutar — aşırı kullanım tam tersi bir performans sorununa yol açabilir). En faydalı olduğu yerler: listelerde sık değişen (bir animasyon, bir canlı sayaç içeren) satırlar, ya da Bölüm 8 Konu 38'de öğrendiğimiz CustomPainter gibi pahalı çizim içeren widget'lar.

AutomaticKeepAliveClientMixin — Kaydırınca Kaybolan State'i Koruma

dart
class VideoKarti extends StatefulWidget {
  const VideoKarti({super.key});
  @override
  State<VideoKarti> createState() => _VideoKartiState();
}

class _VideoKartiState extends State<VideoKarti> with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true; // ekrandan çıksa bile state'i KORU

  @override
  Widget build(BuildContext context) {
    super.build(context); // mixin'in ZORUNLU kıldığı çağrı
    return VideoOynatici(/* ... */);
  }
}

Sorunu anla: ListView.builder'ın "lazy" doğası (ekrandan çıkan widget'ları kaldırması), çoğu zaman faydalıdır (bellek tasarrufu) — ama bir video oynatıcı, bir form gibi pahalı kurulan veya kullanıcı girdisi içeren bir widget, kullanıcı biraz kaydırıp geri geldiğinde state'ini kaybederse, bu kötü bir deneyimdir. Dart Bölüm 7'de öğrendiğimiz mixin kavramının burada pratik bir kullanımı: AutomaticKeepAliveClientMixin, Flutter'a "bu widget'ı ekrandan çıksa bile canlı tut" der.

Denge: Bunu her widget'ta açarsan, ListView.builder'ın asıl faydasını (görünmeyeni bellekten atma) kaybedersin — sadece gerçekten gerekli (state kaybı görünür/rahatsız edici olan) widget'larda kullanılmalı.

Resim Önbellekleme — cached_network_image

bash
flutter pub add cached_network_image
dart
CachedNetworkImage(
  imageUrl: urun.resimUrl,
  placeholder: (context, url) => const CircularProgressIndicator(),
  errorWidget: (context, url, hata) => const Icon(Icons.error),
  fadeInDuration: const Duration(milliseconds: 200),
)

Bölüm 3 Konu 12'de öğrendiğimiz Image.network'ün büyük bir eksiği var: her build()'de (ya da ekrandan çıkıp tekrar girdiğinde), aynı resmi ağdan tekrar tekrar indirir. CachedNetworkImage, resmi hem bellekte hem diskte önbellekler — bir ürün listesinde yukarı-aşağı kaydırdıkça, aynı resimler defalarca indirilmez. Büyük bir listede, bu tek başına, const/RepaintBoundary kombinasyonundan daha büyük bir performans (ve veri kullanımı) farkı yaratabilir.

İleri Bir Teknik — Sliver'lar (Kısa Bir Önizleme)

dart
CustomScrollView(
  slivers: [
    SliverAppBar(title: Text('Ürünler'), floating: true),
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) => UrunSatiri(urun: urunler[index]),
        childCount: urunler.length,
      ),
    ),
  ],
)

ListView, aslında arka planda CustomScrollView + SliverList'in basitleştirilmiş bir versiyonudur (tıpkı go_router'ın Navigator 2.0'ı sarmalaması gibi — Bölüm 7 Konu 30'u hatırla). Bir ekranda farklı türde kaydırılabilir alanları (bir AppBar'ın kaydırırken küçülmesi, bir grid'in bir listeyle aynı scroll'u paylaşması gibi) birleştirmen gerektiğinde, Sliver API'sine doğrudan erişmen gerekir. Bu, kendi başına derin bir konu — şimdilik varlığını ve ListView'in aslında bunun üzerine kurulu olduğunu bilmen yeterli.

Özet — Büyük Liste Performansı İçin Kontrol Listesi

metin
□ ListView.builder kullanılıyor mu (ListView(children: [...]) DEĞİL)?
□ Veri de mi büyük? → Pagination (ScrollController + sayfa sayfa yükleme)
□ Satırlar sık değişiyor mu (animasyon, canlı veri)? → RepaintBoundary
□ Kaydırınca kaybolmaması gereken pahalı state var mı? → AutomaticKeepAliveClientMixin
□ Resimler var mı? → cached_network_image
□ Satır widget'ları const olabiliyor mu? → Konu 47'yi uygula
□ key gerekiyor mu (sıralama değişiyor mu)? → Konu 47'yi uygula

🎯 Bu Dersten Çıkarılması Gerekenler

  • ListView.builder, widget oluşturmayı "lazy" yapar ama verinin tamamının API'den tek seferde çekilmesini çözmez — bunun için pagination (ScrollController + sayfa sayfa yükleme) gerekir.
  • RepaintBoundary, sık değişen bir widget'ın çizim işini komşularından izole eder; aşırı kullanımı da kendi maliyetini getirir.
  • AutomaticKeepAliveClientMixin, listeden çıkan ama state'i korunması gereken widget'lar (video, form) için kullanılır — dengeli kullanılmalı.
  • cached_network_image, resimlerin tekrar tekrar ağdan indirilmesini önler; büyük listelerde kritik bir optimizasyondur.
  • ListView, aslında CustomScrollView + SliverList'in basitleştirilmiş hâlidir; karmaşık kaydırma senaryolarında doğrudan Sliver API'sine geçilir.

📝 Ödevler

  • [ ] Sahte bir API (Future.delayed ile Bölüm 5 Konu 18'i hatırlayarak) kullanarak, 20'şer elemanlık sayfalar halinde veri döndüren, ScrollController ile sona yaklaşınca otomatik yükleme yapan bir liste kur.
  • [ ] Aynı listeye, her satıra RepaintBoundary ekleyip eklemeden, DevTools'un (Konu 48) Timeline'ında bir fark olup olmadığını gözlemle.
  • [ ] cached_network_image ile bir resim listesi yap, aynı listeyi yukarı-aşağı kaydırıp Network sekmesinde (Konu 48) resimlerin tekrar indirilmediğini doğrula.
  • [ ] AutomaticKeepAliveClientMixin kullanmadan ve kullanarak, bir PageView/liste içinde bir TextField'ın kaydırınca metnini kaybedip kaybetmediğini karşılaştır.
  • [ ] Kendi cümlelerinle, "neden ListView(children: [...])'un binlerce elemanlı bir liste için uygun olmadığını" açıkla.

🎮 Mini Uygulama — Bölüm 10 Checkpoint: Performanslı Ürün Kataloğu

Bölüm 10'un tamamını (test, CI, rebuild optimizasyonu, DevTools, büyük liste performansı) birleştiren bir mini proje:

Gereksinimler:

  • Sahte/gerçek bir API'den sayfalı (pagination) şekilde çekilen bir ürün listesi (ScrollController ile sonsuz kaydırma).
  • Her ürün satırı const yapılabilecek kısımları const olarak işaretlenmiş, resimler cached_network_image ile gösteriliyor.
  • Ürünlerin favorilere eklenip çıkarılabildiği bir liste — favori durumu değiştiğinde sadece o satırın yeniden çizildiğini DevTools'un "Track widget rebuilds" özelliğiyle doğrula.
  • En az bir birim test (favori ekleme/çıkarma mantığı için) ve bir widget test (listenin doğru sayıda ürün gösterdiğini doğrulayan).
  • GitHub Actions ile bu testleri her push'ta otomatik çalıştıran bir CI workflow.
  • Bonus: RepaintBoundary ekleyip eklemeden Timeline'da bir fark olup olmadığını kendi ölçümünle karşılaştır ve bir cümleyle sonucunu not al.

Bölüm 10 tamamlandı. Sıradaki konu: Bölüm 11 — Konu 50: Firebase Ekosistemi (Auth, Firestore, Storage, Functions)