Flutter: GridView Widget Kullanımı
Son güncelleme:
6 dk okuma

GridView, öğeleri satır ve sütunlardan oluşan bir ızgaraya dizen ve ekrana sığmayanları kaydırılabilir yapan widget'tır. Fotoğraf galerisi, ürün kataloğu, kategori seçme ekranı gibi "aynı boyutta kutuların yan yana dizildiği" her yerde karşına çıkar. ListView'in ızgara hali diyebilirsin: aynı kurucu mantığı, aynı tembel (lazy) oluşturma, artı bir de hücrelerin boyutunu belirleyen bir delegate. Bu yazıdaki asıl mesele o delegate'i ve hücre oranını doğru ayarlamak; GridView'de hataların çoğu buradan çıkar.
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: GridView.count
En kısa yol sütun sayısını söylemektir. crossAxisCount: 2 dediğinde Flutter mevcut genişliği ikiye böler ve her hücreyi kare yapar:
GridView.count(
crossAxisCount: 2, // 2 sütun
children: [
Container(color: Colors.red, child: const Center(child: Text('1'))),
Container(color: Colors.green, child: const Center(child: Text('2'))),
Container(color: Colors.blue, child: const Center(child: Text('3'))),
Container(color: Colors.orange, child: const Center(child: Text('4'))),
],
)children kurucusu, ListView'deki gibi bütün hücreleri baştan oluşturur. Elle yazdığın on-on beş hücrelik bir menü için sorun yok; veri bir listeden geliyorsa aşağıdaki builder kurucusuna geç.
Önemli Özellikler
| Özellik | Açıklama |
|---|---|
crossAxisCount |
Sütun sayısı (count kurucusu ve FixedCrossAxisCount delegate'i) |
maxCrossAxisExtent |
Bir hücrenin alabileceği en fazla genişlik (extent kurucusu ve MaxCrossAxisExtent delegate'i) |
mainAxisSpacing |
Satırlar arası boşluk |
crossAxisSpacing |
Sütunlar arası boşluk |
childAspectRatio |
Hücrenin en/boy oranı (genişlik ÷ yükseklik), varsayılan 1 |
mainAxisExtent |
Hücre yüksekliğini oran yerine piksel olarak sabitler |
padding |
Izgaranın etrafındaki boşluk |
shrinkWrap / physics |
İç içe kaydırmada kullanılır, aşağıda maliyetiyle birlikte anlatıyorum |
GridView.extent ile Maksimum Genişlik
Sütun sayısı yerine "bir hücre en fazla şu kadar geniş olsun" dersin; Flutter ekrana kaç sütun sığıyorsa onu hesaplar. Telefonu yan çevirince ya da tablette otomatik olarak daha çok sütun çıkar:
GridView.extent(
maxCrossAxisExtent: 150, // Her hücre en fazla 150 piksel genişliğinde
mainAxisSpacing: 8,
crossAxisSpacing: 8,
padding: const EdgeInsets.all(8),
children: [
Container(color: Colors.purple, child: const Center(child: Text('A'))),
Container(color: Colors.pink, child: const Center(child: Text('B'))),
Container(color: Colors.cyan, child: const Center(child: Text('C'))),
Container(color: Colors.amber, child: const Center(child: Text('D'))),
],
)GridView.builder ile Tembel Izgara
Veri bir listeden geliyorsa GridView.builder kullan. itemBuilder yalnızca ekranda görünen hücreler için çağrılır; 20 öğede de 20.000 öğede de aynı anda hayatta olan hücre sayısı ekrana sığan kadardır. Bu kurucuda sütun düzenini bir gridDelegate belirler:
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: 20,
padding: const EdgeInsets.all(8),
itemBuilder: (context, index) {
return Container(
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length],
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
'${index + 1}',
style: const TextStyle(color: Colors.white, fontSize: 24),
),
),
);
},
)İki Delegate: FixedCrossAxisCount ve MaxCrossAxisExtent
gridDelegate için iki hazır sınıf var ve aralarındaki fark, count ile extent arasındaki farkın aynısı:
SliverGridDelegateWithFixedCrossAxisCount: sütun sayısı sabittir, hücre genişliği ekrana göre değişir. Tasarımda "her zaman 2 sütun" gibi kesin bir karar varsa bunu seç.SliverGridDelegateWithMaxCrossAxisExtent: hücre genişliğine üst sınır koyarsın, sütun sayısı ekrana göre değişir. Telefon, tablet ve web'i aynı kodla desteklemek istiyorsan bu daha rahattır; ayrı bir LayoutBuilder ile breakpoint yazmadan makul bir responsive davranış verir.
GridView.builder(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1.5,
),
itemCount: 12,
itemBuilder: (context, index) {
return Card(
color: Colors.teal.shade100,
child: Center(child: Text('Öğe $index')),
);
},
)childAspectRatio Nasıl Hesaplanır?
GridView'de hücre yüksekliği içeriğe göre değil, orana göre belirlenir. Flutter önce hücre genişliğini bulur: kullanılabilir genişlikten padding ve sütun boşlukları düşülür, kalan sütun sayısına bölünür. Sonra yüksekliği genişlik ÷ childAspectRatio ile hesaplar. Yani childAspectRatio: 16 / 9 geniş ve basık kartlar, 0.8 ise dikey kartlar verir:
GridView.count(
crossAxisCount: 2,
childAspectRatio: 16 / 9, // Geniş kartlar
mainAxisSpacing: 8,
crossAxisSpacing: 8,
children: [
Container(color: Colors.red),
Container(color: Colors.blue),
Container(color: Colors.green),
Container(color: Colors.orange),
],
)Hücrenin içinde ne olduğu bu hesaba girmez. Bu, GridView'in en çok yanlış anlaşılan tarafıdır ve aşağıdaki "taşma" hatasının kaynağıdır. Yüksekliği piksel olarak biliyorsan oranla uğraşma, mainAxisExtent: 180 ver; delegate o zaman oranı yok sayar ve her hücreyi 180 piksel yapar.
Özel Tasarımlı Grid Kartları
Ürün kartı gibi ikon, başlık, fiyat ve buton içeren hücrelerde dikey oran gerekir. Aşağıdaki örnekte childAspectRatio: 0.8 ile hücre genişliğinden biraz daha uzun yapılmıştır; metin uzarsa taşma yaşamamak için başlığı maxLines ile sınırlamak iyi bir alışkanlıktır:
final List<Map<String, dynamic>> products = [
{'name': 'Ürün 1', 'price': '₺99', 'icon': Icons.phone_android},
{'name': 'Ürün 2', 'price': '₺149', 'icon': Icons.laptop},
{'name': 'Ürün 3', 'price': '₺299', 'icon': Icons.watch},
{'name': 'Ürün 4', 'price': '₺79', 'icon': Icons.headphones},
];
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 0.8,
),
padding: const EdgeInsets.all(16),
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(product['icon'], size: 48, color: Colors.blue),
const SizedBox(height: 12),
Text(
product['name'],
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(
product['price'],
style: const TextStyle(color: Colors.green, fontSize: 18),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () {},
child: const Text('Satın Al'),
),
],
),
);
},
)Galeri ekranında ise kare hücreler ve BoxFit.cover yeter:
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
),
itemCount: 30,
itemBuilder: (context, index) {
return Image.network(
'https://picsum.photos/200?random=$index',
fit: BoxFit.cover,
);
},
)Ne Zaman Kullanılır, Ne Zaman Kullanılmaz?
GridView'i, aynı boyutta ve aynı türden hücrelerin ekrana sığmayacak kadar çok olduğu durumlarda seç: galeri, katalog, ikon paleti. Veri koleksiyondan geliyorsa builder, sütun sayısı ekrana göre değişmeliyse MaxCrossAxisExtent delegate'i doğru başlangıçtır.
Şu durumlarda başka bir widget daha doğru:
- Öğeler farklı boyutlarda ve sayıları azsa (etiket bulutu, filtre çipleri)
Wrapkullan; kaydırma gerekmez, her öğe kendi boyutunu alır. - Öğeler satır satır alt alta duruyorsa ızgaraya gerek yok, ListView daha sade.
- Izgara, aynı ekranda bir başlık, yatay bir liste ya da küçülen bir AppBar ile birlikte kayacaksa
CustomScrollViewiçindeSliverGridkullan. Bu iç içe kaydırma kararlarını SingleChildScrollView, ListView ve NestedScrollView yazısında anlattım. - Hücrelerin yükseklikleri içeriğe göre farklı olacaksa (Pinterest tarzı masonry düzen) GridView bunu desteklemez; bunun için
flutter_staggered_grid_viewgibi bir paket gerekir.
Sık Yapılan Hatalar
1. childAspectRatio'yu içeriğe göre ayarlamamak
Belirti: hücrelerin altında sarı-siyah çizgiler ve konsolda şu mesaj:
A RenderFlex overflowed by 18 pixels on the bottom.Hücrenin yüksekliği orandan geliyor, içerik ise ondan uzun. Üç çözüm var: oranı küçült (childAspectRatio: 0.75 gibi, hücre uzar), yüksekliği mainAxisExtent ile piksel olarak sabitle ya da hücre içindeki Column'ı esnek yap. Üçüncü yol için metni Expanded ya da Flexible ile sar ve maxLines ver:
Column(
children: [
const Icon(Icons.image, size: 40),
Expanded(
child: Text(
product['name'],
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
Text(product['price']),
],
)Kullanıcı sistemden yazı boyutunu büyüttüğünde de aynı taşma çıkar; bu yüzden hücreyi "tam sığacak" değil, "biraz payı olacak" şekilde tasarla.
2. Column içine doğrudan GridView koymak
Belirti: ekran boş kalır, konsolda Vertical viewport was given unbounded height. görünür. GridView de ListView gibi bulabildiği bütün yüksekliği ister; Column ise sınırsız yükseklik verir. Çözüm GridView'i Expanded ile sarmaktır:
Column(
children: [
const Padding(
padding: EdgeInsets.all(16),
child: Text('Kategoriler'),
),
Expanded(
child: GridView.count(
crossAxisCount: 3,
children: const [/* ... */],
),
),
],
)3. shrinkWrap ve NeverScrollableScrollPhysics'i alışkanlık haline getirmek
GridView'i bir SingleChildScrollView içine koyduğunda Expanded işe yaramaz; o zaman şu ikili gerekir:
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
// ...
)shrinkWrap: true, ızgaranın kendi yüksekliğini bilmesi için bütün hücreleri ölçmesine yol açar; NeverScrollableScrollPhysics ise kaydırmayı dıştaki widget'a bırakır. Sonuç: bütün hücreler tek seferde oluşturulur, builder'ın tembelliği kaybolur. On-on beş hücrelik bir kategori bloğu için kabul edilebilir. Yüzlerce ürün için değil; orada üstteki içerikle ızgarayı aynı CustomScrollView içinde SliverToBoxAdapter ve SliverGrid olarak birleştir:
CustomScrollView(
slivers: [
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.all(16),
child: Text('Öne çıkan ürünler'),
),
),
SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
delegate: SliverChildBuilderDelegate(
(context, index) => Card(child: Center(child: Text('Ürün $index'))),
childCount: 200,
),
),
],
)4. Sabit sütun sayısını her ekranda kullanmak
Belirti: telefonda güzel görünen crossAxisCount: 2, tablette ekranın yarısı kadar dev kartlar üretir; tersine, tablette ayarlanan 4 sütun telefonda okunmayacak kadar küçük hücreler verir. Çözüm ya MaxCrossAxisExtent delegate'ine geçmek ya da sütun sayısını genişlikten hesaplamaktır: (constraints.maxWidth / 180).floor().clamp(2, 6) gibi.
Mini Senaryo: Kategori Seçme Ekranı
Bir alışveriş uygulamasında kullanıcının ilgi alanlarını seçtiği ekranı düşün: ikon ve isimden oluşan kartlar, dokununca seçili hale geliyor, üstte kaç kategori seçildiği yazıyor. Kartlar aynı boyutta ve sayıları belli olmadığı için GridView.builder, tablette de düzgün dursun diye MaxCrossAxisExtent, yükseklik içeriğe göre bilindiği için mainAxisExtent kullanıyoruz.
class CategoryPickerPage extends StatefulWidget {
const CategoryPickerPage({super.key});
@override
State<CategoryPickerPage> createState() => _CategoryPickerPageState();
}
class _CategoryPickerPageState extends State<CategoryPickerPage> {
static const _categories = [
('Elektronik', Icons.devices),
('Kitap', Icons.menu_book),
('Spor', Icons.sports_soccer),
('Moda', Icons.checkroom),
('Ev', Icons.chair),
('Oyun', Icons.sports_esports),
];
final Set<int> _selected = {};
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(title: Text('${_selected.length} kategori seçildi')),
body: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 160,
mainAxisExtent: 120,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
),
itemCount: _categories.length,
itemBuilder: (context, index) {
final (name, icon) = _categories[index];
final isSelected = _selected.contains(index);
return Card(
color: isSelected ? colorScheme.primaryContainer : null,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
setState(() {
isSelected ? _selected.remove(index) : _selected.add(index);
});
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 36),
const SizedBox(height: 8),
Text(name, maxLines: 1, overflow: TextOverflow.ellipsis),
],
),
),
);
},
),
);
}
}Burada üç karar var. maxCrossAxisExtent: 160 sayesinde telefonda iki, tablette dört-beş sütun kendiliğinden çıkıyor. mainAxisExtent: 120 ile hücre yüksekliği ekran genişliğinden bağımsız hale geliyor; oran kullansaydık geniş ekranda kartlar gereksiz uzardı. Seçim durumu bir Set<int> içinde tutuluyor ve setState yalnızca bu ekranı yeniliyor; seçilen kategoriler başka ekranlarda da lazımsa bu state'i Provider gibi bir yapıya taşırsın. Kategoriler bir API'den geliyorsa aynı ızgarayı FutureBuilder içine yerleştirmen yeterli.
Sık Sorulan Sorular
GridView.count ile GridView.builder arasındaki fark nedir?
GridView.count hazır bir children listesi alır ve bütün hücreleri baştan oluşturur; GridView.builder ise itemBuilder'ı yalnızca görünen hücreler için çağırır. Elle yazılmış kısa menülerde ilki, koleksiyondan gelen her veride ikincisi tercih edilir.
Grid hücrelerinin altında neden taşma (overflow) hatası alıyorum?
Hücre yüksekliği içeriğe göre değil, childAspectRatio ile hücre genişliğinden hesaplanır. İçerik bu yükseklikten uzunsa "A RenderFlex overflowed" hatası çıkar; oranı küçült, mainAxisExtent ile yüksekliği sabitle ya da hücre içindeki metni Expanded ve maxLines ile esnek yap.
Sütun sayısını ekran boyutuna göre nasıl değiştiririm?
SliverGridDelegateWithMaxCrossAxisExtent kullan; hücre için en fazla genişlik verirsin, Flutter sütun sayısını ekrana göre hesaplar. Tam kontrol istiyorsan LayoutBuilder içinde genişlikten bir sütun sayısı türetip FixedCrossAxisCount delegate'ine verebilirsin.
GridView'i SingleChildScrollView içine nasıl koyarım?
shrinkWrap: true ve physics: const NeverScrollableScrollPhysics() ver; ancak bu durumda bütün hücreler tek seferde oluşturulur. Uzun ızgaralarda bunun yerine CustomScrollView içinde SliverGrid kullanmak daha doğrudur.
İlgili Yazılar
Flutter: Expanded, Flexible ve Spacer Farkı ve Tarifler
Expanded, Flexible ve Spacer arasındaki fark, FlexFit.loose ile tight ayrımı ve üçünü birlikte kullanan dört tarif: araç çubuğu, kart, form, bölünmüş ekran.
Flutter: ListView Widget Kullanımı
Flutter ListView rehberi: builder ile tembel listeleme, separated, itemExtent, shrinkWrap maliyeti ve unbounded height hatasının çözümü.
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.