TEKNİK YAZI / Developer Tools ve Açık Kaynak

Flutter Extension Methods: Var Olan Tiplere Davranış Eklemek

Flutter ve Dart extension methods ile String, List, BuildContext ve Widget tiplerine davranış eklemek; statik çözümleme, isim çakışması ve sınırlar.

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?

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:

İki extension aynı adda üye tanımladığında çakışmayı çözmenin üç yolu vardır:

final title = StringExtras('ankara').capitalized;

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:

Yardımcı sınıf ya da fonksiyon uygun olduğunda:

İyi uygulamalar

  1. İsimlendirme: Extension’lara açıklayıcı adlar verin (StringExtras, DateTimeFormatting). Adlı extension, çakışmada açık uygulamaya ve hide ile gizlemeye izin verir.
  2. Kapsam: Extension’ları yalnız gereken dosyalarda içe aktarın; her şeyi tek bir genel dosyada toplamak çakışma riskini artırır.
  3. Çakışmalar: Aynı adda üye çıktığında hide, önekli içe aktarma ya da açık uygulama kullanın.
  4. 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ı).

Birincil kaynaklar