Flutter: LayoutBuilder ile Ekran Boyutuna Göre Tasarım
Son güncelleme:
6 dk okuma

LayoutBuilder, build sırasında parent'ından gelen BoxConstraints'i size verir ve o kısıtlamalara göre farklı bir widget ağacı döndürmenize izin verir. Telefonda alt alta, tablette yan yana dizilen bir kart; yalnızca genişliği yettiğinde görünen bir yan panel; kendisine ayrılan alanın yüzde sekseni kadar bir görsel: hepsi bu widget'la yapılır. Bu yazı kavramın kendisine odaklanıyor: LayoutBuilder'ın MediaQuery'den farkı, constraints nesnesinde ne olduğu, sınırsız yükseklik ve "intrinsic dimensions" hatalarının nereden geldiği. Ardından Material 3 ekran sınıflarına dayanan bir telefon/tablet/masaüstü breakpoint düzenine geçiyor. Ekranı oranlama tarafını MediaQuery yazısında ayrıca anlattım.
Canlı Demo: LayoutBuilder Responsive Tasarım
Örneği dikey çizgiden genişletip daraltarak layout'un nasıl değiştiğini görün:
💡 Eğer yukarıdaki örnek açılmazsa, DartPad linkine tıklayarak yeni sekmede çalıştırabilirsiniz.
Temel Kullanım
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Text('Kullanılabilir genişlik: ${constraints.maxWidth}');
},
)builder, normal bir build metodundan farklı olarak layout aşamasında çağrılır; yani parent'ın verdiği kısıtlama belli olduktan sonra. Bu sayede döndürdüğünüz widget ağacı gerçek alana göre şekillenir. Kısıtlama değiştiğinde (ekran döndü, pencere yeniden boyutlandı, yan panel açıldı) builder yeniden çalışır.
BoxConstraints nesnesinde dört sınır vardır: minWidth, maxWidth, minHeight, maxHeight. Yanına birkaç yardımcı özellik eklenmiştir: hasBoundedWidth ve hasBoundedHeight o eksende bir üst sınır olup olmadığını söyler, isTight min ve max'ın eşit olduğunu, biggest ise sığabilecek en büyük Size'ı verir. Pratikte en çok maxWidth okunur; maxHeight'a bakmadan önce mutlaka hasBoundedHeight kontrol edin, aşağıda nedenini anlatacağım.
LayoutBuilder ile MediaQuery Farkı
En kritik kavram şudur: MediaQuery.sizeOf(context) ekranın boyutunu verir, LayoutBuilder ise parent'ın size ayırdığı alanı. İkisi yalnızca widget'ınız tüm ekranı kaplıyorsa aynı sayıyı söyler.
Bir ürün kartı düşünün. Aynı kart hem telefonda tam genişlikte bir listede, hem tablette 300 piksellik bir yan panelde kullanılıyor. Kart "ekran 900 pikselden genişse yatay diz" diye MediaQuery'ye bakarsa, tabletteki yan panelde 300 piksele yatay bir düzen sıkıştırmaya çalışır ve taşar. LayoutBuilder ile "bana 500 pikselden az verildiyse dikey diz" derse, nereye koyulursa koyulsun doğru davranır.
Kural: Karar widget'ın kendi alanıyla ilgiliyse LayoutBuilder, cihazla ilgiliyse (yönelim, klavye yüksekliği, güvenli alan, metin ölçeği) MediaQuery.
Breakpoint'ler ve Ekran Sınıfları
Material 3, pencere genişliğini sınıflara böler: 600 dp altı compact (telefon), 600 ile 839 arası medium (küçük tablet, yatay telefon), 840 ve üstü expanded (tablet, masaüstü). Çok geniş ekranlar için 1200 dp'den başlayan large ve extra-large sınıfları da var. Eşikleri bu sınıflardan almak, rastgele sayılar seçmekten daha tutarlıdır:
enum ScreenType { compact, medium, expanded }
ScreenType screenTypeFor(double width) {
if (width < 600) return ScreenType.compact;
if (width < 840) return ScreenType.medium;
return ScreenType.expanded;
}
LayoutBuilder(
builder: (context, constraints) => switch (screenTypeFor(constraints.maxWidth)) {
ScreenType.compact => const MobileBody(),
ScreenType.medium => const TabletBody(),
ScreenType.expanded => const DesktopBody(),
},
)Eşik değerlerini tek bir fonksiyonda tutun; < 600 sayısı kodun on yerine dağılırsa sonradan değiştirmek eziyet olur. Sütun sayısını da aynı yerden türetebilirsiniz: GridView'da crossAxisCount için constraints.maxWidth ~/ 180 gibi bir hesap (sonucun en az 1 olmasına dikkat ederek), sabit breakpoint'lerden daha esnek çalışır.
Her sınıfta iskelet nasıl değişir?
Breakpoint yalnızca bir eşik değildir; her sınıfta ekranın iskeleti de değişir. Sık kullanılan dağılım şöyle:
- Compact: Tek sütun, içerik dikey akar; ana gezinme altta durur.
- Medium: Gezinme yana taşınabilir; liste ile detay yan yana gelmeye başlar. Kalan genişliği sütunlara paylaştırmak için Expanded en pratik araçtır:
Row(children: [SizedBox(width: 240, child: SideMenu()), Expanded(child: Content())]). - Expanded ve üstü: Birden fazla panel yan yana durur ve içerik bir üst genişlikle sınırlanır.
Son maddenin nedeni okunabilirlik: geniş bir monitörde metin satırları ekran boyunca uzarsa göz satır sonundan bir sonraki satırın başına dönerken kaybolur, içeriği sola yaslayıp sağ tarafı boş bırakmak da sayfayı yarım gösterir. İçeriği ortalayıp bir üst genişlikle sınırlamak ikisini de çözer:
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 1200),
child: const Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(flex: 2, child: ContentArea()),
SizedBox(width: 24),
Expanded(child: Sidebar()),
],
),
),
)Buradaki 1200 sabit bir kural değil, tasarımınızın okunabilir genişliği. Aynı düzen bir pencere içinde de çalışır: masaüstünde kullanıcı pencereyi daralttığında LayoutBuilder yeniden çalışır ve düzen bir alt sınıfa geçer.
Oransal Boyutlandırma
constraints.maxWidth * 0.8 gibi oranlar genişlik için işe yarar. Ama yazı boyutunu genişliğe oranlamayın: küçük telefonda okunmaz, geniş monitörde afiş gibi olur. Yazı boyutu tema ve kullanıcının erişilebilirlik ayarından gelmelidir. Yalnızca oransal genişlik istiyorsanız LayoutBuilder'a bile gerek yok; FractionallySizedBox(widthFactor: 0.8, child: ...) aynı işi tek widget'la yapar.
Ne Zaman Kullanılır, Ne Zaman Kullanılmaz?
LayoutBuilder'a uzanın:
- Bir widget kendi alanına göre farklı yapı kurmalıysa (Row/Column geçişi, panel gizleme, sütun sayısı).
- Yeniden kullanılabilir bir bileşen yazıyorsanız ve nereye konacağını bilmiyorsanız.
- Parent'ın sınırsız olup olmadığını öğrenip ona göre davranmanız gerekiyorsa.
Başka araca geçin:
- Ekranla ilgili kararlar için
MediaQuery(orientationOf,paddingOf,textScalerOf). - Yalnızca dikey/yatay ayrımı için
OrientationBuilder; içeride zaten LayoutBuilder kullanır ama okunması daha kolaydır. - Kalan alanı paylaştırmak için
ExpandedveFlexible; ölçüm gerektirmez. - Sığmayanı alt satıra atmak için
Wrap; chip veya etiket listelerinde breakpoint yazmaktan kurtarır.
Sık Yapılan Hatalar
1. Column veya ListView içinde maxHeight okumak
Belirti: BoxConstraints forces an infinite height. ya da Container yüksekliği Infinity olduğu için layout hatası. Column ve kaydırılabilir widget'lar çocuklarına ana eksende sınırsız yükseklik verir; constraints.maxHeight bu durumda double.infinity döner ve onu bir boyuta çevirmek patlar.
LayoutBuilder(
builder: (context, constraints) {
final height = constraints.hasBoundedHeight
? constraints.maxHeight * 0.5
: 200.0; // sınırsızsa makul bir sabit
return SizedBox(height: height, child: const Placeholder());
},
)Çözüm: hasBoundedHeight ile kontrol edin ya da LayoutBuilder'ı yüksekliği belli olan bir yere (Expanded, SizedBox, Scaffold body) taşıyın.
2. "LayoutBuilder does not support returning intrinsic dimensions"
Belirti: Konsolda tam olarak bu hata. LayoutBuilder çocuğunu ancak kısıtlama geldikten sonra bilir; bu yüzden "kendi doğal boyutun ne?" sorusuna cevap veremez. Bu soruyu soran widget'lar IntrinsicHeight, IntrinsicWidth, Table'ın intrinsic sütun genişlikleri ve dikkat: AlertDialog'un içerik alanıdır. Bir dialog içine doğrudan LayoutBuilder koyduğunuzda bu hatayı görürsünüz. Çözüm: Intrinsic sarmalayıcıyı kaldırın, dialog'a SizedBox(width: 400, child: ...) gibi somut bir boyut verin ya da kararı LayoutBuilder yerine dışarıdan parametreyle geçirin.
3. builder içinde setState çağırmak
Belirti: setState() or markNeedsBuild() called during build. builder layout aşamasında çalışır; orada state değiştirmek ağacı tutarsız bırakır. Genişliğe bağlı bir değeri state'e yazmak yerine builder içinde yerel değişken olarak hesaplayın ve doğrudan kullanın.
4. LayoutBuilder'ı ekran ölçmek için kullanmak
Belirti: Yan panel içindeki bir widget "masaüstü" yerine "mobil" düzenine düşüyor. LayoutBuilder parent'ın alanını söyler, ekranı değil; 300 piksellik bir panelde her zaman 300 okursunuz. Cihaz kararı istiyorsanız MediaQuery.sizeOf(context).width kullanın. Bu aslında hata değil, LayoutBuilder'ın tam olarak yapması gereken şeydir; sürpriz olmaması için niyetinizi netleştirin.
Mini Senaryo: Her Yere Uyan Ürün Kartı
Bir e-ticaret uygulamasında ürün kartı hem ana sayfadaki tam genişlik listede, hem sepet ekranındaki dar kolonda kullanılıyor. Kart, kendisine verilen alan 420 pikselin altındaysa görseli üste alır; üstündeyse görsel solda, metin sağda durur.
class ProductCard extends StatelessWidget {
const ProductCard({super.key, required this.title, required this.price, required this.imageUrl});
final String title;
final String price;
final String imageUrl;
@override
Widget build(BuildContext context) {
return Card(
clipBehavior: Clip.antiAlias,
child: LayoutBuilder(
builder: (context, constraints) {
final isNarrow = constraints.maxWidth < 420;
final image = AspectRatio(
aspectRatio: isNarrow ? 16 / 9 : 1,
child: Image.network(imageUrl, fit: BoxFit.cover),
);
final info = Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(title, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Text(price, style: Theme.of(context).textTheme.bodyLarge),
const SizedBox(height: 12),
FilledButton(onPressed: () {}, child: const Text('Sepete Ekle')),
],
),
);
if (isNarrow) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [image, info],
);
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(width: 160, child: image),
Expanded(child: info),
],
);
},
),
);
}
}Kart, nerede kullanıldığını bilmiyor ve bilmesine gerek yok: telefonda listede 360 piksel alır ve dikey diziliş seçer; tablette 700 piksellik bir alanda yatay dizilişe geçer; aynı tablette 300 piksellik yan panele koyulduğunda yeniden dikeye döner. MediaQuery ile yazsaydınız üçüncü durum bozulurdu. Yatay düzende Expanded, metnin görselden arta kalan genişliği almasını sağlar; neden gerektiğini Expanded yazısında anlattım.
Sık Sorulan Sorular
LayoutBuilder ile MediaQuery arasındaki fark nedir?
MediaQuery ekranın (daha doğrusu uygulama penceresinin) boyutunu, LayoutBuilder ise parent'ın o widget'a ayırdığı alanı verir. Widget tüm ekranı kaplamıyorsa iki değer farklıdır; bileşen kendi alanına göre karar veriyorsa LayoutBuilder doğru araçtır.
LayoutBuilder her frame yeniden build eder mi?
Hayır. builder yalnızca parent'tan gelen kısıtlama değiştiğinde ya da LayoutBuilder'ın kendisi yeniden build edildiğinde çalışır. Yine de builder içinde ağır hesap yapmayın; kısıtlama değişimleri pencere yeniden boyutlandırırken art arda gelir.
constraints.maxHeight neden Infinity dönüyor?
Parent o eksende sınır vermediği için; Column, ListView ve SingleChildScrollView ana eksende sınırsız kısıtlama verir. hasBoundedHeight ile kontrol edin ya da LayoutBuilder'ı yüksekliği belli bir parent'ın altına taşıyın.
LayoutBuilder çocuğun boyutunu öğrenmek için kullanılabilir mi?
Hayır. LayoutBuilder çocuğa ne kadar alan verilebileceğini söyler, çocuğun sonunda ne kadar yer kapladığını değil. Bir widget'ın gerçek boyutunu okumak için layout tamamlandıktan sonra GlobalKey ile RenderBox'a ulaşmak gibi başka teknikler gerekir.
Hangi breakpoint değerlerini kullanmalıyım?
Sıfırdan başlıyorsanız Material 3 ekran sınıfları iyi bir temel: 600 dp altı compact, 600 ile 839 arası medium, 840 ve üstü expanded. Değerleri tek bir fonksiyonda tutun ve kararı ekrana göre değil, LayoutBuilder'dan gelen genişliğe göre verin; böylece aynı bileşen dar bir panelde de doğru davranır.
İlgili Yazılar
Flutter: MediaQuery ile Ekrana Göre Oranlama
MediaQuery.sizeOf, paddingOf ve textScalerOf ile ekrana göre boyutlama; ne zaman LayoutBuilder veya FractionallySizedBox daha doğru olur ve sık yapılan hatalar.
Flutter: BottomSheet ve showModalBottomSheet Kullanımı
Flutter'da showModalBottomSheet ile değer döndürmek, isScrollControlled, useSafeArea, showDragHandle, klavye boşluğu, DraggableScrollableSheet ve kalıcı sheet.
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.