Flutter: mainAxisSize ile Alan ve Hizalama Kontrolü
Son güncelleme:
10 dk okuma

mainAxisSize, bir Row veya Column'un ana ekseni boyunca ne kadar yer kaplayacağını belirleyen parametredir. Row için ana eksen yatay, Column için dikeydir. Bir arka plan rengi verdiğinizde kutunun ekranı boydan boya kaplaması, bir dialog'un gereksiz yere uzaması ya da mainAxisAlignment.center yazdığınız halde hiçbir şeyin kıpırdamaması çoğu zaman bu tek satırla ilgilidir. Bu yazıda iki değerin tam olarak ne yaptığını, üst widget'tan gelen kısıtlarla ve hizalama ayarlarıyla nasıl etkileştiğini, ardından dialog, bottom sheet ve buton gruplarında nereye yazılacağını anlatıyorum.
Canlı Demo: mainAxisSize Örnekleri
Aşağıdaki örnekte MainAxisSize.max ile MainAxisSize.min arasındaki farkı, Row'un arka plan rengine bakarak görebilirsiniz:
💡 Eğer yukarıdaki örnek açılmazsa, DartPad linkine tıklayarak yeni sekmede çalıştırabilirsiniz.
İki Değer: max ve min
MainAxisSize.max (Varsayılan)
Hiçbir şey yazmazsanız Row ve Column max ile çalışır: üst widget'ın izin verdiği kadar genişler, çocuklar bu alanın içine yerleştirilir.
Row(
mainAxisSize: MainAxisSize.max, // Varsayılan, yazmasanız da aynı
children: const [
Icon(Icons.star),
Text('Maksimum genişlik'),
Icon(Icons.star),
],
)Çocuklar toplamda 150 piksel tutsa bile Row ekran genişliği kadardır. Aradaki fark "boş alan"dır ve bu boş alanı mainAxisAlignment dağıtır.
MainAxisSize.min
min ile Row veya Column çocuklarının toplamı kadar yer kaplar, fazlasını almaz:
Row(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(Icons.star),
Text('Minimum genişlik'),
Icon(Icons.star),
],
)Column'da aynı mantık dikey eksende işler: min verilen bir Column yalnızca içeriği kadar yükseklik alır. Dikkat edilmesi gereken nokta şu: bu parametre yalnızca ana ekseni etkiler. Column'un genişliği ya da Row'un yüksekliği zaten en büyük çocuğa göre belirlenir.
| Özellik | MainAxisSize.max | MainAxisSize.min |
|---|---|---|
| Ana eksendeki boyut | Üst widget'ın izin verdiği kadar | Çocukların toplamı kadar |
| Varsayılan | Evet | Hayır |
| Boş alan | Var, mainAxisAlignment dağıtır |
Yok, dağıtılacak bir şey kalmaz |
| Tipik kullanım | Ekran iskeleti, araç çubuğu satırı | Rozet, dialog içeriği, buton grubu |
Kısıtlar Her Zaman Son Sözü Söyler
Flutter'da boyutu widget tek başına seçmez; üst widget kısıt gönderir, çocuk bu aralıkta bir boyut seçer. mainAxisSize de bu kuralın içinde çalışır ve iki durumda beklediğiniz gibi davranmaz:
- Sıkı (tight) kısıt geldiyse
minetkisizdir. ListView'in çocuğu,crossAxisAlignment: CrossAxisAlignment.stretchverilmiş bir Column'un çocuğu ya daSizedBox(width: double.infinity)içindeki bir Row "tam şu genişlikte ol" emri alır;minyazsanız da o genişlikte olur. - Sınırsız (unbounded) kısıt geldiyse
maxdemingibi davranır.SingleChildScrollViewiçindeki Column'a sonsuz yükseklik sunulur; sonsuzu dolduramayacağı için içeriği kadar yer kaplar.
Row ve Column'un widget ağacındaki yerini baştan görmek isterseniz widget ağacı, Row, Column ve Stack yazısı iyi bir başlangıç noktası.
mainAxisSize ve Hizalama: Row Üç Adımda Yerleşir
mainAxisSize, mainAxisAlignment ve crossAxisAlignment birbirinden bağımsız üç ayar gibi görünür, ama hizalama, boyut ayarının bıraktığı boşluk üzerinde çalışır. Bir Row (ya da Column) çocuklarını yerleştirirken sırayla üç şey yapar:
- Esnek olmayan çocukları ölçer, sonra kalan alanı
ExpandedveFlexibleçocuklara dağıtır. mainAxisSizedeğerine göre kendi ana eksen boyutuna karar verir:maxise üst widget'ın izin verdiği en büyük boyut,minise çocukların toplamı.- Kendi boyutu ile çocukların toplamı arasındaki farkı, yani boş alanı,
mainAxisAlignmentkuralına göre dağıtır.
Üçüncü adım her şeyi açıklar. min seçtiğinizde Row zaten çocukları kadar olduğu için boş alan sıfırdır; center, end, spaceBetween ya da spaceEvenly yazmanızın görünür bir sonucu olmaz. Kod hatasız çalışır, sadece hizalanacak bir şey yoktur.
// Bu iki Row aynı görünür: min ile boş alan olmadığı için center etkisizdir.
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: const [Icon(Icons.star), Text('Favori')],
)
Row(
mainAxisSize: MainAxisSize.min,
children: const [Icon(Icons.star), Text('Favori')],
)| Kombinasyon | Sonuç |
|---|---|
max + start |
Çocuklar sola yaslı, boşluk sağda (varsayılan) |
max + center |
Çocuklar ortada, boşluk iki yanda |
max + spaceBetween |
Boşluk çocukların arasına paylaştırılır |
min + herhangi bir değer |
Boş alan yok, hizalama görünmez |
min + içeride Expanded veya Spacer |
Esnek çocuk tüm alanı istediği için Row max gibi davranır |
Grubu ortalamanın iki yolu
"Row'u ortala" dediğinizde aslında iki farklı şey kastedebilirsiniz ve ikisinin kodu farklıdır.
Row'un kendisi tam genişlikte kalsın, içindeki çocuklar ortada dursun istiyorsanız max ile mainAxisAlignment.center kullanın. Row'a bir arka plan verirseniz bu arka plan satırı boydan boya kaplar.
Row bir bütün olarak içeriği kadar olsun ve bu bütün ortada dursun istiyorsanız Row'a min verip konumlandırma işini üst widget'a bırakın:
Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
color: Colors.amber,
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [Icon(Icons.star), SizedBox(width: 4), Text('Favori')],
),
),
)Burada arka plan yalnızca ikon ve metni sarar. Sağa yaslamak için Center yerine Align(alignment: Alignment.centerRight, ...) yeterlidir; Align'ın diğer seçenekleri için Align widget'ı yazısına bakabilirsiniz. Kısacası kural şu: min verdiğiniz bir Row'un yerini Row'un kendisi değil, onu saran widget belirler.
crossAxisAlignment ayrı bir eksende çalışır
crossAxisAlignment Row'da dikey, Column'da yatay hizalamayı yönetir ve mainAxisSize'dan etkilenmez. Row'un yüksekliği her zaman en yüksek çocuğu kadardır; diğer çocuklar bu yükseklik içinde start, center, end ya da baseline ile hizalanır. Varsayılan değer center olduğu için farklı boyuttaki bir ikon ve metin kendiliğinden ortalanır.
Tek istisna stretch: çocukları çapraz eksende zorla en büyük boyuta çeker. Bunun için çapraz eksenin sınırlı olması gerekir; aşağıdaki hatalar bölümünde sınırsız olduğu durum var.
Bir de yön ayrıntısı: start ve end metin yönüne göre çalışır. Arapça gibi sağdan sola dillerde Row için MainAxisAlignment.start sağ tarafı ifade eder; kesin olarak sola yaslamak istiyorsanız Row'a textDirection: TextDirection.ltr verebilirsiniz.
Tarifler: Dialog, Buton Grubu ve Bottom Sheet
min değerinin en çok işe yaradığı yerler, yüksekliğin ya da genişliğin içerikten gelmesi gereken küçük parçalardır.
Dialog içeriği
Bir AlertDialog'un içine Column koyup çalıştırdığınızda dialog'un ekranın neredeyse tamamını kapladığını görürsünüz. Dialog, içeriğine ekran yüksekliğine yakın bir üst sınır sunar; max ile çalışan bir Column bu sınırın tamamını alır, min ile çalışan ise yalnızca ikon ve metin kadar yer kaplar:
Future<bool?> showDeleteDialog(BuildContext context) {
return showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Notu sil'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(Icons.warning_amber_rounded, size: 48, color: Colors.orange),
SizedBox(height: 16),
Text('Bu not kalıcı olarak silinecek.'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Vazgeç'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Sil'),
),
],
),
);
}İçerik tek bir Text ise Column'a hiç gerek yok; doğrudan content: Text(...) yazın. Alttaki butonlar için kendi Row'unuzu kurmayın, actions parametresini kullanın: butonlar sığmadığında alt alta dizilmeyi kendisi halleder, elle yazılmış bir Row ise taşar. Dialog'un diğer parametreleri için AlertDialog kullanımı yazısına bakabilirsiniz.
Çerçeveli buton grubu
Adet seçici, beğen/paylaş ikonları gibi küçük gruplarda Row'un çerçevesi ya da arka planı varsa min şarttır, yoksa çerçeve satır boyunca uzar:
class QuantityStepper extends StatelessWidget {
const QuantityStepper({
super.key,
required this.value,
required this.onChanged,
});
final int value;
final ValueChanged<int> onChanged;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).colorScheme.outline),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: value > 1 ? () => onChanged(value - 1) : null,
icon: const Icon(Icons.remove),
),
Text('$value'),
IconButton(
onPressed: () => onChanged(value + 1),
icon: const Icon(Icons.add),
),
],
),
);
}
}Grup içeriği kadar yer kapladığı için onu bir bütün olarak taşımak kolaylaşır: Center ile ortalayın, Align ile sağa yaslayın. Konumu Row'un kendi mainAxisAlignment değeriyle ayarlamaya çalışmak, yukarıda anlatılan nedenle işe yaramaz.
Bottom sheet
Bir ürün sayfasında "Sepete ekle"ye basınca alttan açılan küçük bir panel düşünün: ürün adı, adet seçici ve tam genişlikte bir onay butonu. Yukarıdaki QuantityStepper burada kullanılıyor:
void showAddToCartSheet(BuildContext context) {
var quantity = 1;
showModalBottomSheet<void>(
context: context,
showDragHandle: true,
builder: (context) {
return StatefulBuilder(
builder: (context, setSheetState) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Filtre kahve 250 g',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
Center(
child: QuantityStepper(
value: quantity,
onChanged: (v) => setSheetState(() => quantity = v),
),
),
const SizedBox(height: 16),
FilledButton(
onPressed: () => Navigator.pop(context),
child: const Text('Sepete ekle'),
),
],
),
),
);
},
);
},
);
}Burada iki ayrı min iki ayrı iş yapıyor. Column'daki min, sheet'in yalnızca üç satırlık içerik kadar yükselmesini sağlıyor; olmasaydı panel izin verilen en büyük yüksekliğe çıkardı. Row'daki min ise adet seçicinin çerçevesini içeriğe sardırıyor. Column stretch olduğu için buton tam genişlikte; adet seçici ise Center sayesinde bu zorlamadan kurtulup ortada kompakt duruyor. Sheet'in içinde tüm ekranı kaplayabilecek bir liste varsa min yetmez; isScrollControlled: true ve kaydırılabilir bir gövde kullanın.
Ne Zaman Kullanılır, Ne Zaman Kullanılmaz?
min değerine şu durumlarda uzanın: Row veya Column'u saran bir Container, Card ya da DecoratedBox içeriğe sarılmalıysa; grubu bir bütün olarak ortalamak veya sağa yaslamak istiyorsanız; dialog ve bottom sheet gibi yüksekliği içerikten gelmesi gereken yerlerde.
max değerinde kalın: çocukları spaceBetween veya spaceEvenly gibi bir değerle alana yaymak istiyorsanız, tam genişlikteki bir araç çubuğunda öğeleri ortalayacaksanız, içeride Expanded veya Spacer kullanıyorsanız ya da ekranın ana iskeletini kuruyorsanız. Boş alan bu durumlarda tasarımın parçasıdır.
Bazı işler için doğru araç başka bir widget'tır:
- Tek bir çocuğu içeriği kadar tutup konumlandırmak için Row'a gerek yok, Align veya
Centeryeterlidir. - Sığmayınca alt satıra geçmesi gereken etiket ve chip gruplarında
Wrapkullanılır;mintaşmayı engellemez. - "Birini seç" türü gruplarda (Gün / Hafta / Ay)
SegmentedButtonseçili durumu ve erişilebilirliği hazır verir. - İki öğe arasına esnek boşluk koyacaksanız
spaceBetweenyerineSpacerdaha okunaklı olabilir; ikisinin farkı Spacer yazısında anlatılıyor.
Sık Yapılan Hatalar
1. min yazıldı ama Row hâlâ tam genişlikte
Belirti: Rozetin arka planı min yazmanıza rağmen satırı boydan boya kaplıyor. Sebep yukarıdaki sıkı kısıt: üst widget genişliği zorluyor. ListView'in çocuğu olmak ya da stretch verilmiş bir Column'un içinde durmak buna yeter. Çözüm, araya kısıtı gevşeten bir widget koymak:
ListView(
children: [
Align(
alignment: Alignment.centerLeft,
child: Container(
color: Colors.amber,
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [Icon(Icons.bolt), Text('Hızlı teslimat')],
),
),
),
],
)2. min verilmiş Row veya Column'un içine Expanded ya da Spacer koymak
Belirti: min yazdınız ama Row yine tüm genişliği alıyor, dialog yine boydan boya uzuyor. Expanded ve Spacer kalan alanın tamamını ister; çocuklardan biri tüm alanı isteyince toplam da tüm alan olur. İçerik kadar yer istiyorsanız esnek çocuğu kaldırın ya da Flexible kullanın (varsayılan fit değeri FlexFit.loose olduğu için çocuk küçük kalabilir). Dialog ve sheet içinde boşluklar sabit olmalı: SizedBox(height: 16). Expanded ile Flexible'ın farkını Expanded, Flexible ve Spacer karşılaştırmasında ayrıntılı anlattım.
3. min ile mainAxisAlignment.center yazıp ortalanmasını beklemek
Belirti: Row'daki çocuklar sola yaslı kalır, hiçbir hata çıkmaz. Sebep, boş alanın sıfır olması. Ya mini kaldırıp max ile center kullanın ya da mini koruyup Row'u Center ile sarın. Hangisini seçeceğiniz arka planın satırı mı yoksa içeriği mi kaplaması gerektiğine bağlıdır.
4. min'in taşmayı önleyeceğini sanmak
Belirti: Küçük ekranda ya da klavye açıkken dialog'un altında sarı-siyah şerit ve A RenderFlex overflowed by ... pixels on the bottom. mesajı. min içeriği küçültmez, yalnızca boş alanı almaz. İçerik uzayabiliyorsa kaydırılabilir yapın:
AlertDialog(
scrollable: true,
title: const Text('Kullanım koşulları'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: const [
Text('Uzun metin...'),
],
),
)5. Kaydırılabilir alanda Expanded kullanmak
SingleChildScrollView(
child: Column(
children: [
const Text('Başlık'),
Expanded(child: Container(color: Colors.blue)), // Hata
],
),
)Belirti, konsoldaki şu mesaj: RenderFlex children have non-zero flex but incoming height constraints are unbounded. Sonsuz yükseklikte "kalan alan" hesaplanamaz. Hata metni de çözümü söyler: mainAxisSize: MainAxisSize.min ile birlikte esnek çocuklarda FlexFit.loose kullanın ya da Expanded yerine sabit yükseklik verin.
6. Column içindeki Row'a stretch vermek
Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.stretch, // Hata
children: [Container(color: Colors.blue, child: const Text('A'))],
),
],
)Belirti: Konsolda BoxConstraints forces an infinite height. mesajı ve boş ekran. Column dikeyde sınır koymadığı için Row'un çocukları sonsuz yüksekliğe çekilmeye çalışılır. Row'a bir yükseklik verin: SizedBox(height: 48, child: Row(...)). Çocukların en yüksek olana eşitlenmesi yeterliyse IntrinsicHeight da kullanılabilir, ama maliyetli olduğu için uzun listelerde tercih etmeyin.
7. baseline hizalamada textBaseline'ı unutmak
Belirti: crossAxisAlignment: CrossAxisAlignment.baseline yazdığınızda uygulama textBaseline is required if you specify the crossAxisAlignment with CrossAxisAlignment.baseline assertion'ı ile durur. Farklı boyutlu iki metni taban çizgisinden hizalarken textBaseline: TextBaseline.alphabetic eklemek gerekir.
Mini Senaryo 1: Sipariş Listesinde Durum Rozeti
Bir sipariş listesi düşünün; her satırda "Kargoda", "Teslim edildi" gibi küçük bir rozet var. Rozet, ikon ve metinden oluşan bir Row'u saran renkli bir Container'dır:
class StatusBadge extends StatelessWidget {
const StatusBadge({super.key, required this.label, required this.icon});
final String label;
final IconData icon;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: scheme.secondaryContainer,
borderRadius: BorderRadius.circular(999),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 16, color: scheme.onSecondaryContainer),
const SizedBox(width: 6),
Text(label, style: TextStyle(color: scheme.onSecondaryContainer)),
],
),
);
}
}Container kendi boyutunu çocuğundan alır. Row max olsaydı Container da satır boyunca uzar, rozet renkli bir şeride dönerdi. min ile Container ikon ve metne sarılır. Rozeti crossAxisAlignment: CrossAxisAlignment.start verilmiş bir Column'a koyduğunuzda sola yaslı ve kompakt durur; aynı Column stretch olsaydı birinci hatadaki durum oluşur ve rozeti Align ile sarmanız gerekirdi.
Mini Senaryo 2: Sohbet Balonu
Bir mesajlaşma ekranı düşünün. Kendi mesajlarınız sağda, karşı tarafınkiler solda; her balon metnin genişliği kadar ama en fazla ekranın dörtte üçü. Balonun içinde metin ve sağ altta küçük bir saat var:
class ChatBubble extends StatelessWidget {
const ChatBubble({
super.key,
required this.text,
required this.time,
required this.isMine,
});
final String text;
final String time;
final bool isMine;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final maxWidth = MediaQuery.sizeOf(context).width * 0.75;
return Align(
alignment: isMine ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
constraints: BoxConstraints(maxWidth: maxWidth),
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
padding: const EdgeInsets.fromLTRB(12, 8, 8, 6),
decoration: BoxDecoration(
color: isMine ? scheme.primaryContainer : scheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(child: Text(text)),
const SizedBox(width: 8),
Text(time, style: Theme.of(context).textTheme.labelSmall),
],
),
),
);
}
}Üç ayar burada üç ayrı işi yapıyor. Row'daki min, balonun arka planının yalnızca metin ve saati sarmasını sağlıyor; max olsaydı her balon dörtte üç genişlikte olurdu. Balonun sağda mı solda mı duracağına Row değil, onu saran Align karar veriyor; mainAxisAlignment ile bunu yapmaya çalışsaydınız min yüzünden hiçbir şey olmazdı. crossAxisAlignment: CrossAxisAlignment.end ise çok satırlı bir mesajda saati son satırın hizasına indiriyor. Metin Flexible ile sarılı, çünkü uzun bir mesajın balon sınırında alt satıra geçmesi gerekiyor; Expanded kullansaydık metin her zaman tüm genişliği ister ve min etkisiz kalırdı.
Sık Sorulan Sorular
mainAxisSize'ın varsayılan değeri nedir?
Row ve Column için varsayılan MainAxisSize.max değeridir. Hiçbir şey yazmazsanız widget, üst widget'ın izin verdiği kadar ana eksende genişler.
mainAxisAlignment.center neden çalışmıyor?
Büyük olasılıkla Row veya Column'a mainAxisSize: MainAxisSize.min verilmiş. min ile boş alan kalmadığı için hizalama görünmez. Ya max kullanın ya da widget'ı Center ile sarın.
AlertDialog neden tüm ekranı kaplıyor?
content içindeki Column varsayılan MainAxisSize.max ile çalıştığı için dialog'un izin verdiği tüm yüksekliği alır. Column'a mainAxisSize: MainAxisSize.min eklediğinizde dialog içeriği kadar olur. İçerik yine de sığmıyorsa scrollable: true verin.
mainAxisSize.min taşma hatasını çözer mi?
Hayır. min yalnızca boş alanı almaz; çocukların toplamı mevcut alandan büyükse yine "RenderFlex overflowed" hatası alırsınız. Taşma için Expanded, Flexible, Wrap veya kaydırılabilir bir widget gerekir.
min yazdığım halde Row neden hâlâ tam genişlikte?
Büyük olasılıkla üst widget sıkı kısıt gönderiyor (ListView çocuğu, stretch verilmiş Column, sonsuz genişlikli SizedBox) ya da Row'un içinde bir Expanded veya Spacer var. Row'u Align veya Center ile sarmak ya da esnek çocuğu kaldırmak sorunu çözer.
mainAxisSize çapraz ekseni de etkiler mi?
Hayır, yalnızca ana ekseni etkiler. Column'un genişliği en geniş çocuğuna göre belirlenir; genişliği doldurmak için crossAxisAlignment: CrossAxisAlignment.stretch kullanılır. Bunun için de çapraz eksenin sınırlı olması gerekir.
İlgili Yazılar
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: Expanded Widget ile Alan Genişletme
Expanded'ın derinlemesine incelemesi: flex oranı nasıl hesaplanır, iç içe kullanım, Row ve Column dışında neden çalışmaz ve en sık görülen üç hata.
Flutter'a Giriş: Widget Ağacı, Row, Column ve Stack Yapıları
Dart'ı öğrendikten sonra nihayet Flutter'a geçiş yaptım. İlk günümde widget ağacını, Row, Column ve Stack yapılarını öğreniyorum.