Flutter: Expanded, Flexible ve Spacer Farkı ve Tarifler
Son güncelleme:
6 dk okuma

Row ve Column içinde alanı paylaştırmanın üç aracı var: Expanded kalan alanı doldurur, Flexible çocuğun küçük kalmasına izin verir, Spacer yalnızca boşluk açar. Bu yazı önce üçünün farkını tek bir satır üzerinde gösteriyor, sonra onları aynı düzende birleştiren dört günlük senaryoyu (araç çubuğu, kart alt satırı, form satırları, responsive ikiye bölünmüş ekran) kod ve gerekçesiyle anlatıyor. Tek tek widget'ların ayrıntısı ayrı yazılarda: flex hesabı, iç içe kullanım ve Expanded hataları Expanded derinlemesine yazısında, boşluk oranları ve kaydırılabilir alanlardaki tuzaklar Spacer yazısında.
Canlı Demo
Aşağıdaki interaktif örnekte Expanded'ı deneyebilirsiniz:
💡 Eğer yukarıdaki örnek açılmazsa, DartPad linkine tıklayarak yeni sekmede çalıştırabilirsiniz.
Temel Fark
Üçü de aynı mekanizmanın parçası: Row veya Column önce sabit boyutlu çocukları ölçer, geriye kalan alanı flex değeri olan çocuklar arasında paylaştırır. Aralarındaki fark, çocuğun kendisine ayrılan payla ne yaptığıdır.
Expanded: Payın tamamını kaplamak zorundadır. AslındaFlexible(fit: FlexFit.tight)widget'ının kısa yazımıdır.Flexible: VarsayılanfitdeğeriFlexFit.loose'tur, yani çocuk payına "en fazla" bu kadar yer kaplayabilir. Kendi boyutu daha küçükse küçük kalır, sığmıyorsa payın içine sıkıştırılır.Spacer: İçi boş birExpanded'dır; payı görünmez bir boşluk olarak kullanır ve içerik taşımaz.
Flexible ile Expanded arasındaki farkı görmek için çocuğun kendine ait bir boyutu olmalı. Kısa bir etiket bunun için yeterli:
Row(
children: [
Flexible(
child: Container(
color: Colors.green,
padding: const EdgeInsets.all(8),
child: const Text('Kısa etiket'),
),
),
const SizedBox(width: 8),
const Icon(Icons.info_outline),
],
)Bu satırda yeşil alan metin kadar geniştir ve ikon hemen yanında durur. Metni satıra sığmayacak kadar uzatırsanız Flexible onu kalan genişlikle sınırlar ve metin alt satıra geçer; taşma hatası oluşmaz. Aynı kodda Flexible yerine Expanded yazdığınızda yeşil alan kısa metinde de ikona kadar uzanır ve ikon satırın sağ ucuna itilir.
Karar Tablosu
Tariflere geçmeden önce hangi çocuğa hangi widget'ın geleceğine karar vermenin kısa yolu:
| İhtiyaç | Araç |
|---|---|
| Çocuk kalan alanın tamamını doldursun | Expanded |
| Çocuk kendi boyutunda kalsın, sığmazsa küçülsün | Flexible |
| İki öğe arasında esnek boşluk | Spacer |
| İki öğe arasında sabit boşluk | SizedBox |
| Öğeler sığmayınca alt satıra geçsin | Wrap |
Tarif 1: Araç Çubuğu
Bir düzenleyici ekranının alt çubuğu düşünün: solda iki ikon, sağda bir "Kaydet" butonu. İkonlar ve buton kendi boyutlarında kalmalı, aradaki boşluk esnek olmalı:
Row(
children: [
IconButton(onPressed: () {}, icon: const Icon(Icons.undo)),
IconButton(onPressed: () {}, icon: const Icon(Icons.redo)),
const Spacer(),
FilledButton(onPressed: () {}, child: const Text('Kaydet')),
],
)Spacer burada mainAxisAlignment.spaceBetween ile de yapılabilirdi, ama spaceBetween her aralığı eşitler; iki ikonun yan yana kalmasını istiyorsanız Spacer doğru araçtır. Ortaya bir başlık eklemek isterseniz Spacer yerine Expanded(child: Text(title, textAlign: TextAlign.center, overflow: TextOverflow.ellipsis)) koyun: başlık kalan alanı alır, uzunsa üç noktayla kesilir.
Tarif 2: Kart Alt Satırı
Ürün kartının alt satırında solda fiyat, sağda favori ikonu ve "Sepete ekle" butonu:
Row(
children: [
Text('249,90 ₺', style: Theme.of(context).textTheme.titleMedium),
const Spacer(),
IconButton(onPressed: () {}, icon: const Icon(Icons.favorite_border)),
FilledButton.tonal(onPressed: () {}, child: const Text('Sepete ekle')),
],
)Bu satır dar bir ekranda taşabilir: fiyat uzun, buton metni uzun. Spacer, flex olan bir çocuk olduğu için sıfıra kadar küçülebilir; taşma, sabit çocukların toplamı Row'dan büyük olduğunda başlar. O noktada fiyatı Flexible ile sarıp overflow: TextOverflow.ellipsis verin ya da satırı Wrap ile değiştirin. Kart bileşeninin kendisi için Card kullanımı yazısına bakabilirsiniz.
Tarif 3: Form Satırları
Adres formunda "Şehir" ve "Posta kodu" aynı satırda, şehir alanı iki kat geniş:
Row(
children: [
Expanded(
flex: 2,
child: TextFormField(
decoration: const InputDecoration(labelText: 'Şehir'),
),
),
const SizedBox(width: 12),
Expanded(
child: TextFormField(
keyboardType: TextInputType.number,
decoration: const InputDecoration(labelText: 'Posta kodu'),
),
),
],
)Row içindeki metin alanları Expanded olmak zorundadır; çıplak bırakırsanız uygulama hata verir (aşağıda). Aradaki boşluk sabit olduğu için SizedBox, Spacer değil. Form doğrulama ve Form widget'ı Form kullanımı yazısında anlatılıyor.
Tarif 4: Responsive İkiye Bölünmüş Ekran
Tablet ve masaüstünde solda liste, sağda detay; telefonda yalnızca liste. Karar ekran genişliğine değil, bu widget'a verilen genişliğe göre alınmalı; bunun için LayoutBuilder kullanılır:
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 720) {
return const NoteList();
}
return const Row(
children: [
SizedBox(width: 320, child: NoteList()),
VerticalDivider(width: 1),
Expanded(child: NoteDetail()),
],
);
},
)Liste sabit 320 piksel, detay geriye kalan her şeyi alır. Liste de oranla büyüsün istiyorsanız SizedBox yerine Expanded(flex: 1) ve detaya flex: 2 verin. LayoutBuilder ile MediaQuery arasındaki seçimi MediaQuery yazısında, breakpoint'lerle çalışan tam bir responsive düzeni LayoutBuilder yazısında ele aldım.
Ne Zaman Kullanılır, Ne Zaman Kullanılmaz?
Bu üçlü, alanın kardeşler arasında paylaşıldığı her Row ve Column'da doğru araçtır. Ama üç durumda başka bir şeye ihtiyacınız var:
- Row veya Column bir
SingleChildScrollViewiçindeyse ana eksende sınır yoktur;ExpandedveSpacerorada çalışmaz. Sabit boyut verin ya da kaydırmayı dıştaki widget'tan kaldırın. - Sayısı belli olmayan chip ve etiketlerde
Flexiblebile yetmez; öğeler alt satıra geçmelidir.Wrapkullanın. - Row'un içeriğine sarılması gerekiyorsa (
mainAxisSize: MainAxisSize.min)ExpandedveSpacerbu niyeti bozar, çünkü ikisi de kalan alanın tamamını ister. Böyle satırlarda yalnızcaFlexibleveSizedBoxkullanın. Row'un kendi genişliğini nasıl seçtiği mainAxisSize yazısında anlatılıyor.
Sık Yapılan Hatalar
1. Row içinde çıplak TextField
Belirti: An InputDecorator, which is typically created by a TextField, cannot have an unbounded width. Row, çocuklarına genişlik sınırı vermez; metin alanı ne kadar geniş olacağını bilemez. Çözüm, TextField'ı Expanded ya da Flexible ile sarmak. Sabit genişlik istiyorsanız SizedBox(width: 120, child: TextField(...)) da olur.
2. Flexible yazıp doldurmasını beklemek
Belirti: Flexible(child: Container(color: Colors.blue, child: Text('A'))) yazdınız, mavi alan yalnızca metin kadar. Flexible'ın varsayılan fit değeri FlexFit.loose: çocuk büyümek zorunda değildir ve Container çocuğuna sarılır. Doldurmasını istiyorsanız Expanded kullanın ya da fit: FlexFit.tight verin.
3. Flexible'ı çocuksuz bir Container ile denemek
Belirti: Flexible(child: Container(color: Colors.green)) yazdınız ve sonuç Expanded ile birebir aynı; "ikisi arasında fark yok" sonucuna varıyorsunuz. Fark var, ama bu örnek onu gösteremez. Çocuğu ve boyutu olmayan bir Container sınırlı kısıt aldığında olabildiğince büyür. loose kısıt "en fazla şu kadar" dediği için Container zaten izin verilen en büyük boyutu seçer ve payın tamamını doldurur. Farkı görmek için yukarıdaki etiket örneğindeki gibi kendi boyutu olan bir çocuk kullanın.
4. Kaydırılabilir alanda Spacer
SingleChildScrollView(
child: Column(
children: const [
Text('Üst'),
Spacer(), // Hata
Text('Alt'),
],
),
)Belirti: RenderFlex children have non-zero flex but incoming height constraints are unbounded. Kaydırma alanında yükseklik sonsuzdur, "kalan alan" tanımsızdır. Sabit SizedBox(height: 24) kullanın; alt öğeyi gerçekten ekranın altına yapıştırmak istiyorsanız Column'u kaydırma alanından çıkarıp içerik kısmını Expanded(child: SingleChildScrollView(...)) yapın.
5. Flex olmayan bir üst widget içinde kullanmak
Üçü de yalnızca Row, Column veya Flex'in doğrudan çocuğu olabilir. Stack, Container ya da Padding içine koyduğunuzda Incorrect use of ParentDataWidget. hatası alırsınız. En sık görülen biçimi, Expanded ile Row arasına bir Padding girmesidir; çözümü (sırayı ters çevirmek) Expanded yazısında ayrıntılı anlattım.
Mini Senaryo: Ürün Listesi Satırı
Dört tarifi bir araya getiren bir ürün satırı. Solda küçük resim, ortada başlık ve açıklama, sağda fiyat; altta favori ikonu ve sepete ekle butonu:
class ProductRow extends StatelessWidget {
const ProductRow({
super.key,
required this.title,
required this.subtitle,
required this.price,
});
final String title;
final String subtitle;
final String price;
@override
Widget build(BuildContext context) {
final text = Theme.of(context).textTheme;
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(width: 64, height: 64, child: Placeholder()),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: text.titleMedium, maxLines: 1, overflow: TextOverflow.ellipsis),
Text(subtitle, style: text.bodySmall, maxLines: 2, overflow: TextOverflow.ellipsis),
],
),
),
const SizedBox(width: 12),
Text(price, style: text.titleMedium),
],
),
const SizedBox(height: 8),
Row(
children: [
const Spacer(),
IconButton(onPressed: () {}, icon: const Icon(Icons.favorite_border)),
FilledButton.tonal(onPressed: () {}, child: const Text('Sepete ekle')),
],
),
],
),
),
);
}
}Üst satırda resim ve fiyat sabit, ortadaki Column Expanded ile aradaki tüm genişliği alıyor; başlık uzunsa taşmak yerine üç noktayla kesiliyor, çünkü Expanded metne sınırlı bir genişlik veriyor. Boşluklar sabit olduğu için SizedBox. Alt satırda Spacer ikon ve butonu sağa itiyor; solda başka bir öğe olmadığı için mainAxisAlignment.end de aynı sonucu verirdi, Spacer ise ileride sola bir "Stokta" etiketi eklediğinizde kodu değiştirmeden çalışmaya devam eder. Kartın dıştaki Column'u mainAxisSize.min olduğu için içinde Expanded yok; olsaydı kart liste satırında sonsuz yükseklik hatası verirdi.
Sık Sorulan Sorular
Row içindeki TextField neden hata veriyor?
Row çocuklarına genişlik sınırı vermez ve metin alanı sınırsız genişlikte olamaz. TextField'ı Expanded veya Flexible ile sarın ya da SizedBox ile sabit genişlik verin.
Spacer mı mainAxisAlignment mi?
Bütün aralıklar eşit olacaksa spaceBetween veya spaceEvenly yeterli. Boşluğun belirli bir yerde olması ya da farklı oranlarda paylaşılması gerekiyorsa Spacer(flex:) kullanın. İkisini aynı satırda birleştirmeyin: Spacer boş alanın tamamını aldığı için spaceBetween görünür bir etki yapmaz.
Flexible ne zaman Expanded yerine tercih edilir?
Çocuğun kendi boyutunda kalması ama alan yetmediğinde küçülmesi gerektiğinde. Sohbet balonu ve chip gibi içeriğe sarılan düzenlerde Flexible, kalan alanın hepsinin dolması gereken yerlerde Expanded doğru seçimdir.
Bu widget'lar ScrollView içinde neden çalışmıyor?
Kaydırma alanı ana eksende sonsuz alan sunar ve "kalan alan" hesaplanamaz. Sabit boyut verin ya da kaydırmayı Column'un dışına değil, Expanded ile sarılmış iç kısmına taşıyın.
İlgili Yazılar
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: Spacer Widget ile Esnek Boşluklar
Spacer'ın içinde ne olduğu, flex ile boşluk oranlama, SizedBox ve mainAxisAlignment ile farkı, kaydırılabilir alanlarda neden hata verdiği ve çözümü.
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.