Flutter: Stack ve Positioned ile Üst Üste Yerleşim
7 dk okuma

Row çocuklarını yan yana, Column alt alta dizer. Stack ise üst üste koyar: listedeki ilk çocuk en altta, son çocuk en üstte çizilir. Bir ikonun köşesindeki bildirim sayısı, fotoğrafın üstündeki başlık, kaydederken ekranı kaplayan yükleniyor katmanı ya da kapak görselinin kenarına taşan profil fotoğrafı hep bu fikirle yapılır. Bu yazıda Stack'in boyutunu nasıl belirlediğini, fit, alignment ve clipBehavior parametrelerini, Positioned ailesini, sık kullanılan tarifleri ve en çok karşılaşılan yerleşim hatalarını anlatıyorum.
Temel Kullanım
Stack(
alignment: Alignment.center,
children: [
Container(width: 200, height: 120, color: Colors.indigo),
Container(width: 120, height: 70, color: Colors.amber),
const Text('En üstte'),
],
)Üç çocuk da ortalanır ve sırayla üst üste biner: önce mavi kutu, üstüne sarı kutu, en üste metin. Flutter'da z-index gibi bir özellik yoktur; hangi widget'ın üstte olacağını children listesindeki sıra belirler.
Stack'in Boyutu ve fit
Stack'in çocukları iki gruba ayrılır. Konumlandırılmamış çocuklar (Positioned ile sarılmamış olanlar) Stack'in boyutunu belirler: Stack, en büyük konumlandırılmamış çocuğu kadar olur. Konumlandırılmış (Positioned) çocuklar ise bu boyutun hesabına katılmaz; Stack'in boyutu belli olduktan sonra ona göre yerleştirilir.
Konumlandırılmamış çocuklara hangi kısıtların iletileceğini fit belirler:
fit |
Konumlandırılmamış çocuklara ne olur? |
|---|---|
StackFit.loose (varsayılan) |
İstedikleri kadar küçük olabilirler, üst sınır Stack'e verilen alandır |
StackFit.expand |
Stack'e verilen en büyük alanı doldurmak zorundadırlar |
StackFit.passthrough |
Stack'e gelen kısıtlar olduğu gibi iletilir |
StackFit.expand özellikle "arka plan resmi ve üstündeki katmanlar aynı alanı kaplasın" durumunda işe yarar. Bir ayrıntı daha: Stack'in hiç konumlandırılmamış çocuğu yoksa, yani bütün çocukları Positioned ise, Stack alabildiği en büyük boyutu seçer. Aşağıdaki hatalar bölümünde bunun neden sorun çıkardığına değiniyorum.
alignment: Konumlandırılmamış Çocukların Yeri
alignment (varsayılan AlignmentDirectional.topStart, yani soldan sağa dillerde sol üst köşe), konumlandırılmamış çocukların Stack içinde nereye yerleşeceğini söyler. Positioned çocuklar bundan etkilenmez, ama bir eksende değer vermezsen o eksende de alignment kullanılır: yalnızca top: 8 verilmiş bir Positioned çocuk, yatayda alignment'a göre yerleşir.
Tek bir çocuğu farklı bir noktaya koymak istiyorsan onu Align ile sarabilirsin; Stack içinde Align ile Positioned arasındaki farkı Align yazısında ayrıntılı anlattım.
Positioned Ailesi
Positioned, çocuğunu Stack'in kenarlarına göre piksel cinsinden konumlar:
SizedBox(
width: 300,
height: 200,
child: Stack(
children: [
const Positioned.fill(child: ColoredBox(color: Colors.black12)),
const Positioned(top: 8, right: 8, child: Icon(Icons.close)),
const Positioned(left: 16, right: 16, bottom: 16, child: Text('Alt şerit')),
const PositionedDirectional(top: 8, start: 8, child: Icon(Icons.star)),
Positioned(top: 60, width: 100, height: 40, child: Container(color: Colors.teal)),
],
),
)left,top,right,bottom: Kenara uzaklık.leftverightbirlikte verilirse çocuğun genişliği aradaki mesafe olur; alt şerit örneği böyle çalışır.width,height: Doğrudan boyut. Her eksende en fazla iki değer verilebilir:left,rightvewidth'in üçü birden verilemez.Positioned.fill: Dört kenar da 0; çocuk Stack'in tamamını kaplar. Katmanlar için en sık kullanılan kısayoldur.PositionedDirectional:left/rightyerinestart/endalır ve metin yönüne uyar. Arapça ya da İbranice gibi sağdan sola dillerdestartsağ taraf olur. Uygulaman birden fazla dil destekliyorsa ikon ve düğmeleri bununla konumlamak iyi bir alışkanlıktır.
clipBehavior: Taşan Çocuklar
Varsayılan clipBehavior: Clip.hardEdge, Stack'in sınırları dışına taşan her şeyi keser. Bir ikonun köşesinden hafifçe dışarı taşan rozet ya da kapak görselinin altına sarkan avatar için clipBehavior: Clip.none verirsin. Burada önemli bir tuzak var: taşan kısım görünür, ama dokunmaları almaz. Flutter dokunma testini ebeveynin sınırları içinde yapar; Stack'in dışında kalan piksele yapılan dokunuş o çocuğa ulaşmaz. Taşan parça tıklanabilir olacaksa Stack'i o parçayı da kapsayacak kadar büyüt. Aşağıdaki mini senaryo tam olarak bunu yapıyor. Dokunma algılama tarafı için GestureDetector yazısına bakabilirsin.
Tarif 1: Bildirim Rozeti
class CartIconWithBadge extends StatelessWidget {
const CartIconWithBadge({super.key, required this.count});
final int count;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Stack(
clipBehavior: Clip.none,
children: [
const Icon(Icons.shopping_cart_outlined, size: 28),
if (count > 0)
Positioned(
top: -6,
right: -8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
decoration: BoxDecoration(
color: colors.error,
borderRadius: BorderRadius.circular(10),
),
constraints: const BoxConstraints(minWidth: 18),
child: Text(
count > 99 ? '99+' : '$count',
textAlign: TextAlign.center,
style: TextStyle(color: colors.onError, fontSize: 11),
),
),
),
],
);
}
}Stack'in boyutunu ikon belirler; rozet negatif top ve right değerleriyle köşeden dışarı taşar, Clip.none sayesinde kesilmez. Standart bir bildirim sayısı için bunu elle yazmana gerek yok: Material 3'ün hazır Badge widget'ı aynı işi yapar ve tema renklerini kendisi kullanır: Badge.count(count: count, isLabelVisible: count > 0, child: const Icon(Icons.shopping_cart_outlined)). Stack ile elle yapmak, rozetin şekli ya da içeriği standart dışı olduğunda anlamlıdır.
Tarif 2: Resim Üstünde Başlık
class ImageCaptionCard extends StatelessWidget {
const ImageCaptionCard({super.key, required this.image, required this.title});
final ImageProvider image;
final String title;
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(16),
child: AspectRatio(
aspectRatio: 16 / 9,
child: Stack(
fit: StackFit.expand,
children: [
Image(image: image, fit: BoxFit.cover),
DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withValues(alpha: 0.7),
],
stops: const [0.5, 1.0],
),
),
),
Positioned(
left: 16,
right: 16,
bottom: 12,
child: Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Colors.white,
),
),
),
],
),
),
);
}
}StackFit.expand resmi ve gradyanı kartın tamamına yayar, AspectRatio kartın oranını sabitler, ClipRRect köşeleri yuvarlar. Alttan yukarı koyulaşan gradyan, resmin rengi ne olursa olsun beyaz yazının okunmasını sağlar. Burada yazı rengini bilerek sabit beyaz verdik: yazı temanın yüzeyinin değil, her zaman koyu bir gradyanın üstünde duruyor. ImageProvider olarak AssetImage('assets/kapak.jpg') ya da NetworkImage(url) verebilirsin; resim eklemenin ayrıntıları resim ekleme yazısında.
Tarif 3: Kaydederken Ekranı Kilitleyen Katman
class SaveOverlay extends StatelessWidget {
const SaveOverlay({super.key, required this.isSaving, required this.child});
final bool isSaving;
final Widget child;
@override
Widget build(BuildContext context) {
return Stack(
children: [
child,
if (isSaving) ...[
Positioned.fill(
child: ModalBarrier(
dismissible: false,
color: Theme.of(context).colorScheme.scrim.withValues(alpha: 0.3),
),
),
const Positioned.fill(
child: Center(child: CircularProgressIndicator()),
),
],
],
);
}
}Stack'in boyutunu sayfanın içeriği (child) belirler; katmanlar Positioned.fill ile tam o alanı kaplar. ModalBarrier alttaki içeriğe giden dokunuşları yakalar, böylece kayıt sürerken kullanıcı butona ikinci kez basamaz. Positioned.fill burada bir tercih değil, gereklilik: ModalBarrier olabildiğince büyümek ister; konumlandırılmamış bir çocuk olarak eklenirse Stack'i de büyütür, kaydırılabilir bir alanın içindeyse sonsuz yükseklik hatası verir.
IndexedStack: Aynı Anda Tek Çocuk
IndexedStack de çocuklarını üst üste tutar, ama yalnızca index numaralı olanı gösterir. Diğerleri ağaçta kalmaya devam ettiği için State'leri kaybolmaz: bir sekmede listeyi aşağı kaydırıp başka sekmeye geçtiğinde, geri döndüğünde kaldığın yeri görürsün. Alt navigasyon çubuğuyla sekme değiştirirken sayfa durumunu korumanın standart yolu budur; örneği BottomNavigationBar yazısında var.
Mini Senaryo: Profil Başlığı
Sosyal uygulamalarda sık görülen bir düzen: üstte renkli bir kapak alanı, sağ üstte kapak düzenleme butonu, sol altta kapağın kenarına taşan yuvarlak avatar ve avatarın köşesinde çevrimiçi göstergesi.
class ProfileHeader extends StatelessWidget {
const ProfileHeader({super.key});
static const _coverHeight = 140.0;
static const _avatarRadius = 44.0;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return SizedBox(
height: _coverHeight + _avatarRadius,
child: Stack(
children: [
Positioned(
top: 0,
left: 0,
right: 0,
height: _coverHeight,
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [colors.primary, colors.tertiary],
),
),
),
),
PositionedDirectional(
top: 8,
end: 8,
child: IconButton.filledTonal(
onPressed: () {},
tooltip: 'Kapak fotoğrafını değiştir',
icon: const Icon(Icons.edit_outlined),
),
),
PositionedDirectional(
start: 16,
bottom: 0,
child: Stack(
clipBehavior: Clip.none,
children: [
CircleAvatar(
radius: _avatarRadius,
backgroundColor: colors.surface,
child: CircleAvatar(
radius: _avatarRadius - 4,
backgroundColor: colors.primaryContainer,
child: Text(
'AB',
style: TextStyle(
fontSize: 24,
color: colors.onPrimaryContainer,
),
),
),
),
Positioned(
right: 4,
bottom: 4,
child: Container(
width: 18,
height: 18,
decoration: BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
border: Border.all(color: colors.surface, width: 3),
),
),
),
],
),
),
],
),
);
}
}Bu düzendeki kilit karar, avatarı negatif bir bottom değeriyle dışarı taşırmak yerine Stack'i kapak + avatar yarıçapı kadar yüksek yapmak. Kapak üst 140 pikseli kaplıyor, avatar Stack'in altına yaslanıyor ve yarısı kapağın üstüne biniyor. Böylece avatar bütünüyle Stack'in içinde kalıyor: dokunulabilir, altındaki içeriğin üstüne binmiyor ve Column içinde bir sonraki widget doğru yerden başlıyor. Stack'in sabit yüksekliğini veren SizedBox'ın rolü için SizedBox yazısına bakabilirsin. Düzenleme butonu ve avatar PositionedDirectional ile konumlandığı için sağdan sola bir dilde ikisi kendiliğinden yer değiştirir. Çevrimiçi göstergesindeki surface renkli çerçeve de noktayı avatardan ayırıyor ve iki temada da doğru görünüyor; renk rollerinin mantığı Theme ve ThemeData yazısında.
Ne Zaman Stack, Ne Zaman Başka Bir Şey?
Stack güçlü olduğu için her üst üste binme ihtiyacında ilk akla gelen araç oluyor, ama birçok durumda daha basit bir yol var. Bir widget'ın arkasına yalnızca renk, gradyan ya da kenarlık koyacaksan Container'ın decoration alanı ya da DecoratedBox yeter. Tek bir çocuğu birkaç piksel kaydırmak için Transform.translate, tek bir çocuğu bir köşeye yaslamak için Align daha az kodla iş görür. Ekranın sağ altında duran eylem butonu için Stack kurmak yerine Scaffold'un floatingActionButton alanını kullanırsın; klavye ve SnackBar ile uyumunu Scaffold kendisi yönetir. Standart bildirim sayısı için de yukarıda gördüğümüz Badge var. Stack'i, birden fazla katmanın gerçekten aynı alanı paylaştığı durumlar için sakla: resim ve üstündeki yazı, içerik ve üstündeki yükleniyor katmanı, kapak ve üstüne binen avatar.
Sık Yapılan Hatalar
1. Positioned'ı Stack'in doğrudan çocuğu yapmamak
Positioned'ı bir Padding, Center ya da Column içine koyarsan ya da Stack dışında kullanırsan şu hatayı alırsın:
Incorrect use of ParentDataWidget.Positioned yalnızca bir Stack'in doğrudan çocuğu olarak çalışır. Aynı hata Expanded'ı Stack içine koyduğunda da çıkar; Expanded yalnızca Row, Column ve Flex içinde anlamlıdır (Expanded yazısı).
2. Yalnızca Positioned çocuğu olan Stack'i Column ya da ListView içine koymak
Konumlandırılmamış çocuğu olmayan Stack, alabildiği en büyük boyutu seçer. Dikeyde sınırsız alan veren bir ListView ya da kaydırılabilir bir Column içinde bu "sonsuz" demektir ve düzen hata verir. Hata metni sürüme göre değişir: Flutter 3.38 ve sonrasında A Stack requires bounded constraints from its parent ile başlar, daha eski sürümlerde RenderStack object was given an infinite size during layout görürsün. Çözüm, Stack'e SizedBox ile bir boyut vermek ya da en az bir konumlandırılmamış çocuk eklemektir.
3. left, right ve width'i birlikte vermek
Aynı eksende üç değer birden verilemez. Positioned(left: 0, right: 0, width: 100, ...) bir assertion hatasına yol açar; ya iki kenarı ya da bir kenar ile boyutu ver.
4. Taşan parçanın dokunmaları almaması
Clip.none ile dışarı taşan butona dokunulamıyor. Sebep yukarıda anlatılan dokunma testi kuralı: ebeveynin sınırları dışında dokunuş algılanmaz. Stack'i taşan parçayı kapsayacak kadar büyüt.
5. Align ya da Center'ın Stack'i büyütmesi
Rozeti köşeye koymak için konumlandırılmamış bir Align kullandıysan, Align mümkün olan en büyük alanı istediği için Stack de ekran genişliğine yayılır ve rozet ekranın köşesine gider. Konumlandırmayı Positioned ile yap ya da Stack'e bir boyut ver; aynı sorunun Center versiyonu Center yazısında.
Sık Sorulan Sorular
Bir widget'ı sonradan öne ya da arkaya nasıl alırım?
Flutter'da z-index yoktur; çizim sırası children listesindeki sıradır. Öne almak istediğin çocuğu listede sona taşırsın. Sıra kullanıcı etkileşimiyle değişecekse (örneğin seçilen kart öne gelsin) listeyi state'te tutup sıralamasını değiştirmen ve her çocuğa bir Key vermen gerekir; aksi hâlde Flutter çocukların State'lerini yanlış eşleştirebilir.
Stack ile Overlay arasındaki fark nedir?
Stack, katmanları kendi kapladığı alanın içinde tutar. Açılır menü, tooltip ya da bir alanın dışına, sayfanın geri kalanının üstüne çıkması gereken bir öneri kutusu için Flutter'ın Overlay altyapısı kullanılır; OverlayPortal widget'ı bunun widget ağacıyla uyumlu hâlidir. Ekranın belli bir parçasındaki katmanlar için Stack, uygulamanın her şeyinin üstünde süzülen öğeler için Overlay doğru araçtır.
Positioned değerleri yüzde olarak verilebilir mi?
Positioned yalnızca piksel alır. Oransal konum istiyorsan konumlandırılmamış bir Align (örneğin Alignment(0.8, -0.6)) ya da Stack'i bir LayoutBuilder ile sarıp gelen genişlik ve yükseklikten pikseli hesaplamak işe yarar.
İlgili Yazılar
Flutter: Align Widget ile İnce Hizalama İşlemleri
Align ile hassas hizalama: Alignment(x, y) koordinat sistemi, FractionalOffset, widthFactor ve heightFactor, Stack içinde Align mı Positioned mı, hatalar.
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ü.