İçeriğe geç / Skip to content / Zum Inhalt

Flutter: ListView Widget Kullanımı

Ahmet Balaman

Son güncelleme:

5 dk okuma

FlutterListViewWidgetScrollPerformanceLayout
Flutter: ListView Widget Kullanımı

ListView, öğeleri art arda dizen ve ekrana sığmayan kısmı kaydırılabilir yapan widget'tır. Mesaj listesi, ayarlar menüsü, ürün akışı gibi "aynı türden satırların alt alta geldiği" her ekranda karşına çıkar. Asıl mesele ListView yazmak değil, hangi kurucusunu (constructor) ne zaman seçeceğini bilmek: yanlış seçim küçük listede fark edilmez, bin satırlık listede takılmaya dönüşür.

Canlı Demo

Aşağıdaki interaktif örnekte bu widget'ı deneyebilirsin:

💡 Eğer yukarıdaki örnek açılmazsa, DartPad linkine tıklayarak yeni sekmede çalıştırabilirsin.

Temel Kullanım

En sade hali, hazır bir widget listesi vermektir:

ListView(
  children: const [
    Text('Öğe 1'),
    Text('Öğe 2'),
    Text('Öğe 3'),
  ],
)

Bu kurucuda children listesindeki bütün widget nesneleri baştan oluşturulur. Beş on satırlık sabit bir menü için sorun değildir; yüzlerce satır için doğru araç ListView.builder'dır (aşağıda).

Önemli Özellikler

Özellik Açıklama
scrollDirection Kaydırma yönü (Axis.vertical / Axis.horizontal)
padding Liste içeriğinin etrafındaki boşluk
reverse Listeyi sondan başlatır (sohbet ekranları için kullanışlı)
physics Kaydırma davranışı
shrinkWrap Listeyi içeriği kadar yer kaplamaya zorlar (pahalıdır)
itemExtent Her öğe için sabit yükseklik
prototypeItem Yüksekliği örnek bir widget'tan ölçtürür
controller Kaydırma konumunu okumak ve değiştirmek için ScrollController

ListTile ile Klasik Satır

Satır tasarımı için çoğu zaman ListTile yeter; ikon, başlık, alt başlık ve sağdaki ok hazır gelir:

ListView(
  children: [
    ListTile(
      leading: const Icon(Icons.person),
      title: const Text('Kullanıcı Adı'),
      subtitle: const Text('Alt başlık'),
      trailing: const Icon(Icons.chevron_right),
      onTap: () {},
    ),
    const ListTile(
      leading: Icon(Icons.email),
      title: Text('E-posta'),
      subtitle: Text('[email protected]'),
      trailing: Icon(Icons.chevron_right),
    ),
  ],
)

ListView.builder ile Tembel (Lazy) Listeleme

ListView.builder, öğeleri baştan üretmez. itemBuilder fonksiyonunu yalnızca ekranda görünen (ve hemen çevresindeki önbellek alanına düşen) satırlar için çağırır. Kullanıcı kaydırdıkça yeni satırlar üretilir, ekrandan çıkanlar bırakılır. 100 öğede de 100.000 öğede de aynı anda hayatta olan satır sayısı aşağı yukarı ekrana sığan kadardır.

ListView.builder’da yalnızca ekranda görünen satırlar kurulur; üstte ve altta kalan satırlar için itemBuilder çağrılmaz

ListView.builder(
  itemCount: 100,
  itemBuilder: (context, index) {
    return ListTile(
      leading: CircleAvatar(child: Text('$index')),
      title: Text('Öğe $index'),
      subtitle: const Text('Bu öğenin açıklaması'),
    );
  },
)

ListTile yetmediğinde satırı istediğin gibi kurabilirsin. Satır içinde metnin taşmaması için Row içindeki metni Expanded ile sarmak iyi bir alışkanlıktır:

final List<Map<String, dynamic>> items = [
  {'title': 'Flutter', 'icon': Icons.flutter_dash, 'color': Colors.blue},
  {'title': 'Dart', 'icon': Icons.code, 'color': Colors.teal},
  {'title': 'Firebase', 'icon': Icons.cloud, 'color': Colors.orange},
];

ListView.builder(
  itemCount: items.length,
  padding: const EdgeInsets.all(16),
  itemBuilder: (context, index) {
    final item = items[index];
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            Icon(item['icon'], size: 40, color: item['color']),
            const SizedBox(width: 16),
            Expanded(
              child: Text(item['title'], style: const TextStyle(fontSize: 18)),
            ),
            ElevatedButton(
              onPressed: () {},
              child: const Text('Detay'),
            ),
          ],
        ),
      ),
    );
  },
)

ListView.separated ile Ayırıcılı Liste

Satırların arasına çizgi ya da boşluk koymak için her satırın sonuna Divider eklemek yerine ListView.separated kullan. separatorBuilder yalnızca öğelerin arasına çağrılır; son öğeden sonra ayırıcı çizilmez, bu yüzden "son satırın altındaki fazladan çizgi" sorunu yaşanmaz. Bu kurucu da builder gibi tembel çalışır.

ListView.separated(
  itemCount: 10,
  separatorBuilder: (context, index) => const Divider(height: 1),
  itemBuilder: (context, index) {
    return ListTile(title: Text('Öğe $index'));
  },
)

Yatay ListView

scrollDirection: Axis.horizontal listeyi yana yatırır. Dikkat: yatay liste yüksekliğini kendisi belirleyemez, ona sınırlı bir yükseklik vermen gerekir.

SizedBox(
  height: 120, // Yatay liste için sınırlı yükseklik gerekli
  child: ListView.builder(
    scrollDirection: Axis.horizontal,
    itemCount: 10,
    itemBuilder: (context, index) {
      return Container(
        width: 100,
        margin: const EdgeInsets.all(8),
        color: Colors.primaries[index % Colors.primaries.length],
        child: Center(child: Text('${index + 1}')),
      );
    },
  ),
)

itemExtent ve prototypeItem

Bütün satırlar aynı yükseklikteyse bunu ListView'e söylemek kaydırmayı ucuzlatır. Flutter her satırı tek tek ölçmek zorunda kalmaz; kaydırma çubuğunu sürüklemek ya da jumpTo ile uzak bir konuma atlamak gibi büyük sıçramalarda aradaki satırları yerleştirmeden hedefi hesaplayabilir.

ListView.builder(
  itemExtent: 72, // Her satır tam 72 piksel
  itemCount: 1000,
  itemBuilder: (context, index) => ListTile(title: Text('Öğe $index')),
)

Yüksekliği piksel olarak bilmiyorsan (yazı tipi boyutu kullanıcı ayarına göre değişebilir) prototypeItem ver; Flutter o örnek widget'ı ölçer ve bütün satırlara aynı yüksekliği uygular. İkisi aynı anda kullanılamaz.

ListView.builder(
  prototypeItem: const ListTile(title: Text('Örnek satır')),
  itemCount: 1000,
  itemBuilder: (context, index) => ListTile(title: Text('Öğe $index')),
)

Kaydırma Kontrolü

"Başa dön" butonu ya da sonsuz kaydırma için ScrollController kullanılır. Controller'ı State içinde oluştur ve dispose içinde kapat:

final ScrollController _scrollController = ScrollController();

@override
void dispose() {
  _scrollController.dispose();
  super.dispose();
}

// build içinde
ListView.builder(
  controller: _scrollController,
  itemCount: 50,
  itemBuilder: (context, index) => ListTile(title: Text('Öğe $index')),
)

// Listeyi en üste kaydır
_scrollController.animateTo(
  0,
  duration: const Duration(milliseconds: 500),
  curve: Curves.easeInOut,
);

Ne Zaman Kullanılır, Ne Zaman Kullanılmaz?

ListView'i, aynı türden ve sayısı belli olmayan öğeleri alt alta (ya da yan yana) gösterirken seç. Veri bir koleksiyondan geliyorsa neredeyse her zaman builder ya da separated kurucusuna git; children kurucusunu yalnızca elle yazdığın kısa menülere ayır.

Şu durumlarda başka bir widget daha doğru:

  • Form ya da ayar sayfası gibi birbirinden farklı, az sayıda widget varsa Column + SingleChildScrollView daha rahattır. Ayrıntısı ve ListView ile farkı SingleChildScrollView, ListView ve NestedScrollView yazısında.
  • Öğeler satır satır değil ızgara halinde dizilecekse GridView kullan.
  • Aynı ekranda liste, ızgara ve küçülen bir başlık birlikte kayacaksa CustomScrollView ve sliver'lar gerekir; toplanan bir başlığın altında her sekmenin kendi listesi varsa NestedScrollView. İkisinin farkı da ilk maddedeki yazıda.
  • İçerik zaten ekrana sığıyorsa kaydırmaya hiç gerek yok; düz bir Column yeter.

Sık Yapılan Hatalar

1. Column içine doğrudan ListView koymak

Belirti: ekran boş kalır ve konsolda şu hata görünür:

Vertical viewport was given unbounded height.

Column, çocuklarına "istediğin kadar uzun olabilirsin" der; ListView ise bulabildiği bütün yüksekliği kaplamak ister. İki sonsuz bir araya gelince Flutter boyutu hesaplayamaz. Çözüm, ListView'e Column'da kalan alanı vermektir:

Column(
  children: [
    const Text('Başlık'),
    Expanded(
      child: ListView.builder(
        itemCount: 50,
        itemBuilder: (context, index) => ListTile(title: Text('Öğe $index')),
      ),
    ),
  ],
)

Aynı sorunun yatay hali Horizontal viewport was given unbounded height. hatasıdır; orada çözüm yukarıdaki gibi SizedBox ile yükseklik vermektir.

2. Hatayı susturmak için her yere shrinkWrap: true yazmak

shrinkWrap: true yukarıdaki hatayı gerçekten giderir, ama bedeli vardır: ListView kendi yüksekliğini bilmek için içeriğinin tamamını ölçmek zorunda kalır. Bunu bir de SingleChildScrollView içine NeverScrollableScrollPhysics ile koyarsan bütün satırlar tek seferde oluşturulur ve builder'ın tembelliğinden geriye bir şey kalmaz. Belirti: liste uzadıkça ekranın açılışı yavaşlar, kaydırma takılır.

Beş on satırlık bir liste için shrinkWrap kabul edilebilir. Uzun listede ise üstteki içeriği listenin ilk öğesi yap:

ListView.builder(
  itemCount: items.length + 1,
  itemBuilder: (context, index) {
    if (index == 0) {
      return const Padding(
        padding: EdgeInsets.all(16),
        child: Text('Başlık alanı'),
      );
    }
    final item = items[index - 1];
    return ListTile(title: Text(item['title']));
  },
)

3. Büyük veriyi children ile vermek

ListView(children: products.map((p) => ProductRow(p)).toList()) çalışır, ama map(...).toList() her build çağrısında bütün satır widget'larını yeniden üretir. Belirti: her setState sonrasında hissedilen takılma. Çözüm aynı veriyi ListView.builder ile vermek; itemCount: products.length ve itemBuilder içinde ProductRow(products[index]).

Mini Senaryo: Görev Listesi Ekranı

Bir görev listesi düşün: üstte kalan görev sayısı, altında işaretlenebilir görevler, liste boşsa bir bilgi mesajı. Üstteki sayaç sabit kalacak, liste kayacak; bu yüzden Column + Expanded + ListView.separated üçlüsü tam yerinde.

class Task {
  Task(this.title, {this.done = false});

  final String title;
  bool done;
}

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

  @override
  State<TaskListPage> createState() => _TaskListPageState();
}

class _TaskListPageState extends State<TaskListPage> {
  final List<Task> _tasks = [
    Task('Flutter dersini bitir'),
    Task('ListView örneğini yaz', done: true),
    Task('Projeyi depoya gönder'),
  ];

  @override
  Widget build(BuildContext context) {
    final remaining = _tasks.where((task) => !task.done).length;

    return Scaffold(
      appBar: AppBar(title: const Text('Görevler')),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Text('$remaining görev kaldı'),
          ),
          Expanded(
            child: _tasks.isEmpty
                ? const Center(child: Text('Henüz görev yok'))
                : ListView.separated(
                    itemCount: _tasks.length,
                    separatorBuilder: (context, index) =>
                        const Divider(height: 1),
                    itemBuilder: (context, index) {
                      final task = _tasks[index];
                      return CheckboxListTile(
                        value: task.done,
                        title: Text(task.title),
                        onChanged: (value) {
                          setState(() => task.done = value ?? false);
                        },
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }
}

Burada üç karar var. Expanded, listeye sayaçtan arta kalan alanı vererek "unbounded height" hatasını baştan engelliyor. Boş durum kontrolü ListView'in dışında yapılıyor, çünkü boş bir liste kullanıcıya hiçbir şey söylemez. Ayırıcılar da satırın parçası olmak yerine separatorBuilder'dan geliyor. Veri internetten geliyorsa aynı yapıyı FutureBuilder içine yerleştirirsin.

Sık Sorulan Sorular

ListView ile ListView.builder arasındaki fark nedir?

ListView(children: [...]) bütün satır widget'larını baştan oluşturur, ListView.builder ise yalnızca ekranda görünen satırlar için itemBuilder çağırır. Kısa ve sabit menülerde ilki, koleksiyondan gelen ya da uzun olabilecek her listede ikincisi tercih edilir.

"Vertical viewport was given unbounded height" hatası nasıl çözülür?

ListView'i yüksekliği sınırsız olan bir Column içine koyduğunda oluşur. ListView'i Expanded ile sar ya da SizedBox ile sabit bir yükseklik ver; shrinkWrap: true yalnızca kısa listelerde makul bir çözümdür.

shrinkWrap: true neden yavaş?

Liste, kendi boyutunu belirlemek için içeriğinin tamamını ölçmek zorunda kalır ve kaydırma sırasında bu boyut yeniden hesaplanır. Uzun listelerde bu, tembel oluşturmanın avantajını ortadan kaldırır.

ListView'in üstünde neden kendiliğinden boşluk oluşuyor?

Ekranda AppBar yoksa ListView, durum çubuğu gibi sistem alanlarına denk gelmemek için MediaQuery dolgusunu varsayılan padding olarak uygular. İstemiyorsan padding: EdgeInsets.zero ver.

Yorumlar