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

Flutter: Expanded, Flexible ve Spacer Farkı ve Tarifler

Ahmet Balaman

Son güncelleme:

6 dk okuma

FlutterExpandedFlexibleSpacerLayoutResponsive
Flutter: Expanded, Flexible ve Spacer Farkı ve Tarifler

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 kalan alanın tamamını alır, Flexible yalnızca ihtiyacı kadarını alır, Spacer ise iki kutuyu birbirinden iter

  • Expanded: Payın tamamını kaplamak zorundadır. Aslında Flexible(fit: FlexFit.tight) widget'ının kısa yazımıdır.
  • Flexible: Varsayılan fit değeri FlexFit.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ş bir Expanded'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 SingleChildScrollView içindeyse ana eksende sınır yoktur; Expanded ve Spacer orada çalışmaz. Sabit boyut verin ya da kaydırmayı dıştaki widget'tan kaldırın.
  • Sayısı belli olmayan chip ve etiketlerde Flexible bile yetmez; öğeler alt satıra geçmelidir. Wrap kullanın.
  • Row'un içeriğine sarılması gerekiyorsa (mainAxisSize: MainAxisSize.min) Expanded ve Spacer bu niyeti bozar, çünkü ikisi de kalan alanın tamamını ister. Böyle satırlarda yalnızca Flexible ve SizedBox kullanı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.

Yorumlar