Bölüm 7 — Konu 34: Clean Architecture Uyarlaması, SOLID Prensipleri
Dizi · 34/61 Flutter Türkçe Tutorial
- Bölüm 1 — Konu 1: Flutter Nedir, Mimarisi (Widget → Element → RenderObject, Skia/Impeller)
- Bölüm 1 — Konu 2: Ortam Kurulumu (Flutter SDK, Android Studio/VS Code, Emulator/Simulator, DevTools'a İlk Bakış)
- Bölüm 1 — Konu 3: İlk Proje Yapısı (`pubspec.yaml`, `lib/`, Klasör Mimarisi, `flutter create` Anatomisi)
- Bölüm 1 — Konu 4: Hot Reload / Hot Restart Ne Yapıyor, Neden Önemli
- Bölüm 2 — Konu 5: "Her Şey Widget'tır" Felsefesi, Widget Ağacı
- Bölüm 2 — Konu 6: StatelessWidget vs StatefulWidget + `setState()` Derinlemesine (Rebuild Mekanizması)
- Bölüm 2 — Konu 7: Temel Layout — Container, Row, Column, Stack, Padding, Align, Center
- Bölüm 2 — Konu 8: Constraint Sistemi — "Constraints Go Down, Sizes Go Up, Parent Sets Position"
- Bölüm 2 — Konu 9: `Expanded`, `Flexible`, `Spacer`, Intrinsic Widget'lar
- Bölüm 3 — Konu 10: MaterialApp/CupertinoApp, Scaffold, AppBar
- Bölüm 3 — Konu 11: `ListView`, `GridView`, `SingleChildScrollView` (+ Builder Pattern)
- Bölüm 3 — Konu 12: Text, TextStyle, Icon, Image, Asset Yönetimi
- Bölüm 3 — Konu 13: Custom Widget Yazma Prensipleri (Composition Over Inheritance)
- Bölüm 3 — Konu 14: Navigator 1.0 — Push/Pop, Named Routes
- Bölüm 4 — Konu 15: Form, TextField/TextFormField, GlobalKey<FormState>, Validasyon
- Bölüm 4 — Konu 16: Theme Sistemi (ThemeData, ColorScheme)
- Bölüm 4 — Konu 17: Responsive & Adaptive Tasarım (MediaQuery, LayoutBuilder, OrientationBuilder, Breakpoint Stratejileri)
- Bölüm 5 — Konu 18: Future/Async-Await Flutter Bağlamında, FutureBuilder
- Bölüm 5 — Konu 19: `Stream`, `StreamBuilder`
- Bölüm 5 — Konu 20: HTTP İstekleri (`http` / `dio` Paketleri)
- Bölüm 5 — Konu 21: JSON Serialization (Manuel → `json_serializable`/`freezed`)
- Bölüm 5 — Konu 22: Local Storage — `shared_preferences` → `Hive` → `sqflite`/Drift
- Bölüm 6 — Konu 23: InheritedWidget ve InheritedModel — "Neden setState Yetmiyor" Sorusunun Cevabı
- Bölüm 6 — Konu 24: Provider Paketi
- Bölüm 6 — Konu 25: Riverpod (Modern Yaklaşım, Provider'ın Halefi)
- Bölüm 6 — Konu 26: BLoC/Cubit Pattern (`flutter_bloc`)
- Bölüm 6 — Konu 27: GetX (Tartışmalı Ama Yaygın)
- Bölüm 6 — Konu 28: Karşılaştırma — Hangi Projede Hangisi?
- Bölüm 7 — Konu 29: Navigator 2.0 (Router, RouteInformationParser, RouterDelegate)
- Bölüm 7 — Konu 30: `go_router` Paketi (Pratik ve Modern Çözüm)
- Bölüm 7 — Konu 31: Deep Linking
- Bölüm 7 — Konu 32: Repository Pattern, Katmanlı Mimari (Data/Domain/Presentation)
- Bölüm 7 — Konu 33: Dependency Injection (`get_it`, `injectable`)
- Bölüm 7 — Konu 34: Clean Architecture Uyarlaması, SOLID Prensipleri
- Bölüm 8 — Konu 35: Implicit Animasyonlar
- Bölüm 8 — Konu 36: Explicit Animasyonlar (`AnimationController`, `Tween`, `Curve`)
- Bölüm 8 — Konu 37: Hero Animasyonları
- Bölüm 8 — Konu 38: `CustomPainter` / `Canvas`
- Bölüm 8 — Konu 39: Rive / Lottie Entegrasyonu
- Bölüm 9 — Konu 40: Platform Channels
- Bölüm 9 — Konu 41: Permission Yönetimi (`permission_handler`)
- Bölüm 9 — Konu 42: Kamera, Konum, Sensörler
- Bölüm 9 — Konu 43: Push Notification (Firebase Cloud Messaging)
- Bölüm 9 — Konu 44: Android/iOS Build Sistemleri
- Bölüm 10 — Konu 45: Test Yazımı (Unit, Widget, Integration, Golden)
- Bölüm 10 — Konu 46: CI/CD
- Bölüm 10 — Konu 47: Rebuild Optimizasyonu (`const`, `key` Kullanımı)
- Bölüm 10 — Konu 48: DevTools Profiling
- Bölüm 10 — Konu 49: Lazy Loading, Pagination, Büyük Liste Performansı
- Bölüm 11 — Konu 50: Firebase Ekosistemi (Auth, Firestore, Storage, Functions)
- Bölüm 11 — Konu 51: Supabase Alternatifi
- Bölüm 11 — Konu 52: GraphQL (Opsiyonel)
- Bölüm 11 — Konu 53: Store Yayınlama Süreci (İmzalama, Listing, Versiyonlama)
- Bölüm 11 — Konu 54: App Size, Obfuscation, Flavor Yönetimi
- Bölüm 12 — Konu 55: Flutter Web / Desktop
- Bölüm 12 — Konu 56: Custom `RenderObject` Yazımı
- Bölüm 12 — Konu 57: Engine Mimarisi Derinlemesine (Impeller vs Skia)
- Bölüm 12 — Konu 58: Plugin Geliştirme, pub.dev'e Paket Yayınlama
- Bölüm 12 — Konu 59: Monorepo Mimarisi (Melos)
- Bölüm 12 — Konu 60: Erişilebilirlik (Accessibility) Derinlemesine
- Bölüm 12 — Konu 61: Yerelleştirme (Localization / i18n)
İçindekiler 14 başlık
- Neden Bir "Mimari"ye İhtiyacın Var?
- Üç Katman
- domain Katmanı Neden Flutter'ı Bilmemeli?
- SOLID Prensipleri — Flutter Bağlamında
- S — Single Responsibility (Tek Sorumluluk)
- O — Open/Closed (Açık/Kapalı)
- L — Liskov Substitution (Liskov Yerine Geçme)
- I — Interface Segregation (Arayüz Ayrımı)
- D — Dependency Inversion (Bağımlılığın Tersine Çevrilmesi)
- Hepsi Bir Arada — Küçük Bir Uçtan Uca Örnek
- Bu Kadar Katman Her Projede Gerekli mi?
- 🎯 Bu Dersten Çıkarılması Gerekenler
- 📝 Ödevler
- 🎮 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
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:
presentation/screens/profil_ekrani.dart— ekran açılır, state yönetimine (ProfilNotifiergibi) "profili getir" der.domain/usecases/profil_getir.dart— bu, tek bir iş kuralını temsil eden bir sınıf;ProfilRepository(soyut) üzerinden veri ister.domain/repositories/profil_repository.dart— bu sadece bir arayüz (interface), Dart Bölüm 7'de öğrendiğimizabstract classkavramı. Nasıl veri getirileceğini bilmiyor, sadece "getirilebilir" sözünü veriyor.data/repositories/profil_repository_impl.dart— arayüzün gerçek implementasyonu;data/datasourcesüzerinden API'ye gidiyor.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,
WidgetTesterkullanmana gerek kalmaz — sıradan bir Dart testi yeterlidir (Dart Bölüm 8'de öğrendiğimiztestpaketini 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ı."
// ❌ 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ı."
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."
// ❌ 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ı."
// ❌ 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:
// 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:
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
// 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 birservices/repositoriesklasörü yeterli olabilir,domainkatmanı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ı) vedata(dış dünya erişimi) olmak üzere üç katmana ayırır. domainkatmanı 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
domainkatmanı 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+ soyutRepository+UseCaseüçlüsü tasarla (örneğin bir "Not Alma" uygulaması içinNot,NotRepository,NotEkleUseCase). - [ ] Bir
data/repositoriesimplementasyonu yaz (gerçek bir API çağrısı yapmasına gerek yok, sabit/mock veri döndürebilir),get_itile 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:Goreventity'si (id, başlık, tamamlandı mı), soyutGorevRepository(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, birList<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_itileGorevRepository'yi ve use case'leri kaydet (Bölüm 7 Konu 33). - Bonus:
FakeGorevRepositoryadı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)