Flutter: ListView Widget Kullanımı
Son güncelleme:
5 dk okuma

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(
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+SingleChildScrollViewdaha 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
CustomScrollViewve sliver'lar gerekir; toplanan bir başlığın altında her sekmenin kendi listesi varsaNestedScrollView. İkisinin farkı da ilk maddedeki yazıda. - İçerik zaten ekrana sığıyorsa kaydırmaya hiç gerek yok; düz bir
Columnyeter.
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.
İlgili Yazılar
Flutter: SingleChildScrollView, ListView ve NestedScrollView
SingleChildScrollView, ListView ve NestedScrollView farkı, klavye açılınca taşma, Expanded unbounded height hatası ve sekmeli NestedScrollView kurulumu.
Flutter: GridView Widget Kullanımı
Flutter GridView rehberi: count, extent ve builder kurucuları, iki grid delegate'in farkı, childAspectRatio taşma hatası ve shrinkWrap maliyeti.
Flutter: SizedBox ile Boyutlandırma ve Boşluk
SizedBox ile sabit boyut ve boşluk: expand, shrink, square kurucuları, Container farkı, verdiğiniz boyutun neden yok sayıldığı ve sık yapılan hatalar.