Dart'ta Enum ve Composition: Kodu Nasıl Düzenleriz?
Son güncelleme:
6 dk okuma

Dart'a yeni başlayan öğrencilerin kodunda iki alışkanlık çok sık görülür. Birincisi, sabit seçenekleri metinle temsil etmek: pizza boyutu "kucuk", "orta", "buyuk" gibi String'lerle dolaşır. İkincisi, iki sınıf arasındaki ilişkiyi alanları kopyalayarak kurmak: araba sınıfının içine sahibin adı, yaşı ve kimlik numarası tek tek yazılır. Bu yazıda bu iki sorunun Dart'taki karşılığını ele alıyoruz: enum'lar ve composition.
Tip dönüşümleri ve static üyeler yazısını okuduysanız zemin hazır. Örnekler Dart 3 ile yazıldı; eski sürümlerde görmediğiniz switch ifadeleri ve alan taşıyan enum'lar burada merkezde.
Canlı Demo: Dart Enum ve Composition
Enum ve Composition kullanımlarını interaktif olarak deneyin:
String ile Seçenek Tutmanın Sorunu
Önce enum'un çözdüğü problemi görelim:
void siparisVer(String boyut) {
if (boyut == 'orta') {
print('Orta boy pizza hazırlanıyor');
} else if (boyut == 'buyuk') {
print('Büyük boy pizza hazırlanıyor');
} else {
print('Küçük boy pizza hazırlanıyor');
}
}
void main() {
siparisVer('Orta'); // Büyük harf: sessizce "Küçük boy" çalışır
}Kod derlenir, çalışır ve yanlış sonuç verir. Derleyici 'Orta' ile 'orta' arasındaki farkı bilemez, çünkü onun için ikisi de sadece bir String'dir. Enum ise olası değerleri bir tipe dönüştürür; listede olmayan bir değeri yazamazsınız bile.
Enum'un Temelleri
enum Renk { kirmizi, mavi, beyaz }
void main() {
final secilen = Renk.mavi;
print(secilen.name); // mavi
print(secilen.index); // 1
print(Renk.values); // [Renk.kirmizi, Renk.mavi, Renk.beyaz]
}Her enum'la birlikte üç şey ücretsiz gelir: değerin adını veren name, sırasını veren index ve tüm değerleri içeren values listesi. Editörünüz de Renk. yazdığınız anda seçenekleri önerir.
Switch ve Eksiksizlik Kontrolü
Dart 3 ile switch iki önemli değişiklik geçirdi. Birincisi, dolu bir case bittiğinde switch'ten otomatik çıkılır; eski örneklerde gördüğünüz break satırlarına artık gerek yok. İkincisi, bir enum üzerindeki switch eksiksiz olmak zorundadır: bir değeri atlarsanız kod derlenmez.
Değer üreten durumlarda switch ifadesi (switch expression) daha kısadır:
enum Renk { kirmizi, mavi, beyaz }
String renkAdi(Renk renk) => switch (renk) {
Renk.kirmizi => 'Kırmızı',
Renk.mavi => 'Mavi',
Renk.beyaz => 'Beyaz',
};
void main() {
print(renkAdi(Renk.mavi)); // Mavi
}Bir gün enum'a yesil eklediğinizi düşünün. Derleyici, bu değeri ele almayan her switch'i hata olarak gösterir; nereleri güncellemeniz gerektiğini aramak zorunda kalmazsınız.
Burada öğrencilerin sık yaptığı bir hata var: "ne olur ne olmaz" diye switch'in sonuna default ya da _ eklemek. Bu, eksiksizlik kontrolünü kapatır. Yeni eklenen yesil sessizce varsayılan dala düşer ve hatayı ancak çalışma zamanında fark edersiniz. Enum üzerinde switch yazarken tüm değerleri açıkça listelemek, derleyicinin sizin yerinize bekçilik yapmasını sağlar.
Alan ve Metot Taşıyan Enum'lar
Enum'lar yalnızca isim listesi değildir. Dart 2.17'den beri her değere veri bağlayabilir, enum'a metot ve getter ekleyebilirsiniz:
import 'dart:math';
enum PizzaBoyutu {
kucuk(cap: 25, fiyat: 180),
orta(cap: 30, fiyat: 240),
buyuk(cap: 35, fiyat: 310);
const PizzaBoyutu({required this.cap, required this.fiyat});
final int cap; // santimetre
final int fiyat;
double get alan => pi * (cap / 2) * (cap / 2);
double get santimetreKareFiyati => fiyat / alan;
bool dahaBuyukMu(PizzaBoyutu diger) => cap > diger.cap;
}
void main() {
const secim = PizzaBoyutu.orta;
print('${secim.name}: ${secim.cap} cm, ${secim.fiyat} TL');
print(secim.santimetreKareFiyati.toStringAsFixed(3));
print(PizzaBoyutu.buyuk.dahaBuyukMu(secim)); // true
}Artık boyuta ait her bilgi tek yerde. Başka bir dosyada if (boyut == 'orta') fiyat = 240; gibi dağınık eşleştirmeler kalmaz.
Bu tür enum'ların kuralları dokümanda net olarak listelenir:
- Tüm değerler enum'un en başında tanımlanır ve en az bir değer olmalıdır.
- Alanlar
finalolmak zorundadır; mixin'den gelenler dahil. - Generative constructor'lar
constolmalıdır. Factory constructor ise yalnızca mevcut değerlerden birini döndürebilir. - Enum başka bir sınıftan
extendsedemez, çünkü otomatik olarakEnumsınıfını genişletir. Amaimplementsvewithkullanabilir. index,hashCodeve==override edilemez;valuesadında bir üye tanımlanamaz.
Metinden Enum'a: byName ve asNameMap
Bir önceki yazıdaki int.parse ve int.tryParse ikilisinin enum'daki karşılığı da var. JSON'dan ya da shared_preferences gibi yerel depolamadan gelen 'orta' metnini enum değerine çevirmek için:
enum PizzaBoyutu { kucuk, orta, buyuk }
void main() {
final boyut = PizzaBoyutu.values.byName('orta'); // PizzaBoyutu.orta
print(boyut);
// PizzaBoyutu.values.byName('dev'); // ArgumentError fırlatır
final guvenli = PizzaBoyutu.values.asNameMap()['dev'] ?? PizzaBoyutu.orta;
print(guvenli); // PizzaBoyutu.orta
}Kaydederken de index yerine name kullanın. Enum'daki değerlerin sırasını değiştirdiğiniz ya da araya yeni bir değer eklediğiniz gün, kayıtlı 1 sayısı artık başka bir değeri gösterir. 'orta' metni ise değer yeniden adlandırılmadıkça aynı kalır.
Flutter'da Enum ile Ekran Durumu
Bir sayfanın yükleniyor, başarılı, hatalı ya da boş olma durumları tipik bir enum örneğidir. Switch ifadesi doğrudan widget döndürebilir:
import 'package:flutter/material.dart';
enum YuklemeDurumu { yukleniyor, basarili, hata, bos }
Widget durumaGoreGoster(YuklemeDurumu durum) => switch (durum) {
YuklemeDurumu.yukleniyor => const CircularProgressIndicator(),
YuklemeDurumu.basarili => const Text('Veriler yüklendi'),
YuklemeDurumu.hata => const Text('Bir hata oluştu'),
YuklemeDurumu.bos => const Text('İçerik bulunamadı'),
};Enum'un sınırı burada görünür: her değer tek bir sabittir, yani "hata" durumuna o anki hata mesajını, "başarılı" durumuna da yüklenen listeyi bağlayamazsınız. Durumlar kendi verisini taşımak zorunda kaldığında Dart 3'ün sealed sınıfları devreye girer; bunu kalıtım ve polymorphism yazısında ele alıyoruz.
Composition: "Sahiptir" İlişkisi
Composition, bir sınıfın başka bir sınıfın nesnesini alan olarak tutmasıdır. İlişki "araba bir kişiye sahiptir" diye okunur; İngilizcede buna has-a ilişkisi denir.
class Kisi {
final int id;
final String isim;
final int yas;
const Kisi({required this.id, required this.isim, required this.yas});
}
class Araba {
final Kisi sahip; // Composition: Araba bir Kisi'ye sahiptir
final String model;
final String plaka;
const Araba({required this.sahip, required this.model, required this.plaka});
}
void main() {
const ahmet = Kisi(id: 1233, isim: 'Ahmet', yas: 25);
const araba = Araba(sahip: ahmet, model: 'Toyota Corolla', plaka: '34 ABC 123');
print('${araba.sahip.isim} isimli kişinin aracı: ${araba.model}');
print('Plakası: ${araba.plaka}');
}Kişi bilgisi tek bir sınıfta yaşar. Yarın Ev, Sigorta ya da Randevu sınıfları da aynı Kisi'yi kullanabilir; kişiye telefon numarası eklemek istediğinizde yalnızca Kisi sınıfını değiştirirsiniz.
late ile required Birlikte Kullanılmaz
Bu yazının eski sürümünde alanlar late int id; şeklinde tanımlanmış ve constructor'da required this.id ile doldurulmuştu. İnternetteki pek çok örnekte hâlâ bu kalıp var ve öğrenciler bunu çok sık kopyalıyor. Sorun şu: late, "bu alanı daha sonra, constructor dışında bir yerde atayacağım" demektir. Oysa required this.id alanın nesne oluşturulurken zaten atanacağını garanti eder. late burada hiçbir iş görmez ama iki şey kaybettirir:
- Alan değiştirilebilir hâle gelir;
kisi.id = 5gibi bir atama derleyiciden geçer. - Nesne
constolarak oluşturulamaz.
Doğrusu, yukarıdaki gibi final alan ve const constructor'dır. late'i yalnızca değeri gerçekten constructor'da bilemediğiniz durumlara (örneğin initState içinde oluşturulan bir controller) saklayın.
Değişmez Nesneyi Güncellemek: copyWith
Alanlar final olunca "yaşı nasıl güncelleyeceğim?" sorusu gelir. Cevap, değişikliği içeren yeni bir nesne üretmektir:
class Kisi {
final int id;
final String isim;
final int yas;
const Kisi({required this.id, required this.isim, required this.yas});
Kisi copyWith({String? isim, int? yas}) => Kisi(
id: id,
isim: isim ?? this.isim,
yas: yas ?? this.yas,
);
}
void main() {
const kisi = Kisi(id: 1, isim: 'Ahmet', yas: 25);
final birYilSonra = kisi.copyWith(yas: 26);
print('${kisi.yas} -> ${birYilSonra.yas}'); // 25 -> 26
}Flutter'daki ThemeData.copyWith ve TextStyle.copyWith da aynı fikre dayanır. Değişmez nesneler, state management araçlarının "veri değişti mi?" sorusunu da kolaylaştırır.
Enum ve Composition Birlikte
İki kavram gerçek bir modelde yan yana kullanılır. Bir sipariş, bir müşteriye (composition), bir boyuta (enum) ve ekstralar listesine sahiptir:
enum PizzaBoyutu {
kucuk(fiyat: 180),
orta(fiyat: 240),
buyuk(fiyat: 310);
const PizzaBoyutu({required this.fiyat});
final int fiyat;
}
enum Ekstra {
peynir(fiyat: 30),
mantar(fiyat: 25),
zeytin(fiyat: 20);
const Ekstra({required this.fiyat});
final int fiyat;
}
class Kisi {
final String isim;
const Kisi(this.isim);
}
class Siparis {
final Kisi musteri;
final PizzaBoyutu boyut;
final List<Ekstra> ekstralar;
const Siparis({
required this.musteri,
required this.boyut,
this.ekstralar = const [],
});
int get toplam => ekstralar.fold(boyut.fiyat, (t, e) => t + e.fiyat);
}
void main() {
const siparis = Siparis(
musteri: Kisi('Elif'),
boyut: PizzaBoyutu.orta,
ekstralar: [Ekstra.peynir, Ekstra.mantar],
);
print('${siparis.musteri.isim}: ${siparis.toplam} TL'); // Elif: 295 TL
}Fiyat bilgisi enum'larda, kişi bilgisi Kisi'de, hesap mantığı Siparis'te duruyor. Her sınıfın tek bir sorumluluğu var.
Flutter Zaten Composition ile Çalışır
Flutter'da kendi widget'ınızı yazarken Card ya da ListTile sınıfını genişletmezsiniz; onları build metodunun içinde bir araya getirirsiniz. Widget ağacı dediğimiz yapı baştan sona composition'dır:
import 'package:flutter/material.dart';
class Kullanici {
final String isim;
final String email;
const Kullanici({required this.isim, required this.email});
}
class KullaniciKarti extends StatelessWidget {
const KullaniciKarti({super.key, required this.kullanici});
final Kullanici kullanici; // Composition: kart bir kullanıcıya sahiptir
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
leading: CircleAvatar(
child: Text(kullanici.isim.isNotEmpty ? kullanici.isim[0] : '?'),
),
title: Text(kullanici.isim),
subtitle: Text(kullanici.email),
),
);
}
}Buradaki tek extends, Flutter'ın widget sözleşmesi olan StatelessWidget içindir. Görünümün kendisi, küçük widget'ların birleşiminden oluşur.
Composition mı, Kalıtım mı?
Karar için basit bir test işe yarar: ilişkiyi yüksek sesle okuyun. "Kedi bir hayvandır" doğal geliyorsa kalıtım (is-a) uygundur. "Araba bir kişiye sahiptir" doğal geliyorsa composition (has-a) uygundur. "Sipariş bir kişidir" gibi tuhaf bir cümle çıkıyorsa, kalıtım yalnızca alanları tekrar kullanmak için zorlanıyor demektir. Emin olamadığınızda composition daha esnek kalır, çünkü sahip olunan nesneyi değiştirmek bir sınıf hiyerarşisini değiştirmekten çok daha kolaydır.
Sık Yapılan Hatalar
- Sabit seçenekleri String ile tutmak; yazım hatası derleyiciye yakalanmaz.
- Enum üzerindeki switch'e
defaulteklemek; yeni değer eklendiğinde uyarı alamazsınız. - Enum'u
indexile kaydetmek; sıra değişince kayıtlı veri bozulur. - Alanları
lateyapıp constructor'darequired this.alanile doldurmak;finalkullanın. - Kod tekrar kullanımı için kalıtıma başvurmak; ilişki "sahiptir" ise composition kullanın.
Sonraki Adım
Enum'un taşıyamadığı durum verisi ve "is-a" ilişkileri için sıradaki konu kalıtım ve polymorphism. Composition'ı davranış değiştirilebilir hâle getirmek için de interface ve implements yazısına geçebilirsiniz.
Kaynaklar
İletişim:
Sık Sorulan Sorular
Dart'ta enum'lara alan ve metot eklenebilir mi?
Evet. Dart 2.17'den beri enum'lar final alanlar, const constructor, getter ve metotlar içerebilir. Örneğin her pizza boyutuna bir fiyat alanı bağlayıp PizzaBoyutu.orta.fiyat diye okuyabilirsiniz.
switch içinde bir enum değerini unutursam ne olur?
Dart 3'te enum üzerindeki switch eksiksiz olmak zorundadır; bir değeri atlarsanız kod derlenmez. Enum'a sonradan yeni bir değer eklediğinizde güncellemeniz gereken yerleri derleyici size gösterir. default eklerseniz bu kontrolden vazgeçmiş olursunuz.
Enum değerini kaydederken name mi index mi kullanmalıyım?
name kullanın. index değerlerin sırasına bağlıdır ve enum'a yeni bir değer eklendiğinde ya da sıra değiştiğinde eski kayıtlar yanlış değeri gösterir. Okurken values.byName veya bilinmeyen değerlere karşı values.asNameMap() kullanılır.
Composition mı kullanmalıyım, kalıtım mı?
İlişki "bir X'tir" (is-a) diye okunuyorsa kalıtım, "bir X'e sahiptir" (has-a) diye okunuyorsa composition uygundur. Emin olamadığınız durumlarda composition genellikle daha esnek kalır.
İlgili Yazılar
Dart'ta Interface ve Implements: Sınıflara Ekstra Özellik
Dart'ta implements ile sözleşme kurmak, Dart 3'ün interface class ve abstract interface class yapıları, mixin ile with farkı ve test edilebilir kod örneği.
Dart'ta Kalıtım (Inheritance) ve Polymorphism Rehberi
Dart'ta extends, super ve @override ile kalıtım, polymorphism, abstract sınıflar, güvenli tip kontrolü ve Dart 3'ün sealed, final, base sınıf niteleyicileri.
Flutter'da Kamerayı Kullanıcıya Bırakmak: Yön Zorlamadan Video Çekmek
Flutter camera paketinde ters dönen önizleme, kayıt başlarken donma ve kilitlenen kamera: Recapday'de çözdüğüm üç hatanın kodunu anlatıyorum.