Flutter: Kendi Widget'larımızı Oluşturmak
Son güncelleme:
5 dk okuma

Flutter'da ekranı hazır widget'ları iç içe koyarak kurarız; bir süre sonra aynı Card + Padding + Column yapısını üçüncü kez yazdığımızı fark ederiz. Kendi widget'ımızı oluşturmak tam bu noktada devreye girer: tekrar eden arayüz parçasına bir isim verir, değişen kısımlarını constructor parametresi yaparız. Sonuç daha kısa build metodları, tek yerden yapılan değişiklikler ve doğru kullanıldığında daha az yeniden çizimdir.
Canlı Demo: Özel Widget Oluşturma
Kendi widget'larınızı nasıl oluşturacağınızı interaktif olarak öğrenin:
💡 Eğer yukarıdaki örnek açılmazsa, DartPad linkine tıklayarak yeni sekmede çalıştırabilirsiniz.
StatelessWidget ile Temel Yapı
Kendi içinde değişen bir verisi olmayan, yalnızca aldığı parametreleri gösteren widget'lar için StatelessWidget kullanırız:
class MyCustomWidget extends StatelessWidget {
const MyCustomWidget({super.key});
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.all(16),
child: Text('Özel Widget\'ım'),
);
}
}İki ayrıntı her özel widget'ta tekrar eder. super.key, key parametresini üst sınıfa aktarmanın kısa yoludur; eski örneklerde göreceğiniz {Key? key}) : super(key: key) yazımıyla aynı işi yapar. Constructor'ın başındaki const ise widget'ı const MyCustomWidget() şeklinde oluşturabilmemizi sağlar. Flutter, const oluşturulmuş bir widget'ın değişmediğini bildiği için üst widget yeniden çizilirken onu atlayabilir.
Constructor ile Parametreler
Widget'ı özelleştirilebilir yapan şey parametreleridir. Alanlar her zaman final olur, değerler constructor'dan gelir:
class CustomButton extends StatelessWidget {
const CustomButton({
super.key,
required this.text,
required this.onPressed,
this.icon,
});
final String text;
final VoidCallback onPressed;
final IconData? icon;
@override
Widget build(BuildContext context) {
return FilledButton(
onPressed: onPressed,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: 18),
const SizedBox(width: 8),
],
Text(text),
],
),
);
}
}Kullanımı:
CustomButton(
text: 'Kaydet',
icon: Icons.save,
onPressed: () {
print('Kaydedildi');
},
)Widget kullanıcının ne yaptığını bilmez, yalnızca onPressed ile dışarıya haber verir. Kararı üstteki ekran verir. Bu "veri aşağı, olay yukarı" akışı, özel widget tasarımının temel kuralıdır.
Required, Optional ve Varsayılan Değerli Parametreler
class CustomCard extends StatelessWidget {
const CustomCard({
super.key,
required this.title, // Zorunlu
this.subtitle, // İsteğe bağlı (nullable)
this.padding = 16, // Varsayılan değerli
});
final String title;
final String? subtitle;
final double padding;
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Card(
child: Padding(
padding: EdgeInsets.all(padding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: textTheme.titleMedium),
if (subtitle != null) // Varsa göster
Text(subtitle!, style: textTheme.bodySmall),
],
),
),
);
}
}const CustomCard(title: 'Başlık') // subtitle yok
const CustomCard(
title: 'Başlık',
subtitle: 'Alt başlık',
padding: 24,
)Seçim kuralı basit: widget o değer olmadan anlamsızsa required, olmadan da çalışabiliyorsa nullable, çoğu kullanımda aynı değer veriliyorsa varsayılan değerli parametre. Kartın kendisiyle ilgili ayrıntılar Card yazısında.
child Parametresi ile Esnek Widget'lar
Her olasılık için parametre eklemek yerine içeriği dışarıdan almak çoğu zaman daha iyi bir tasarımdır. Flutter'ın kendi widget'ları da böyle çalışır:
class SectionBox extends StatelessWidget {
const SectionBox({
super.key,
required this.child,
this.borderRadius = 12,
});
final Widget child;
final double borderRadius;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(borderRadius),
),
child: child,
);
}
}const SectionBox(child: Text('İçerik'))
SectionBox(
child: Column(
children: const [Text('Başlık'), Text('Alt başlık')],
),
)Widget Sınıfı mı, Yardımcı Metod mu?
Uzun bir build metodunu bölmenin iki yolu var: _buildHeader() gibi Widget döndüren özel metodlar yazmak ya da parçayı ayrı bir widget sınıfına çıkarmak. İlki daha az kod gerektirir, ama ikisi aynı şey değildir:
// Yardımcı metod: üst widget her çizildiğinde bu da baştan çalışır
Widget _buildHeader() {
return Row(
children: [
CircleAvatar(child: Text(author[0])),
const SizedBox(width: 8),
Text(author),
],
);
}
// Ayrı widget: const olabilir, kendi BuildContext'i vardır
class _PostHeader extends StatelessWidget {
const _PostHeader({required this.author});
final String author;
@override
Widget build(BuildContext context) {
return Row(
children: [
CircleAvatar(child: Text(author[0])),
const SizedBox(width: 8),
Text(author),
],
);
}
}Yardımcı metod, Flutter'ın gözünde ayrı bir widget değildir; çıktısı doğrudan üst widget'ın build sonucuna yapışır. Üst widget'ta setState çağrıldığında metod da her seferinde yeniden çalışır. Ayrı bir sınıf ise const oluşturulabildiği ve parametreleri değişmediği sürece yeniden çizimden muaf tutulabildiği için sık güncellenen ekranlarda fark yaratır. Ayrıca kendi BuildContext'ine sahiptir, widget ağacında kendi adıyla görünür ve tek başına test edilebilir.
Pratik kural: parça birkaç satırsa ve yalnızca o build içinde anlamlıysa metod kalabilir. Parça büyüyorsa, başka yerde kullanılacaksa ya da üst widget sık sık yeniden çiziliyorsa sınıfa çıkarmak gerekir. Başındaki alt çizgi (_PostHeader) sınıfı dosyaya özel yapar; her parçayı projenin tamamına açmak zorunda değiliz. Ağacın nasıl kurulduğunu tazelemek için widget ağacı yazısına bakabilirsiniz.
StatefulWidget ile Özel Widget
Widget kendi içinde değişen bir değer tutacaksa StatefulWidget kullanılır. Adet seçici iyi bir örnek:
class QuantitySelector extends StatefulWidget {
const QuantitySelector({
super.key,
this.initialValue = 1,
required this.onChanged,
});
final int initialValue;
final ValueChanged<int> onChanged;
@override
State<QuantitySelector> createState() => _QuantitySelectorState();
}
class _QuantitySelectorState extends State<QuantitySelector> {
late int _quantity = widget.initialValue;
void _update(int value) {
if (value < 1) return;
setState(() => _quantity = value);
widget.onChanged(value);
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: () => _update(_quantity - 1),
),
Text('$_quantity', style: Theme.of(context).textTheme.titleLarge),
IconButton(
icon: const Icon(Icons.add),
onPressed: () => _update(_quantity + 1),
),
],
);
}
}Widget sayıyı kendisi tutuyor ama her değişikliği onChanged ile dışarıya bildiriyor; sepet toplamını hesaplamak üst ekranın işi. initState, dispose ve diğer adımlar yaşam döngüsü yazısında ayrıntılı olarak var.
Ne Zaman Kullanılır, Ne Zaman Kullanılmaz?
Şu işaretlerden biri varsa parçayı ayrı bir widget'a çıkarmanın zamanı gelmiştir:
- Aynı yapı ikinci ya da üçüncü kez yazılıyor.
buildmetodu bir ekrana sığmıyor, girintiler okunmaz hâle geldi.- Ekranın küçük bir kısmı sık güncelleniyor ve
setStatebütün sayfayı yeniden çizdiriyor.
Şu durumlarda ise ayrı widget gereksiz ya da yanlış araçtır:
- Yalnızca stil değişiyorsa: Bütün butonlar aynı renkte olsun diye
MyButtonsarmalayıcısı yazmak yerineThemeDataiçindeki buton temalarını ayarlamak daha doğrudur. Tema tek yerden yönetilir ve hazır widget'ların hepsini etkiler. - Tek seferlik, üç satırlık bir parça: Her
Padding + Textikilisini sınıfa çevirmek kodu dosyalar arasında dağıtır ve okumayı zorlaştırır. - Parametre sayısı kontrolden çıkıyorsa: On tane
boolbayrağı olan bir widget, aslında iki ya da üç ayrı widget olmak istiyordur.childya daleading/trailinggibi widget alan parametreler bayraklardan daha esnektir.
Sık Yapılan Hatalar
1. StatelessWidget içinde final olmayan alan
Belirti: Analizör "This class (or a class that this class inherits from) is marked as '@immutable', but one or more of its instance fields aren't final" uyarısını verir; alanı değiştirsek bile ekran güncellenmez.
// Yanlış
class PriceTag extends StatelessWidget {
PriceTag({super.key, required this.price});
double price; // final değil
// ...
}Widget'lar değişmez nesnelerdir. Değer dışarıdan değişecekse üst widget yeni değerle yeniden çizer; widget'ın kendi içinde değişecekse StatefulWidget ve State içinde bir alan gerekir.
2. const constructor yazmamak
Belirti: const MyWidget() yazdığımızda "The constructor being called isn't a const constructor" hatası. Constructor'a const eklemek için tüm alanların final olması yeterlidir. const olmadan da kod çalışır, ama Flutter'ın o widget'ı yeniden çizimde atlama fırsatını kaybederiz. Analizörün prefer_const_constructors önerilerini takip etmek bu alışkanlığı kendiliğinden kazandırır.
3. Callback'i geçirmek yerine çağırmak
// Yanlış: fonksiyon build sırasında hemen çalışır
CustomButton(text: 'Sil', onPressed: _delete())
// Doğru: fonksiyonun kendisi geçirilir
CustomButton(text: 'Sil', onPressed: _delete)Belirti: İşlem butona basılmadan, ekran açılır açılmaz çalışır. İçinde setState varsa "setState() or markNeedsBuild() called during build" hatası gelir. Parantez koymak fonksiyonu çağırır; parantezsiz yazmak ya da () => _delete() kullanmak gerekir.
4. Parametreyi State'e kopyalayıp güncellemeyi unutmak
Belirti: Üst widget yeni bir değer gönderiyor ama ekranda eski değer kalıyor. initState yalnızca bir kez çalıştığı için orada kopyalanan değer sonraki değişiklikleri görmez. Değer sadece gösterilecekse kopyalamadan doğrudan widget.value okunmalı; gerçekten kopyalanması gerekiyorsa didUpdateWidget içinde güncellenmelidir:
@override
void didUpdateWidget(covariant QuantitySelector oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.initialValue != widget.initialValue) {
_quantity = widget.initialValue;
}
}Mini Senaryo: Boş Durum Widget'ı
Hemen her uygulamada "henüz bir şey yok" ekranı birden fazla yerde gerekir: boş sepet, boş favoriler, sonuçsuz arama. Hepsi aynı iskelete sahiptir; ikon, başlık, açıklama ve isteğe bağlı bir buton.
class EmptyState extends StatelessWidget {
const EmptyState({
super.key,
required this.icon,
required this.title,
required this.message,
this.actionLabel,
this.onAction,
});
final IconData icon;
final String title;
final String message;
final String? actionLabel;
final VoidCallback? onAction;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 64, color: theme.colorScheme.outline),
const SizedBox(height: 16),
Text(title, style: theme.textTheme.titleLarge),
const SizedBox(height: 8),
Text(
message,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium,
),
if (actionLabel != null && onAction != null) ...[
const SizedBox(height: 24),
FilledButton(onPressed: onAction, child: Text(actionLabel!)),
],
],
),
),
);
}
}Sepet ekranında kullanımı tek bir koşula iniyor:
body: items.isEmpty
? EmptyState(
icon: Icons.shopping_cart_outlined,
title: 'Sepetin boş',
message: 'Beğendiğin ürünleri sepete ekleyince burada görünecek.',
actionLabel: 'Alışverişe Başla',
onAction: () => Navigator.pop(context),
)
: CartList(items: items),Renkleri ve yazı stillerini elle yazmak yerine temadan aldığımız için widget açık ve koyu temada ek iş gerektirmeden doğru görünüyor. Buton iki parametre birlikte verildiğinde çıkıyor; yalnızca bilgi veren bir boş durum için ikisini de atlamak yeterli.
Sık Sorulan Sorular
StatelessWidget mı, StatefulWidget mı kullanmalıyım?
Widget yalnızca dışarıdan aldığı değerleri gösteriyorsa StatelessWidget yeterlidir. Kendi içinde zamanla değişen bir değer, controller ya da animasyon tutuyorsa StatefulWidget gerekir. Şüphede kalındığında StatelessWidget ile başlamak ve ihtiyaç doğunca dönüştürmek daha sağlıklıdır.
super.key nedir, yazmak zorunlu mu?
super.key, widget'ın key parametresini üst sınıfa aktaran kısa yazımdır. Zorunlu değildir ama widget listelerde yeniden sıralandığında ya da durumunun korunması gerektiğinde key'e ihtiyaç olur; bu yüzden her özel widget'ta bulundurmak standart bir alışkanlıktır.
const constructor ne işe yarar?
const ile oluşturulan widget derleme zamanında tek bir örnek olarak üretilir ve Flutter onun değişmediğini bilir. Üst widget yeniden çizildiğinde bu alt ağaç atlanabilir, bu da özellikle sık güncellenen ekranlarda gereksiz işi azaltır.
Yardımcı metod mu, ayrı widget sınıfı mı daha iyi?
Küçük ve yalnızca tek bir build içinde anlamlı parçalar için yardımcı metod yeterlidir. Parça büyüdüğünde, tekrar kullanılacağında ya da üst widget sık yeniden çizildiğinde ayrı bir widget sınıfı const olabildiği ve kendi BuildContext'ine sahip olduğu için daha doğru tercihtir.
İlgili Yazılar
Flutter: ListView Widget Kullanımı
Flutter ListView rehberi: builder ile tembel listeleme, separated, itemExtent, shrinkWrap maliyeti ve unbounded height hatasının çözümü.
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: FutureBuilder ile Asenkron Listeleme
Flutter FutureBuilder rehberi: snapshot durumlarını doğru okuma, Future'ı build içinde oluşturma hatası, initState çözümü ve setState ile yenileme.