Flutter’da Sealed Class Kavramı
Merhaba değerli geliştirici arkadaşlar, bu yazımda sizlere Flutter’da Sealed Class kavramını açıklamaya çalışıyor olacağım. Bu kavramı anlamak için temel bilgilere sahip olmanız gerekiyor. Dart OOP serisinde, bu kavramını daha iyi anlamanıza yardımcı olacak konulara değinmiştim. Buraya Tıklayarak ilgili makaleme erişebilirsiniz. Sözü daha fazla uzatmanda konumuza geçelim.
Yazılım geliştirmede en temel hedeflerimizden biri, belirsizliği ortadan kaldırarak öngörülebilir ve sağlam kod yazmaktır. Özellikle kullanıcı arayüzü (UI) ve durum yönetimi (state management) gibi dinamik alanlarda, bir durumun alabileceği tüm olasılıkları kontrol altında tutmak, hataları ayıklamanın değil, en baştan önlemenin anahtarıdır.
İşte bu noktada Flutter (ve Dart dili) bize güçlü bir araç sunuyor: Sealed Class (Mühürlenmiş Sınıf). Bu kavrama direkt geçmeden önce gerekli hatırlatmaları ne olur ne olmaz diyerek yapmak istiyorum.
Temel Kavramları Hatırlayalım
Sealed class’ı anlamak için önce onun yapı taşlarını tanımamız gerekir.
1. Sınıf (Class): Bir nesnenin planıdır. Örneğin, bir Araba sınıfı, tüm arabaların sahip olacağı renk, model, hiz gibi özellikleri ve calistir(), durdur() gibi davranışları tanımlar. Bu plandan istediğimiz kadar araba nesnesi üretebiliriz.
2. Enum (Enumeration — Numaralandırma): Bir değişkenin alabileceği sabit ve sınırlı değerler kümesidir. Örneğin, bir API isteğinin durumu için Durum { yukleniyor, basarili, hata } şeklinde bir enum tanımlayabiliriz. Enum’lar harikadır, ancak bir dezavantajları vardır: Her bir durum, kendisiyle ilişkili farklı türde veri taşıyamaz. basarili durumu bir veri listesi taşırken, hata durumu bir metin mesajı taşımak isteyebilir. Enum bunu doğal bir yolla yapamaz.
3. Soyut Sınıf (Abstract Class): “Kendisi bir nesne olamayan, ancak başka sınıflara miras vererek onlara bir şablon sunan” sınıftır. Örneğin, bir Hayvan soyut sınıfı tanımlayıp, tüm hayvanların sesCikar() adında bir metoda sahip olması gerektiğini söyleyebiliriz. Ancak Hayvan sınıfından doğrudan bir nesne üretemeyiz. Sadece ondan türeyen Kedi veya Kopek gibi somut sınıflardan nesneler üretebiliriz.
Bu üç kavramı aklımızın bir köşesinde tutarak asıl konumuza giriş yapalım.
Sealed Class Nedir?
Sealed Class, en basit tanımıyla, sınırlı ve bilinen bir alt sınıf hiyerarşisi oluşturmanıza olanak tanıyan soyut bir sınıftır.
Kulağa karmaşık mı geldi? Harika bir betimleme ile basitleştirelim.
Bir restorana gittiğinizi hayal edin. Menü (Sealed Class), o restoranda sipariş edebileceğiniz tüm yemeklerin (Alt Sınıflar) listesidir.
- Menü Soyuttur: Menünün kendisini yiyemezsiniz. O sadece bir listedir (Sealed Class soyuttur, ondan nesne üretilemez).
- Seçenekler Sınırlıdır: Sadece menüde yazan yemekleri sipariş edebilirsiniz: Çorba, Izgara Tavuk, Salata… Menüde olmayan bir “Limonlu Kalamar” sipariş edemezsiniz. Çünkü bu restoranın mutfağı sadece menüdeki yemekleri hazırlamayı bilir (Sealed Class’ın tüm alt sınıfları önceden bellidir ve dışarıdan yeni bir alt sınıf eklenemez).
- Her Seçenek Farklıdır: Çorba (Alt Sınıf 1) servis edilirken yanında kaşık gelir. Izgara Tavuk (Alt Sınıf 2) servis edilirken yanında pilav ve bıçak gelir. Salata (Alt Sınıf 3) servis edilirken yanında sos şişesi gelir. Her yemek, kendine özgü özelliklere ve “ekstralara” sahiptir (Her alt sınıf, kendi özel verisini taşıyabilir).
İşte sealed class tam olarak bu menü gibidir. Bir durumun alabileceği tüm olasılıkları tek bir çatı altında toplar ve her bir olasılığın kendine özgü veriler taşımasına izin verir.
Sealed Class’ın Altın Kuralları
- sealed anahtar kelimesi ile tanımlanırlar.
- Doğaları gereği abstract (soyut)’turlar, yani onlardan doğrudan nesne (new SealedClassName()) oluşturulamaz.
- En önemli kural: Tüm doğrudan alt sınıfları, sealed class’ın tanımlandığı aynı dosya içinde bulunmak zorundadır. Bu “mühür”ün ta kendisidir. Bu sayede Dart derleyicisi, bu sınıfın alabileceği tüm olası formları %100 bilir.
Şimdi dilerseniz gelin bu kavramı bir örnek senaryo ile daha iyi anlamaya çalışalım.
Tipik Bir Senaryo
Bir API’den veri çeken bir sayfa düşünelim. Bu sayfanın durumu şu olasılıklardan biri olabilir:
- Initial: Henüz hiçbir şey yapılmamış, başlangıç durumu.
- Loading: Veri çekiliyor, bir yüklenme göstergesi (spinner) gösterilmeli.
- Success: Veri başarıyla çekildi, veriler ekranda gösterilmeli.
- Error: Bir hata oluştu, kullanıcıya bir hata mesajı gösterilmeli.
Bu durumu modellemek için sealed class kullanalım.
// api_state.dart dosyası
// İşte "Menümüz": ApiState.
// Bu durumun alabileceği tüm olasılıklar bu dosyada olacak.
sealed class ApiState {}
// Menüdeki ilk seçenek: Başlangıç durumu. Hiçbir ek veriye ihtiyacı yok.
class ApiInitial extends ApiState {}
// İkinci seçenek: Yüklenme durumu. Bunun da ek veriye ihtiyacı yok.
class ApiLoading extends ApiState {}
// Üçüncü seçenek: Başarı durumu.
// Dikkat: Bu durum, kendine özgü bir veri taşıyor: bir "makale listesi".
class ApiSuccess extends ApiState {
final List<String> articles;
ApiSuccess(this.articles);
}
// Dördüncü seçenek: Hata durumu.
// Bu da kendine özgü bir veri taşıyor: bir "hata mesajı".
class ApiError extends ApiState {
final String errorMessage;
ApiError(this.errorMessage);
}
Gördüğünüz gibi, ApiState çatısı altında dört farklı ama ilişkili durumu zarifçe modelledik. ApiSuccess kendi verisini, ApiError da kendi verisini taşıyor. Bu, bir enum ile yapamayacağımız bir şeydi.
when ile Tükenme Kontrolü (Exhaustiveness Checking)
Sealed class’ın gerçek gücü, onu kullandığınız yerde ortaya çıkar. Dart derleyicisi, bir sealed class’ın tüm olası alt sınıflarını bildiği için, bu durumları kontrol ederken hiçbirini unutmamanızı sağlar. Buna tükenme kontrolü (exhaustiveness checking) denir.
Flutter’da bu kontrolü genellikle when metodu (veya Dart 3 ile gelen switch ifadeleri) ile yaparız.
Diyelim ki ApiState türünde bir state değişkenimiz var ve bu duruma göre bir Widget döndürmek istiyoruz.
// Bu kod genellikle bir Bloc, Provider veya başka bir state management çözümünün
// build metodunda yer alır.
Widget buildWidgetForState(ApiState state) {
// Dart 3 ve sonrası için modern switch kullanımı
return switch (state) {
// Eğer durum ApiInitial ise...
ApiInitial() => Text('Başlamak için butona dokunun'),
// Eğer durum ApiLoading ise...
ApiLoading() => Center(child: CircularProgressIndicator()),
// Eğer durum ApiSuccess ise...
// Dikkat: "success" adıyla içindeki veriye ulaşıyoruz!
ApiSuccess(articles: final articles) => ListView.builder(
itemCount: articles.length,
itemBuilder: (context, index) => ListTile(
title: Text(articles[index]),
),
),
// Eğer durum ApiError ise...
// "error" adıyla içindeki hata mesajına ulaşıyoruz!
ApiError(errorMessage: final message) => Center(
child: Text(
'Bir hata oluştu: $message',
style: TextStyle(color: Colors.red),
),
),
// NOT: 'default' case'e ihtiyaç YOK! Çünkü tüm olasılıkları ele aldık.
};
}
Kodu incelediğinizde yani burada ne yaptık şimdi elimize ne geçti diye sorabilirsiniz onuda şöyle açıklayayım;
Gelecekte uygulamamıza yeni bir durum eklemeye karar verdiğimizi varsayalım. Örneğin, internet bağlantısı olmadığında gösterilecek bir ApiNoInternet durumu.
api_state.dart dosyamıza gidip şunu ekleyelim
// ... diğer sınıfların altına
class ApiNoInternet extends ApiState {}
Bu değişikliği yaptıktan sonra buildWidgetForState fonksiyonumuzun olduğu dosyayı açtığımızda, derleyici anında bir hata verecektir! Hata mesajı şöyle bir şey olacaktır:
“The switch is not exhaustive. Try adding a case for ‘ApiNoInternet’.”
(Switch ifadesi tüm durumları kapsamıyor. ‘ApiNoInternet’ için bir case eklemeyi deneyin.)
İşte olay burada gizli. Derleyici, yeni durumu ele almayı unuttuğumuzu bize söylüyor. Bu sayede, uygulamayı çalıştırıp “neden internet yokken ekran boş kalıyor?” gibi bir hatayı aramak yerine, daha kodu yazarken potansiyel bir hatayı yakalamış oluyoruz.
Bu, sealed class’ların en büyük faydasıdır: Çalışma zamanı hatalarını (runtime errors) derleme zamanı hatalarına (compile-time errors) dönüştürür.
Neden Sealed Class Kullanmalıyız?
Sealed class’lar ilk başta biraz soyut gelebilir, ancak sağladıkları faydalar onları modern Flutter geliştirmenin vazgeçilmez bir parçası yapar.
- Tip Güvenliği (Type Safety): Bir durumun ne olabileceğini ve her durumun hangi veriyi taşıyacağını net bir şekilde tanımlarsınız.
- Okunabilirlik ve Anlaşılırlık: Kodunuz, bir durumun alabileceği tüm olasılıkları tek bir yerde toplayarak kendini belgeler hale gelir. if (isLoading) { … } else if (error != null) { … } gibi karmaşık if-else zincirlerinden kurtulursunuz.
- Sağlamlık: Derleyicinin tükenme kontrolü sayesinde, yeni bir durum eklediğinizde uygulamanızın hiçbir yerinde o durumu ele almayı unutmazsınız. Bu, öngörülemeyen hataları büyük ölçüde azaltır.
- İfade Gücü: Karmaşık durumları ve olayları (events) basit, zarif ve güçlü bir şekilde modellemenizi sağlar.
Sealed class’lar, sadece bir dil özelliği değildir; onlar, yazılımda belirsizlikle savaşmak ve daha temiz, daha güvenli, daha bakımı kolay uygulamalar yazmak için bir düşünce biçimidir. Dart ekosisteminin bana göre geliştiricilere kazandırdığı mükemmel bir olaydır.
Değerli arkadaşlar bir makalemizin daha sonuna geldik. Umarım sizler için bu makale faydalı olmuştur. Sonraki makalelerde görüşmek üzere.
Github: www.github.com/abdullah017
Linkedin: www.linkedin.com/in/abdullahtas
Stackoverflow: https://stackoverflow.com/users/13807726/abdullah-t