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

Flutter: Kendi Widget'larımızı Oluşturmak

Ahmet Balaman

Son güncelleme:

5 dk okuma

FlutterWidgetStatelessWidgetStatefulWidgetConstructorconst
Flutter: Kendi Widget'larımızı Oluşturmak

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:

StatelessWidget parametreyi alıp çizer, StatefulWidget ise kendi State nesnesini tutar ve setState ile build’i yeniden çalıştırır

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.
  • build metodu bir ekrana sığmıyor, girintiler okunmaz hâle geldi.
  • Ekranın küçük bir kısmı sık güncelleniyor ve setState bü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 MyButton sarmalayıcısı yazmak yerine ThemeData iç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 + Text ikilisini 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 bool bayrağı olan bir widget, aslında iki ya da üç ayrı widget olmak istiyordur. child ya da leading/trailing gibi 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.

Yorumlar