Flutter: MediaQuery ile Ekrana Göre Oranlama
Son güncelleme:
5 dk okuma

MediaQuery, uygulamanın çalıştığı pencere hakkındaki bilgileri (boyut, güvenli alan, klavye yüksekliği, yön, metin ölçeği) widget ağacına taşıyan widget'tır. "Kart ekranın yüzde 80'i olsun", "geniş ekranda yan menü göster", "klavye açılınca içeriği yukarı kaydır" gibi kararların hepsi buradan beslenir. Bu yazıda önce doğru okuma biçimini (MediaQuery.sizeOf gibi hedefli erişimciler), sonra hangi işin MediaQuery'ye, hangisinin LayoutBuilder veya FractionallySizedBox'a ait olduğunu ve en çok görülen hataları anlatıyorum.
Temel Kullanım
Pencere boyutunu okumanın iki yolu var. Eski alışkanlık MediaQuery.of(context).size yazmaktır; bu, widget'ı MediaQuery'deki her değişikliğe bağlar. Klavye açıldığında viewInsets değişir ve yalnızca genişliği merak eden widget'ınız da yeniden çizilir. Tek bir özelliğe bağlanan erişimciler bu sorunu çözer:
@override
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
final padding = MediaQuery.paddingOf(context);
return Container(
width: size.width * 0.8,
margin: EdgeInsets.only(top: padding.top),
color: Colors.blue,
child: Center(child: Text('${size.width.toInt()} x ${size.height.toInt()}')),
);
}sizeOf yalnızca boyut değişince, paddingOf yalnızca güvenli alan değişince rebuild tetikler. Her özellik için bir karşılığı vardır: orientationOf, viewInsetsOf, devicePixelRatioOf, platformBrightnessOf, textScalerOf; hepsinin tam listesi Flutter'ın MediaQuery API dokümanında statik metotlar başlığı altında duruyor. Yeni kodda MediaQuery.of(context) yazmak için bir sebep kalmadı.
Hangi Özellik Ne Söyler?
| Özellik | Anlamı | Tipik kullanım |
|---|---|---|
size |
Pencerenin mantıksal piksel cinsinden boyutu | Breakpoint kararları, yüzde hesabı |
padding |
Çentik, durum çubuğu, home göstergesi gibi sistem alanları | SafeArea bunu kullanır |
viewInsets |
Klavye gibi geçici olarak alanı kaplayan sistem arayüzü | Klavyeye göre alt boşluk |
orientation |
Genişlik yüksekten büyükse landscape |
Yatayda farklı düzen |
textScaler |
Kullanıcının sistemdeki metin büyütme ayarı | Erişilebilirlik testi, sabit yükseklikleri ölçekleme |
devicePixelRatio |
Mantıksal piksel başına fiziksel piksel | Uygun çözünürlükte görsel seçmek |
platformBrightness |
Sistemin açık/koyu tema tercihi | Genelde ThemeMode.system yeterlidir |
Tablodaki her alanın tam tanımını MediaQueryData sınıfının API dokümanında bulabilirsiniz. Dikkat edilmesi gereken bir nokta: size ekranı değil pencereyi verir. Telefonda ikisi aynıdır ama masaüstünde, web'de ve tablet bölünmüş ekranında pencere ekrandan küçüktür. Kararlarınızı "ekran" diye değil "pencere" diye düşünmek yanılgıları azaltır.
Yüzdeyle Boyutlama ve Alternatifi
Genişliğin yüzde 80'i kadar bir kart için size.width * 0.8 yazmak çalışır, ama bu kart bir yan panelin içine taşındığında oran panelin değil pencerenin yüzde 80'i olarak kalır. Üst widget'a göre oran istiyorsanız FractionallySizedBox bunu hesap yapmadan halleder:
Center(
child: FractionallySizedBox(
widthFactor: 0.8,
child: Card(child: Padding(padding: EdgeInsets.all(16), child: Text('İçerik'))),
),
)Kural olarak: "pencerenin yüzdesi" gerçekten isteniyorsa MediaQuery, "üst widget'ın yüzdesi" isteniyorsa FractionallySizedBox. Çoğu zaman ikincisi kastedilir.
MediaQuery mi LayoutBuilder mı?
İkisi farklı soruyu yanıtlar. MediaQuery.sizeOf "pencere ne kadar büyük?" der; LayoutBuilder "bu widget'a ne kadar yer verildi?" der. Bir ayar ekranını tablet'te sağ panelde açtığınızda pencere geniştir ama panele verilen genişlik dardır; LayoutBuilder doğru sonucu, MediaQuery yanlış sonucu verir.
Pratik ayrım şöyle: uygulama iskeletiyle ilgili kararlar (alt bar mı yan ray mı, tek sütun mu çift sütun mu) pencere boyutuna göre alınır, MediaQuery uygundur. Yeniden kullanılabilir bir bileşenin kendi içindeki düzen (kart yatay mı dikey mi) verilen alana göre alınır, LayoutBuilder uygundur. Ayrıntılı örnekleri ve breakpoint düzenini LayoutBuilder ile responsive tasarım yazısında bulabilirsiniz.
Metin Ölçeği
Kullanıcı sistemde metni büyüttüğünde Text widget'ı bunu zaten uygular; sizin fontSize ile çarpmanız gerekmez. MediaQuery.textScalerOf(context) iki durumda gerekir: metinle birlikte büyümesi gereken sabit yükseklikler ve büyümeyi sınırlama.
final scaler = MediaQuery.textScalerOf(context);
final rowHeight = scaler.scale(48); // 48 mantıksal piksel, metin ölçeğiyle büyür
// Belirli bir alt ağaçta büyütmeyi 1.3 ile sınırlamak:
MediaQuery.withClampedTextScaling(
maxScaleFactor: 1.3,
child: const PriceTag(),
)Eski textScaleFactor sayısal bir çarpandı ve kullanımdan kaldırıldı; TextScaler sınıfının API dokümanının anlattığı gibi TextScaler doğrusal olmayan ölçeklemeyi de destekler, bu yüzden değeri bir sayı gibi çarpmak yerine scale() çağırın.
Ne Zaman Kullanılır, Ne Zaman Kullanılmaz?
MediaQuery şu işlerde doğru araçtır: pencere genişliğine göre gezinme yapısını seçmek, güvenli alanı elle hesaba katmak gereken özel çizimler, klavye yüksekliğine göre alt boşluk vermek, erişilebilirlik için metin ölçeğini okumak, yöne göre tamamen farklı bir sayfa düzeni kurmak.
Şu durumlarda başka bir araç daha iyi:
- Bileşenin kendisine verilen alan önemliyse
LayoutBuilder; yalnızca yön gerekiyorsaOrientationBuilder(o da üst widget'ın kısıtlarına bakar). - Üst widget'ın yüzdesi için
FractionallySizedBox; kardeşler arasında oran içinExpanded. Bu ikisinin tarifleri Expanded, Flexible ve Spacer ile layout kontrolü yazısında var. - Güvenli alan için elle padding hesaplamak yerine
SafeArea. Klavye için elle boşluk yerineScaffold'un varsayılanresizeToAvoidBottomInset: truedavranışı. - Font boyutunu genişlikle çarpmak yerine
Theme.of(context).textThemeve birkaç breakpoint. Yüzdeyle büyüyen yazı tablette devasa, küçük telefonda okunmaz olur ve kullanıcının erişilebilirlik ayarını ezer.
Sık Yapılan Hatalar
1. initState içinde MediaQuery okumak
Belirti: dependOnInheritedWidgetOfExactType<MediaQuery>() or dependOnInheritedElement() was called before _MyPageState.initState() completed. initState çalışırken widget henüz ağaca bağlı değildir. Değeri build içinde okuyun; bir kez hesaplanacaksa didChangeDependencies doğru yerdir.
2. fontSize'ı textScaleFactor ile çarpmak
Belirti: Kullanıcı sistemde metni yüzde 150 büyüttüğünde yazılar iki kat büyür, düzen dağılır. Text ölçeği zaten uyguluyor; fontSize: 16 * factor yazmak çift ölçekleme demektir. Çarpmayı kaldırın; sabit yükseklikleri ölçeklemek istiyorsanız textScalerOf(context).scale(...) kullanın.
3. Scaffold içinde klavye için elle padding vermek
Belirti: Klavye açıldığında formun altında klavye yüksekliğinin iki katı boşluk kalır. Scaffold gövdeyi klavyeye göre zaten küçültür; üstüne padding: EdgeInsets.only(bottom: viewInsets.bottom) eklemek boşluğu ikiye katlar. Ya Scaffold'a bırakın ya da resizeToAvoidBottomInset: false yapıp boşluğu kendiniz yönetin; ikisi birden değil.
4. Yan panel içindeki widget'ta pencere genişliğine bakmak
Belirti: Tablette sağ panelde açılan kart, pencere geniş olduğu için "geniş" düzeni seçer ve taşar. Widget'a verilen alan pencereden bağımsızdır. Kararı LayoutBuilder'ın constraints.maxWidth değerine göre verin.
Mini Senaryo: Genişliğe Göre Gezinme
Bir not uygulaması düşünün. Telefonda altta NavigationBar, tablette ve masaüstünde solda NavigationRail olsun; içerik ise her iki durumda da güvenli alanın içinde kalsın:
class HomeShell extends StatefulWidget {
const HomeShell({super.key});
@override
State<HomeShell> createState() => _HomeShellState();
}
class _HomeShellState extends State<HomeShell> {
int _index = 0;
static const _destinations = [
(icon: Icons.notes, label: 'Notlar'),
(icon: Icons.search, label: 'Ara'),
(icon: Icons.settings, label: 'Ayarlar'),
];
@override
Widget build(BuildContext context) {
final isWide = MediaQuery.sizeOf(context).width >= 600;
final body = SafeArea(child: NotesPage(index: _index));
if (isWide) {
return Scaffold(
body: Row(
children: [
NavigationRail(
selectedIndex: _index,
onDestinationSelected: (i) => setState(() => _index = i),
labelType: NavigationRailLabelType.all,
destinations: [
for (final d in _destinations)
NavigationRailDestination(icon: Icon(d.icon), label: Text(d.label)),
],
),
const VerticalDivider(width: 1),
Expanded(child: body),
],
),
);
}
return Scaffold(
body: body,
bottomNavigationBar: NavigationBar(
selectedIndex: _index,
onDestinationSelected: (i) => setState(() => _index = i),
destinations: [
for (final d in _destinations)
NavigationDestination(icon: Icon(d.icon), label: d.label),
],
),
);
}
}Karar pencere genişliğine göre alındığı için MediaQuery.sizeOf doğru araç; bu bir iskelet kararı, bir bileşen kararı değil. sizeOf kullanıldığı için klavye açıldığında bu widget yeniden çizilmez; yalnızca pencere döndüğünde ya da masaüstünde yeniden boyutlandığında çalışır. Güvenli alanı SafeArea hallediyor, paddingOf ile elle hesap yok. NotesPage'in kendi içindeki düzen (notların tek mi iki sütun mu olduğu) ise pencereye değil ona verilen alana bağlı; orada LayoutBuilder kullanmak gerekir. İki gezinme widget'ının seçimi hakkında NavigationBar ve BottomNavigationBar karşılaştırması ayrıntı veriyor.
Sık Sorulan Sorular
MediaQuery.of ile MediaQuery.sizeOf arasındaki fark nedir?
MediaQuery.of(context) widget'ı MediaQuery'deki her değişikliğe bağlar; klavye açılınca bile rebuild olur. MediaQuery.sizeOf(context) yalnızca boyut değiştiğinde rebuild tetikler. Aynı mantıkla paddingOf, viewInsetsOf ve textScalerOf da vardır.
MediaQuery mi LayoutBuilder mı kullanmalıyım?
Pencere boyutuna göre iskelet kararı alıyorsanız (alt bar mı yan ray mı) MediaQuery. Bir bileşenin kendisine verilen alana göre düzen kuruyorsanız LayoutBuilder. Yeniden kullanılabilir bileşenlerde neredeyse her zaman ikincisi doğrudur.
Font boyutunu ekran genişliğine göre ölçeklemeli miyim?
Hayır. Text kullanıcının sistem metin ayarını zaten uygular; genişlikle çarpmak tablette devasa yazı üretir ve erişilebilirlik ayarını bozar. Birkaç breakpoint ile textTheme stillerini değiştirmek yeterlidir.
Klavye açıldığında içerik neden taşıyor?
Scaffold gövdeyi varsayılan olarak klavyeye göre küçültür; taşma oluyorsa gövde kaydırılabilir değildir. Formu SingleChildScrollView veya ListView içine alın. viewInsets ile elle boşluk vermek yalnızca resizeToAvoidBottomInset: false yaptığınızda gerekir.
İlgili Yazılar
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: 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: Padding ile İç Boşluk Yönetimi
Padding ve EdgeInsets'in tüm kurucuları, EdgeInsetsDirectional ile RTL desteği, SafeArea ve klavye boşlukları, sık yapılan hatalar ve bir form örneği.