↑↓ seç · Enter aç · Esc kapat

Mutlu Tekin
Mutlu Tekin
← Yazılar

Bölüm 7 — Konu 34: Clean Architecture Uyarlaması, SOLID Prensipleri

5 dk okuma #flutter
Dizi · 34/61 Flutter Türkçe Tutorial
  1. Bölüm 1 — Konu 1: Flutter Nedir, Mimarisi (Widget → Element → RenderObject, Skia/Impeller)
  2. Bölüm 1 — Konu 2: Ortam Kurulumu (Flutter SDK, Android Studio/VS Code, Emulator/Simulator, DevTools'a İlk Bakış)
  3. Bölüm 1 — Konu 3: İlk Proje Yapısı (`pubspec.yaml`, `lib/`, Klasör Mimarisi, `flutter create` Anatomisi)
  4. Bölüm 1 — Konu 4: Hot Reload / Hot Restart Ne Yapıyor, Neden Önemli
  5. Bölüm 2 — Konu 5: "Her Şey Widget'tır" Felsefesi, Widget Ağacı
  6. Bölüm 2 — Konu 6: StatelessWidget vs StatefulWidget + `setState()` Derinlemesine (Rebuild Mekanizması)
  7. Bölüm 2 — Konu 7: Temel Layout — Container, Row, Column, Stack, Padding, Align, Center
  8. Bölüm 2 — Konu 8: Constraint Sistemi — "Constraints Go Down, Sizes Go Up, Parent Sets Position"
  9. Bölüm 2 — Konu 9: `Expanded`, `Flexible`, `Spacer`, Intrinsic Widget'lar
  10. Bölüm 3 — Konu 10: MaterialApp/CupertinoApp, Scaffold, AppBar
  11. Bölüm 3 — Konu 11: `ListView`, `GridView`, `SingleChildScrollView` (+ Builder Pattern)
  12. Bölüm 3 — Konu 12: Text, TextStyle, Icon, Image, Asset Yönetimi
  13. Bölüm 3 — Konu 13: Custom Widget Yazma Prensipleri (Composition Over Inheritance)
  14. Bölüm 3 — Konu 14: Navigator 1.0 — Push/Pop, Named Routes
  15. Bölüm 4 — Konu 15: Form, TextField/TextFormField, GlobalKey<FormState>, Validasyon
  16. Bölüm 4 — Konu 16: Theme Sistemi (ThemeData, ColorScheme)
  17. Bölüm 4 — Konu 17: Responsive & Adaptive Tasarım (MediaQuery, LayoutBuilder, OrientationBuilder, Breakpoint Stratejileri)
  18. Bölüm 5 — Konu 18: Future/Async-Await Flutter Bağlamında, FutureBuilder
  19. Bölüm 5 — Konu 19: `Stream`, `StreamBuilder`
  20. Bölüm 5 — Konu 20: HTTP İstekleri (`http` / `dio` Paketleri)
  21. Bölüm 5 — Konu 21: JSON Serialization (Manuel → `json_serializable`/`freezed`)
  22. Bölüm 5 — Konu 22: Local Storage — `shared_preferences` → `Hive` → `sqflite`/Drift
  23. Bölüm 6 — Konu 23: InheritedWidget ve InheritedModel — "Neden setState Yetmiyor" Sorusunun Cevabı
  24. Bölüm 6 — Konu 24: Provider Paketi
  25. Bölüm 6 — Konu 25: Riverpod (Modern Yaklaşım, Provider'ın Halefi)
  26. Bölüm 6 — Konu 26: BLoC/Cubit Pattern (`flutter_bloc`)
  27. Bölüm 6 — Konu 27: GetX (Tartışmalı Ama Yaygın)
  28. Bölüm 6 — Konu 28: Karşılaştırma — Hangi Projede Hangisi?
  29. Bölüm 7 — Konu 29: Navigator 2.0 (Router, RouteInformationParser, RouterDelegate)
  30. Bölüm 7 — Konu 30: `go_router` Paketi (Pratik ve Modern Çözüm)
  31. Bölüm 7 — Konu 31: Deep Linking
  32. Bölüm 7 — Konu 32: Repository Pattern, Katmanlı Mimari (Data/Domain/Presentation)
  33. Bölüm 7 — Konu 33: Dependency Injection (`get_it`, `injectable`)
  34. Bölüm 7 — Konu 34: Clean Architecture Uyarlaması, SOLID Prensipleri
  35. Bölüm 8 — Konu 35: Implicit Animasyonlar
  36. Bölüm 8 — Konu 36: Explicit Animasyonlar (`AnimationController`, `Tween`, `Curve`)
  37. Bölüm 8 — Konu 37: Hero Animasyonları
  38. Bölüm 8 — Konu 38: `CustomPainter` / `Canvas`
  39. Bölüm 8 — Konu 39: Rive / Lottie Entegrasyonu
  40. Bölüm 9 — Konu 40: Platform Channels
  41. Bölüm 9 — Konu 41: Permission Yönetimi (`permission_handler`)
  42. Bölüm 9 — Konu 42: Kamera, Konum, Sensörler
  43. Bölüm 9 — Konu 43: Push Notification (Firebase Cloud Messaging)
  44. Bölüm 9 — Konu 44: Android/iOS Build Sistemleri
  45. Bölüm 10 — Konu 45: Test Yazımı (Unit, Widget, Integration, Golden)
  46. Bölüm 10 — Konu 46: CI/CD
  47. Bölüm 10 — Konu 47: Rebuild Optimizasyonu (`const`, `key` Kullanımı)
  48. Bölüm 10 — Konu 48: DevTools Profiling
  49. Bölüm 10 — Konu 49: Lazy Loading, Pagination, Büyük Liste Performansı
  50. Bölüm 11 — Konu 50: Firebase Ekosistemi (Auth, Firestore, Storage, Functions)
  51. Bölüm 11 — Konu 51: Supabase Alternatifi
  52. Bölüm 11 — Konu 52: GraphQL (Opsiyonel)
  53. Bölüm 11 — Konu 53: Store Yayınlama Süreci (İmzalama, Listing, Versiyonlama)
  54. Bölüm 11 — Konu 54: App Size, Obfuscation, Flavor Yönetimi
  55. Bölüm 12 — Konu 55: Flutter Web / Desktop
  56. Bölüm 12 — Konu 56: Custom `RenderObject` Yazımı
  57. Bölüm 12 — Konu 57: Engine Mimarisi Derinlemesine (Impeller vs Skia)
  58. Bölüm 12 — Konu 58: Plugin Geliştirme, pub.dev'e Paket Yayınlama
  59. Bölüm 12 — Konu 59: Monorepo Mimarisi (Melos)
  60. Bölüm 12 — Konu 60: Erişilebilirlik (Accessibility) Derinlemesine
  61. Bölüm 12 — Konu 61: Yerelleştirme (Localization / i18n)
Dizinin sayfası →
İçindekiler 14 başlık
  1. Neden Bir "Mimari"ye İhtiyacın Var?
  2. Üç Katman
  3. domain Katmanı Neden Flutter'ı Bilmemeli?
  4. SOLID Prensipleri — Flutter Bağlamında
  5. S — Single Responsibility (Tek Sorumluluk)
  6. O — Open/Closed (Açık/Kapalı)
  7. L — Liskov Substitution (Liskov Yerine Geçme)
  8. I — Interface Segregation (Arayüz Ayrımı)
  9. D — Dependency Inversion (Bağımlılığın Tersine Çevrilmesi)
  10. Hepsi Bir Arada — Küçük Bir Uçtan Uca Örnek
  11. Bu Kadar Katman Her Projede Gerekli mi?
  12. 🎯 Bu Dersten Çıkarılması Gerekenler
  13. 📝 Ödevler
  14. 🎮 Mini Uygulama — Bölüm 7 Checkpoint: Katmanlı Görev Yöneticisi

Bölüm 7'nin son konusuna geldik. Şimdiye kadar Repository Pattern (Konu 32) ve Dependency Injection (Konu 33) ile parçaları öğrendik — bu derste bu parçaları tek bir tutarlı mimari altında birleştireceğiz ve bu mimarinin arkasındaki prensipleri (SOLID) netleştireceğiz.

Neden Bir "Mimari"ye İhtiyacın Var?

Küçük bir uygulamada, tüm kodu tek bir dosyaya (main.dart) yazsan bile çalışır. Ama uygulaman büyüdükçe (10, 50, 200 ekran), şu sorular ortaya çıkar:

  • Bir API çağrısını değiştirmek istediğimde, kaç dosyaya dokunmam gerekiyor?
  • Bir widget testini yazarken, gerçek bir ağ isteği yapmadan test edebiliyor muyum?
  • Yeni bir geliştirici projeye katıldığında, "kullanıcı girişi nerede yapılıyor?" sorusunun cevabını ne kadar hızlı bulabiliyor?

Clean Architecture (Robert C. Martin tarafından popülerleştirilen bir kavram), bu sorulara "kodu, değişme sıklığına ve sorumluluğuna göre katmanlara ayırarak" cevap verir.

Üç Katman

metin
lib/
├── data/            # DIŞ DÜNYA ile konuşan katman
│   ├── models/      # JSON <-> Dart nesnesi dönüşümü (Bölüm 5 Konu 21)
│   ├── datasources/ # API, veritabanı çağrıları (http/dio, sqflite/Hive)
│   └── repositories/ # Repository'lerin GERÇEK implementasyonu
├── domain/          # İŞ KURALLARI — Flutter'dan bile habersiz olmalı
│   ├── entities/    # Saf Dart sınıfları (JSON bilgisi yok)
│   ├── repositories/ # Repository'lerin SOYUT tanımı (interface)
│   └── usecases/    # Tek bir iş kuralını temsil eden sınıflar
└── presentation/    # KULLANICI ARAYÜZÜ
    ├── screens/     # Widget'lar (Scaffold içeren ekranlar)
    ├── widgets/     # Küçük, yeniden kullanılabilir widget'lar
    └── state/       # Riverpod/BLoC/Provider state yönetimi (Bölüm 6)

Akışı bir örnekle takip edelim — "Kullanıcı profilini göster" senaryosu:

  1. presentation/screens/profil_ekrani.dart — ekran açılır, state yönetimine (ProfilNotifier gibi) "profili getir" der.
  2. domain/usecases/profil_getir.dart — bu, tek bir iş kuralını temsil eden bir sınıf; ProfilRepository (soyut) üzerinden veri ister.
  3. domain/repositories/profil_repository.dart — bu sadece bir arayüz (interface), Dart Bölüm 7'de öğrendiğimiz abstract class kavramı. Nasıl veri getirileceğini bilmiyor, sadece "getirilebilir" sözünü veriyor.
  4. data/repositories/profil_repository_impl.dart — arayüzün gerçek implementasyonu; data/datasources üzerinden API'ye gidiyor.
  5. data/datasources/profil_api.dart — asıl HTTP isteğini yapan kod (Bölüm 5 Konu 20'yi hatırla).

domain Katmanı Neden Flutter'ı Bilmemeli?

Bu, Clean Architecture'ın en çarpıcı kuralıdır: domain/ klasöründeki hiçbir dosya import 'package:flutter/...' içermemelidir — sadece saf Dart. Neden?

  • Test edilebilirlik: İş kurallarını (örn. "bir siparişin toplam tutarını hesapla") test etmek için widget ağacı kurmana, WidgetTester kullanmana gerek kalmaz — sıradan bir Dart testi yeterlidir (Dart Bölüm 8'de öğrendiğimiz test paketini hatırla).
  • Platform bağımsızlığı: Aynı iş mantığını, yarın bir Flutter web uygulamasında, bir komut satırı aracında ya da (teoride) tamamen farklı bir arayüz katmanında değiştirmeden kullanabilirsin.
  • Değişim izolasyonu: UI'ı tamamen yeniden tasarlasan bile (yeni widget'lar, yeni state management paketi), iş kuralların dokunulmadan kalır.

SOLID Prensipleri — Flutter Bağlamında

SOLID, nesne yönelimli tasarımın 5 temel prensibinin kısaltmasıdır. Dart Bölüm 7'de OOP'yi öğrendik — şimdi bu prensipleri gerçek Flutter kararlarına bağlayalım.

S — Single Responsibility (Tek Sorumluluk)

"Bir sınıfın değişmesi için tek bir sebep olmalı."

dart
// ❌ Kötü — bu widget hem UI çiziyor hem API çağrısı yapıyor hem veri dönüştürüyor
class ProfilEkrani extends StatefulWidget {
  // ... build() içinde doğrudan http.get() çağrısı, JSON parse etme, hata yönetimi hepsi bir arada
}

// ✅ İyi — sorumluluklar ayrılmış
class ProfilEkrani extends StatelessWidget { /* sadece UI */ }
class ProfilGetirUseCase { /* sadece iş kuralı */ }
class ProfilRepositoryImpl { /* sadece veri erişimi */ }

Bölüm 6 Konu 24'te Provider ile, Bölüm 3 Konu 13'te custom widget'larla zaten kompozisyon yaparak bu prensibi uyguluyorduk — burada sadece isimlendirip bilinçli hale getiriyoruz.

O — Open/Closed (Açık/Kapalı)

"Sınıflar genişletmeye açık, değiştirmeye kapalı olmalı."

dart
abstract class OdemeYontemi {
  Future<bool> ode(double tutar);
}

class KrediKartiOdeme implements OdemeYontemi {
  @override
  Future<bool> ode(double tutar) async { /* ... */ return true; }
}

class HavaleOdeme implements OdemeYontemi {
  @override
  Future<bool> ode(double tutar) async { /* ... */ return true; }
}

Dart Bölüm 7'de öğrendiğimiz implements ile interface kavramını hatırla — yeni bir ödeme yöntemi (KriptoOdeme gibi) eklemek istediğinde, var olan kodu değiştirmene gerek yok, sadece OdemeYontemi'ni implemente eden yeni bir sınıf ekliyorsun. Bu, switch ile her ödeme türünü tek tek kontrol eden bir yapıya göre çok daha sürdürülebilir.

L — Liskov Substitution (Liskov Yerine Geçme)

"Bir alt sınıf, üst sınıfının yerine sorunsuz geçebilmeli."

dart
// ❌ Kötü — MockRepository, gerçek repository'nin davranışını BOZUYOR
class ProfilRepositoryImpl implements ProfilRepository {
  @override
  Future<Profil> getir(String id) async => /* gerçek API çağrısı */;
}

class BozukMockRepository implements ProfilRepository {
  @override
  Future<Profil> getir(String id) async {
    throw UnimplementedError(); // ❌ Sözleşmeyi ihlal ediyor!
  }
}

Bir ProfilRepository sözü (Dart Bölüm 7'de abstract class'ın bir "sözleşme" olduğunu öğrenmiştik), her implementasyonda tutulmalı. Test için yazdığın bir FakeProfilRepository, gerçek implementasyonun yerine sorunsuzca geçebilmeli — bu, testlerin neden repository arayüzü üzerinden yazılabildiğinin temelidir.

I — Interface Segregation (Arayüz Ayrımı)

"Bir sınıf, kullanmadığı metodlara bağımlı olmaya zorlanmamalı."

dart
// ❌ Kötü — dev bir "her şeyi yapan" interface
abstract class KullaniciServisi {
  Future<Kullanici> girisYap(String email, String sifre);
  Future<void> cikisYap();
  Future<void> profilGuncelle(Profil profil);
  Future<void> bildirimGonder(String mesaj); // profil ekranının umurunda bile değil!
}

// ✅ İyi — küçük, odaklı arayüzler
abstract class KimlikDogrulama {
  Future<Kullanici> girisYap(String email, String sifre);
  Future<void> cikisYap();
}

abstract class ProfilYonetimi {
  Future<void> profilGuncelle(Profil profil);
}

Bir sınıf sadece giriş/çıkış işlemleriyle ilgileniyorsa, bildirimGonder gibi alakasız bir metoda bağımlı olmaya zorlanmamalı. Küçük arayüzler, Bölüm 7 Konu 33'te öğrendiğimiz dependency injection ile birleştiğinde, her sınıfın gerçekten ihtiyaç duyduğu bağımlılığı almasını sağlar.

D — Dependency Inversion (Bağımlılığın Tersine Çevrilmesi)

"Üst seviye modüller, alt seviye modüllere değil, soyutlamalara bağımlı olmalı."

Bu, aslında Bölüm 7 Konu 33'te (Dependency Injection) ve bu dersin başında gördüğümüz domain/data ayrımının temel gerekçesidir:

dart
// domain/usecases/profil_getir.dart — SOYUT ProfilRepository'ye bağımlı
class ProfilGetirUseCase {
  final ProfilRepository _repository; // interface, implementasyon DEĞİL
  ProfilGetirUseCase(this._repository);

  Future<Profil> call(String id) => _repository.getir(id);
}

ProfilGetirUseCase, ProfilRepositoryImpl'i (somut sınıfı) hiç bilmiyor — sadece ProfilRepository (soyut) arayüzünü biliyor. Hangi gerçek implementasyonun kullanılacağına, Bölüm 7 Konu 33'te öğrendiğimiz get_it/injectable gibi bir DI aracı, uygulamanın başlangıcında karar verir:

dart
void kurulumYap() {
  getIt.registerLazySingleton<ProfilRepository>(
    () => ProfilRepositoryImpl(getIt<ProfilApi>()), // somut sınıf BURADA "bağlanıyor"
  );
}

Bu sayede, testte ProfilRepositoryImpl yerine sahte bir FakeProfilRepository kaydedebilirsin — ProfilGetirUseCase'in kodunu hiç değiştirmeden.

Hepsi Bir Arada — Küçük Bir Uçtan Uca Örnek

dart
// domain/entities/profil.dart — saf Dart, Flutter'dan habersiz
class Profil {
  final String id;
  final String isim;
  Profil({required this.id, required this.isim});
}

// domain/repositories/profil_repository.dart — soyut sözleşme
abstract class ProfilRepository {
  Future<Profil> getir(String id);
}

// domain/usecases/profil_getir_usecase.dart — tek bir iş kuralı
class ProfilGetirUseCase {
  final ProfilRepository _repository;
  ProfilGetirUseCase(this._repository);
  Future<Profil> call(String id) => _repository.getir(id);
}

// data/repositories/profil_repository_impl.dart — gerçek implementasyon
class ProfilRepositoryImpl implements ProfilRepository {
  final Dio _dio; // Bölüm 5 Konu 20
  ProfilRepositoryImpl(this._dio);

  @override
  Future<Profil> getir(String id) async {
    final yanit = await _dio.get('/profil/$id');
    return Profil(id: yanit.data['id'], isim: yanit.data['isim']); // Bölüm 5 Konu 21
  }
}

// presentation/state/profil_notifier.dart — Riverpod ile (Bölüm 6 Konu 25)
class ProfilNotifier extends AsyncNotifier<Profil?> {
  @override
  Future<Profil?> build() => null;

  Future<void> getir(String id) async {
    state = const AsyncLoading();
    final useCase = ref.read(profilGetirUseCaseProvider); // DI ile enjekte edilmiş
    state = await AsyncValue.guard(() => useCase(id));
  }
}

Bu akışta, presentation katmanı sadece ProfilGetirUseCase'i çağırıyor; domain katmanı Flutter'dan tamamen habersiz; data katmanı ise dış dünyayla (Dio) konuşan tek yer. Her katman, sadece kendi işine odaklanıyor — tam da Single Responsibility'nin istediği gibi.

Bu Kadar Katman Her Projede Gerekli mi?

Dürüst bir uyarı: Küçük bir uygulamada (5-10 ekranlık bir MVP, bir hackathon projesi), bu kadar katmanlandırma gereksiz karmaşıklık yaratabilir — "aşırı mühendislik" (over-engineering) riski gerçektir. Pratik yaklaşım:

  • Küçük/hızlı proje: presentation + tek bir services/repositories klasörü yeterli olabilir, domain katmanını tamamen atlayabilirsin.
  • Büyüyen/uzun soluklu proje: Test yazmaya başladığında, birden fazla geliştirici katıldığında, ya da bir API sağlayıcısını değiştirmen gerektiğinde, katmanlı mimarinin faydası hızla kendini gösterir.

Bölüm 7 boyunca öğrendiğin repository pattern, DI ve şimdi bu mimari, hepsi birbirine bağlı araçlar — hangisini ne zaman kullanacağına, projenin büyüklüğüne göre sen karar vereceksin.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Clean Architecture, kodu presentation (UI), domain (iş kuralları) ve data (dış dünya erişimi) olmak üzere üç katmana ayırır.
  • domain katmanı Flutter'dan habersiz olmalı — bu, test edilebilirlik ve platform bağımsızlığı sağlar.
  • SOLID: Single Responsibility (her sınıf tek iş yapar), Open/Closed (yeni davranış = yeni sınıf, eski kodu değiştirme), Liskov Substitution (alt sınıflar üst sınıfın sözleşmesini bozmaz), Interface Segregation (küçük, odaklı arayüzler), Dependency Inversion (somut sınıflara değil, soyutlamalara bağımlı ol).
  • Dependency Inversion, Bölüm 7 Konu 33'teki DI araçlarının (get_it/injectable) neden var olduğunun temel gerekçesidir.
  • Bu mimari her projede gerekli değildir — küçük projelerde basit tutmak, büyüyen projelerde katmanlamak mantıklıdır.

📝 Ödevler

  • [ ] Kendi cümlelerinle, "neden domain katmanı Flutter'ı import etmemeli" sorusunu, test edilebilirlik açısından açıkla.
  • [ ] SOLID prensiplerinden birini (Open/Closed önerilir) ihlal eden bir kod örneği yaz, sonra aynı kodu prensibe uygun hale getir.
  • [ ] Basit bir Entity + soyut Repository + UseCase üçlüsü tasarla (örneğin bir "Not Alma" uygulaması için Not, NotRepository, NotEkleUseCase).
  • [ ] Bir data/repositories implementasyonu yaz (gerçek bir API çağrısı yapmasına gerek yok, sabit/mock veri döndürebilir), get_it ile kaydet.

🎮 Mini Uygulama — Bölüm 7 Checkpoint: Katmanlı Görev Yöneticisi

Bölüm 7'yi (Navigasyon + Mimari) kapatan bir uçtan uca mini proje:

Gereksinimler:

  • domain: Gorev entity'si (id, başlık, tamamlandı mı), soyut GorevRepository (listele/ekle/tamamla metodları), en az 2 use case (GorevleriGetirUseCase, GorevEkleUseCase).
  • data: GorevRepository'nin bellek-içi (in-memory) bir implementasyonu — gerçek bir API'ye bağlanmana gerek yok, bir List<Gorev> yeterli.
  • presentation: go_router (Bölüm 7 Konu 30) ile en az 2 rota (görev listesi, görev ekleme formu); state yönetimi için Riverpod veya BLoC (Bölüm 6) kullan.
  • DI: get_it ile GorevRepository'yi ve use case'leri kaydet (Bölüm 7 Konu 33).
  • Bonus: FakeGorevRepository adında ikinci bir implementasyon yaz (sabit, önceden doldurulmuş bir liste döndürsün) ve testte/geliştirmede gerçek implementasyon yerine bunu kaydet — Dependency Inversion'ın pratikteki faydasını birebir gözlemlemiş olursun.

Sıradaki konu: Bölüm 8 — Konu 35: Implicit Animasyonlar (AnimatedContainer, AnimatedOpacity ve benzerleri)