Flutter projelerinde sık kullanılan Dart extension methods, kaynak kodunu veya kalıtım zincirini değiştirmeden var olan bir tipe yeni metotlar, getter’lar ve işleçler ekler. String, List, BuildContext ya da Widget gibi sizin yazmadığınız tipler de buna dahildir. Özellik Dart 2.7 ile geldi ve çözümlemesi statiktir: extension’ın kapsamda olması ve alıcının statik tipinin eşleşmesi gerekir.
Yazı temel söz dizimiyle başlıyor; ardından String ve List örneklerine, Flutter’a özgü BuildContext ve Widget extension’larına, nullable tiplere, isim çakışmalarının çözümüne ve extension’ın çalışmadığı durumlara geçiyor.
Extension method nedir?
Bir extension, extension Ad on Tip { ... } biçiminde tanımlanır. Gövdedeki üyeler, this üzerinden alıcı nesneye erişir:
extension StringExtras on String {
// İlk harfi büyük yapar
String get capitalized =>
isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
}
void main() {
print('ankara'.capitalized); // Ankara
}
Bu tanım String sınıfına gerçek bir üye eklemez. Derleyici 'ankara'.capitalized ifadesini görünce alıcının statik tipine bakar, kapsamdaki extension’lar arasında eşleşeni bulur ve çağrıyı ona yönlendirir. Sarmalayıcı (wrapper) nesne oluşturulmaz ve alt sınıf gerekmez.
C# dünyasında aynı fikir uzun süredir vardır; LINQ operatörleri de IEnumerable<T> üzerine yazılmış extension method’lardır.
Neden kullanılır?
- Yardımcı fonksiyon yerine doğal çağrı:
StringUtils.capitalize(text)yerinetext.capitalizedyazmak, işlemi verinin yanına taşır. - Zincirleme:
' ankara '.trim().capitalizedgibi çağrılar soldan sağa okunur. - Alana özgü sözlük: Projeye özgü kavramlar (örneğin bir tarih tipine
isWeekend) ilgili tipin üzerinde toplanır.
String örnekleri ve Türkçe büyük harf
extension StringExtras on String {
String get capitalized =>
isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
// Kaba kontrol: yalnız '@' ve '.' varlığına bakar.
// "a@." gibi geçersiz değerler de true döner; gerçek doğrulama sunucuda yapılmalı.
bool get looksLikeEmail => contains('@') && contains('.');
}
void main() {
print('john'.capitalized); // John
print('test@mail.com'.looksLikeEmail); // true
print('istanbul'.capitalized); // Istanbul (İstanbul değil)
}
String.toUpperCase() dilden bağımsız Unicode eşlemesini kullanır; bu yüzden Türkçedeki noktalı i harfi I olur ve 'istanbul'.capitalized sonucu Istanbul çıkar. Türkçe metinde intl paketindeki toBeginningOfSentenceCase(metin, 'tr') fonksiyonu kullanılabilir; bu fonksiyon Türkçe ve Azericedeki noktalı i dönüşümünü ayrıca ele alır.
Tip parametreli List extension’ı
Extension’lar tip parametresi alabilir (generic). Aşağıdaki örnek listeden rastgele eleman seçer ve listeyi parçalara böler:
import 'dart:math';
final _random = Random();
extension ListExtras<T> on List<T> {
// Rastgele bir eleman döndürür; liste boşsa StateError atar
T get randomItem {
if (isEmpty) {
throw StateError('Boş listeden eleman seçilemez');
}
return this[_random.nextInt(length)];
}
// Listeyi en fazla size uzunluğunda parçalara böler
List<List<T>> chunk(int size) {
if (size <= 0) {
throw ArgumentError.value(size, 'size', 'Pozitif olmalı');
}
return [
for (var i = 0; i < length; i += size) sublist(i, min(i + size, length)),
];
}
}
void main() {
final items = [1, 2, 3, 4, 5];
print(items.randomItem); // 1 ile 5 arasında bir değer
print(items.chunk(2)); // [[1, 2], [3, 4], [5]]
}
size kontrolü olmadan chunk(0) sonsuz döngüye girer; bu yüzden geçersiz argüman en başta reddedilir. Random() kriptografik amaçlar için uygun değildir; güvenlik gerektiren seçimlerde Random.secure() kullanılmalıdır. Parçalama ihtiyacı için hazır bir çözüm de vardır: package:collection içindeki slices(n) metodu aynı işi yapar ve n < 1 için RangeError atar.
Flutter’da extension kullanımı
Flutter’da en yaygın kullanım BuildContext ve Widget üzerindedir. Aşağıdaki extension’lar Flutter API’sine ince takma adlar (alias) ekler:
import 'package:flutter/material.dart';
extension ContextExtras on BuildContext {
// Theme.of(this) için kısa yol
ThemeData get theme => Theme.of(this);
TextTheme get textTheme => Theme.of(this).textTheme;
// Yalnız ekran boyutu değişince yeniden çizim tetikler
Size get screenSize => MediaQuery.sizeOf(this);
}
extension WidgetPadding on Widget {
// Widget'ı her yönden eşit boşlukla sarar
Widget paddingAll(double value) =>
Padding(padding: EdgeInsets.all(value), child: this);
}
class Greeting extends StatelessWidget {
const Greeting({super.key});
@override
Widget build(BuildContext context) {
final isWide = context.screenSize.width > 600;
return Text(
'Hoş geldiniz',
style: isWide
? context.textTheme.headlineMedium
: context.textTheme.titleMedium,
).paddingAll(16);
}
}
Bu kısayolların ne yaptığını bilmek önemlidir. context.theme arka planda Theme.of(context), context.screenSize ise MediaQuery.sizeOf(context) çağırır; ikisi de widget’ı ilgili üst widget’a bağımlı hâle getirir ve değişiklikte yeniden çizim tetikler. Extension bu bağımlılığı ortadan kaldırmaz, yalnız çağrıyı kısaltır. paddingAll de ağaca normal bir Padding widget’ı ekler; ağacın yapısı Padding(child: ...) yazmakla aynıdır.
Tarih biçimlendirme ve karşılaştırma için DateTime üzerine yazılan extension’lar da sık görülür.
Nullable tiplerde extension
Extension, nullable bir tip üzerine de yazılabilir. Bu durumda alıcı null olduğunda da çağrı yapılabilir:
extension NullableStringExtras on String? {
bool get isNullOrEmpty => this?.isEmpty ?? true;
String orDefault(String defaultValue) => this ?? defaultValue;
}
void main() {
String? name;
if (name.isNullOrEmpty) {
print('İsim boş');
}
print(name.orDefault('Misafir')); // Misafir
}
Sınırlar ve isim çakışmaları
Extension’ın statik çözümlenmesi bazı sınırlar getirir:
dynamicalıcıda çalışmaz.dynamic d = 'ankara'; d.capitalized;derlenir ama çalışma anındaNoSuchMethodErroratar, çünkü derleyicidynamiciçin extension aramaz.- Sınıfın kendi üyesi her zaman önce gelir. Alıcının tipinde aynı adda bir üye varsa extension devreye girmez. Örneğin
Stringüzerinelengthadında bir extension getter’ı yazmak'abc'.lengthsonucunu değiştirmez. - Statik tip belirleyicidir. Değişkenin statik tipi
Objectise (örneğinObjectdöndüren bir fonksiyondan geliyorsa), çalışma anındaki değerStringolsa bileStringextension’ı uygulanmaz.
İki extension aynı adda üye tanımladığında çakışmayı çözmenin üç yolu vardır:
- Görünürlüğü sınırlamak:
import 'string_apis_2.dart' hide OtherStringExtras;gibi birshow/hideifadesiyle çakışan extension’ı o dosyada gizlemek. - Açıkça uygulamak: Extension adını bir sarmalayıcı sınıf gibi kullanmak. Böylece hangi extension’ın çağrıldığı kesinleşir:
final title = StringExtras('ankara').capitalized;
- Önekle içe aktarmak: İki extension’ın adı da aynıysa kütüphanelerden birini
import 'string_apis_2.dart' as other;ile önekli almak ve o extension’ıother.StringExtras(text).capitalizedbiçiminde açıkça uygulamak.
Adsız extension’lar (extension on String { ... }) yalnız tanımlandıkları kütüphanede görünür ve açıkça uygulanamaz.
Performans açısından fark yoktur: Dart dokümantasyonuna göre extension method’lar statik çözümlendiği için çağrıları statik bir fonksiyon çağrısı kadar hızlıdır.
Extension mı, yardımcı sınıf mı?
Extension uygun olduğunda:
- İşlem tipin doğal sözlüğüne uyuyorsa (
text.capitalized,date.isWeekend) - Çağrının zincire katılması okunabilirliği artırıyorsa
- Davranış tek bir alıcı tipi etrafında dönüyorsa
Yardımcı sınıf ya da fonksiyon uygun olduğunda:
- İşlem birden fazla parametreyle karmaşık bir mantık yürütüyorsa
- Belirli bir alıcı tipi yoksa ya da işlem birden fazla tipi eşit derecede ilgilendiriyorsa
- Çağrının nereden geldiğinin açıkça görünmesi gerekiyorsa
İyi uygulamalar
- İsimlendirme: Extension’lara açıklayıcı adlar verin (
StringExtras,DateTimeFormatting). Adlı extension, çakışmada açık uygulamaya vehideile gizlemeye izin verir. - Kapsam: Extension’ları yalnız gereken dosyalarda içe aktarın; her şeyi tek bir genel dosyada toplamak çakışma riskini artırır.
- Çakışmalar: Aynı adda üye çıktığında
hide, önekli içe aktarma ya da açık uygulama kullanın. - Dokümantasyon: Üyelere
///ile belge yorumu ekleyin; IDE bu açıklamayı otomatik tamamlamada gösterir.
Sonuç
Extension methods, küçük ve tip odaklı yardımcı davranışları çağrı noktasına yaklaştırır ve Flutter’da BuildContext ile Widget üzerindeki tekrarlı kodu kısaltır. Ancak gizli bağımlılık üreten BuildContext kısayolları veya çok genel isimler kodun kökenini belirsizleştirebilir. Yalnız tipin doğal sözlüğüne uyan, kapsamı açık ve çakışma riski düşük davranışları extension olarak tanımlayın; enum’lara davranış eklerken de aynı ölçü geçerlidir (Dart enum kullanımı).