Flutter'da Clean Architecture: Ölçeklenebilir Uygulama Geliştirme
# Flutter'da Clean Architecture: Ölçeklenebilir Uygulamalar Geliştirmek
Clean Architecture sadece bir klasör yapısı değil, bir düşünce biçimidir. İş mantığınızı UI frameworklerinden, veritabanlarından ve üçüncü parti servislerden bağımsız tutmanızı sağlar. Birkaç ekranın ötesine geçen Flutter projelerinde bu yaklaşımı benimsemek, haftalar içinde kendini amorti eder.
Neden Clean Architecture?
Her Flutter geliştiricisi o anı yaşamıştır: "küçük bir değişiklik" düzinelerce dosyayı etkiler. Yeni bir API alanı bir widget'ı bozar. State management değişikliği uygulamanın yarısını yeniden yazmayı gerektirir. Clean Architecture, katmanlar arasına kesin sınırlar koyarak bu durumları önler.
Üç Temel Katman
1. Domain Katmanı (En İçteki)
Uygulamanızın kalbidir. Entity'ler, use case'ler ve repository kontratları burada bulunur. Flutter'a, paketlere veya herhangi bir harici kütüphaneye sıfır bağımlılığı vardır. Yalnızca saf Dart.
2. Data Katmanı
Bu katman, domain katmanındaki repository kontratlarını uygular. API'lerden veri çekmeyi, yerel veritabanlarından okumayı ve ham yanıtları domain entity'lerine dönüştürmeyi bilir.
3. Presentation Katmanı (En Dıştaki)
Widget'lar, sayfalar, state management (BLoC, Riverpod, Cubit vb.) ve Flutter'a özgü her şey burada yaşar. Bu katman domain katmanına bağımlıdır ancak doğrudan data katmanına asla bağımlı değildir.
Proje Klasör Yapısı
Prodüksiyon Flutter projelerimde kullandığım klasör yapısı aşağıdadır. Her özellik kendi kendine yeten bir dizine sahiptir:
lib/
core/
error/
exceptions.dart
failures.dart
network/
network_info.dart
usecases/
usecase.dart
di/
injection_container.dart
features/
authentication/
data/
datasources/
auth_remote_data_source.dart
auth_local_data_source.dart
models/
user_model.dart
repositories/
auth_repository_impl.dart
domain/
entities/
user.dart
repositories/
auth_repository.dart
usecases/
login_user.dart
register_user.dart
get_current_user.dart
presentation/
bloc/
auth_bloc.dart
auth_event.dart
auth_state.dart
pages/
login_page.dart
register_page.dart
widgets/
auth_form.dart
products/
data/
...
domain/
...
presentation/
...`core/` dizini paylaşılan yardımcı araçları barındırır. Her `features/` alt dizini üç katmanlı yapıyı yansıtır. Bu sayede ilgili kodlar bir arada kalır ve bir özelliği tamamen silmek ya da ayırmak kolaylaşır.
Her Katman için Kod Örnekleri
Entity (Domain Katmanı)
Entity'ler sade Dart sınıflarıdır. Anotasyon yok, framework bağımlılığı yok.
class User {
final String id;
final String email;
final String displayName;
final DateTime createdAt;
const User({
required this.id,
required this.email,
required this.displayName,
required this.createdAt,
});
bool get isProfileComplete => displayName.isNotEmpty;
}Repository Kontratı (Domain Katmanı)
Hangi operasyonların var olduğunu tanımlayan, nasıl çalıştıklarını belirtmeyen abstract bir sınıf. Dönüş tipi olarak dartz paketindeki `Either` kullanılır; bu sayede hatalar açıkça ele alınır.
import class="code-string">'package:dartz/dartz.dart';
abstract class AuthRepository {
Future<Either<Failure, User>> login(String email, String password);
Future<Either<Failure, User>> register(String email, String password, String name);
Future<Either<Failure, User>> getCurrentUser();
Future<Either<Failure, void>> logout();
}Use Case (Domain Katmanı)
Her use case tam olarak bir iş yapar. Bu, test yazmayı basitleştirir ve iş kurallarını keşfedilebilir kılar.
class LoginUser {
final AuthRepository repository;
LoginUser(this.repository);
Future<Either<Failure, User>> call(LoginParams params) {
return repository.login(params.email, params.password);
}
}
class LoginParams {
final String email;
final String password;
const LoginParams({required this.email, required this.password});
}Model (Data Katmanı)
Modeller, entity'lere eşlenir ve JSON serileştirme işlemlerini yönetir. Ham API verisi ile temiz domain nesneleri arasındaki köprüdür.
class UserModel {
final String id;
final String email;
final String displayName;
final DateTime createdAt;
const UserModel({
required this.id,
required this.email,
required this.displayName,
required this.createdAt,
});
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json[class="code-string">'id'] as String,
email: json[class="code-string">'email'] as String,
displayName: json[class="code-string">'display_name'] as String,
createdAt: DateTime.parse(json[class="code-string">'created_at'] as String),
);
}
Map<String, dynamic> toJson() {
return {
class="code-string">'id': id,
class="code-string">'email': email,
class="code-string">'display_name': displayName,
class="code-string">'created_at': createdAt.toIso8601String(),
};
}
User toEntity() {
return User(
id: id,
email: email,
displayName: displayName,
createdAt: createdAt,
);
}
}Data Source (Data Katmanı)
Data source'lar ham I/O işlemlerini yönetir. Remote source'lar API'lerle, local source'lar veritabanları ve cache ile konuşur.
abstract class AuthRemoteDataSource {
Future<UserModel> login(String email, String password);
Future<UserModel> register(String email, String password, String name);
}
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
final http.Client client;
AuthRemoteDataSourceImpl({required this.client});
@override
Future<UserModel> login(String email, String password) async {
final response = await client.post(
Uri.parse(class="code-string">'$baseUrl/auth/login'),
body: jsonEncode({class="code-string">'email': email, class="code-string">'password': password}),
headers: {class="code-string">'Content-Type': class="code-string">'application/json'},
);
if (response.statusCode == class="code-number">200) {
return UserModel.fromJson(jsonDecode(response.body));
} else {
throw ServerException(message: class="code-string">'Giris basarisiz');
}
}
@override
Future<UserModel> register(String email, String password, String name) async {
final response = await client.post(
Uri.parse(class="code-string">'$baseUrl/auth/register'),
body: jsonEncode({class="code-string">'email': email, class="code-string">'password': password, class="code-string">'name': name}),
headers: {class="code-string">'Content-Type': class="code-string">'application/json'},
);
if (response.statusCode == class="code-number">201) {
return UserModel.fromJson(jsonDecode(response.body));
} else {
throw ServerException(message: class="code-string">'Kayit basarisiz');
}
}
}Repository Implementasyonu (Data Katmanı)
Repository implementasyonu, data source'lardan gelen exception'ları yakalar ve bunları tiplendirilmiş failure'lara dönüştürür. Cacheleme stratejileri için remote ve local source'lar arasında koordinasyon da burada sağlanır.
class AuthRepositoryImpl implements AuthRepository {
final AuthRemoteDataSource remoteDataSource;
final AuthLocalDataSource localDataSource;
final NetworkInfo networkInfo;
AuthRepositoryImpl({
required this.remoteDataSource,
required this.localDataSource,
required this.networkInfo,
});
@override
Future<Either<Failure, User>> login(String email, String password) async {
if (await networkInfo.isConnected) {
try {
final userModel = await remoteDataSource.login(email, password);
await localDataSource.cacheUser(userModel);
return Right(userModel.toEntity());
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
}
} else {
return const Left(NetworkFailure(class="code-string">'Internet baglantisi yok'));
}
}
}BLoC (Presentation Katmanı)
Presentation katmanı yalnızca use case'leri kullanır. Data source'lara veya repository'lere doğrudan erişmez.
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final LoginUser loginUser;
final GetCurrentUser getCurrentUser;
AuthBloc({
required this.loginUser,
required this.getCurrentUser,
}) : super(AuthInitial()) {
on<LoginRequested>(_onLoginRequested);
}
Future<void> _onLoginRequested(
LoginRequested event,
Emitter<AuthState> emit,
) async {
emit(AuthLoading());
final result = await loginUser(
LoginParams(email: event.email, password: event.password),
);
result.fold(
(failure) => emit(AuthError(failure.message)),
(user) => emit(AuthAuthenticated(user)),
);
}
}Dependency Injection Kurulumu
Dependency injection, katmanları birbirine sıkı bağımlılık oluşturmadan bağlayan yapıştırıcıdır. Ben genellikle `get_it` ve `injectable` kullanıyorum, ancak aşağıdaki manuel kurulum kavramı net biçimde gösteriyor.
import class="code-string">'package:get_it/get_it.dart';
final sl = GetIt.instance;
Future<void> initDependencies() async {
class=class="code-string">"code-comment">// Harici bagimliliklar
sl.registerLazySingleton(() => http.Client());
sl.registerLazySingleton<NetworkInfo>(() => NetworkInfoImpl());
class=class="code-string">"code-comment">// Data Sourceclass="code-string">'lar
sl.registerLazySingleton<AuthRemoteDataSource>(
() => AuthRemoteDataSourceImpl(client: sl()),
);
sl.registerLazySingleton<AuthLocalDataSource>(
() => AuthLocalDataSourceImpl(sharedPreferences: sl()),
);
class=class="code-string">"code-comment">// Repository'ler
sl.registerLazySingleton<AuthRepository>(
() => AuthRepositoryImpl(
remoteDataSource: sl(),
localDataSource: sl(),
networkInfo: sl(),
),
);
class=class="code-string">"code-comment">// Use Caseclass="code-string">'ler
sl.registerLazySingleton(() => LoginUser(sl()));
sl.registerLazySingleton(() => RegisterUser(sl()));
sl.registerLazySingleton(() => GetCurrentUser(sl()));
class=class="code-string">"code-comment">// BLoC'lar
sl.registerFactory(
() => AuthBloc(loginUser: sl(), getCurrentUser: sl()),
);
}Kayıt sırası önemlidir: önce harici bağımlılıklar, sonra data source'lar, repository'ler, use case'ler ve en son BLoC'lar. BLoC'lar factory olarak kaydedilir; böylece her ekran taze bir instance alır.
Clean Architecture Ne Zaman Gereksiz Olur?
Her proje Clean Architecture'a ihtiyaç duymaz. Dürüst bir değerlendirme yapalım.
Atlayabileceğiniz durumlar:
Benimsemeniz gereken durumlar:
Prodüksiyon Flutter uygulamalarındaki deneyimlerime göre, başa baş noktası genellikle üçüncü veya dördüncü özelliktir. Ondan önce boilerplate kod fazla gelir. Ondan sonra ise her yeni özellik eklediğinizde geri dönüş almaya başlarsınız, çünkü kalıplar zaten oturmuştur ve yeni kod neredeyse kendini yazar.
Sık Yapılan Mimari Hatalar
1. Framework Bağımlılıklarının Domain Katmanına Sızması
Domain katmanınız saf Dart olmalıdır. Bir entity veya use case'de `package:flutter/material.dart` veya Flutter'a özgü herhangi bir paketi import ettiğiniz an, mimariyi kırmış olursunuz. Bu, `json_serializable` gibi paketlerin anotasyonlarını da kapsar -- bunlar data katmanındaki modellere aittir, entity'lere değil.
2. Use Case Katmanını Atlamak
BLoC'lardan doğrudan repository çağırmak cazip gelir. Bu, birden fazla repository çağrısını birleştirmeniz, doğrulama eklemeniz veya iş kurallarını uygulamanız gerekene kadar işe yarar. Use case'ler yazmak ucuzdur ve mantığınızı presentation koduna yaymaktan kurtarır.
3. Şişman Repository'ler
Cacheleme, yeniden deneme, loglama, analitik ve hata eşlemeyi aynı anda yöneten bir repository çok fazla iş yapıyordur. Repository'leri veri koordinasyonuna odaklı tutun. Kesişen kaygıları decorator'lara veya middleware'lere çıkarın.
4. Her Şeyi Tek BLoC'a Sığdırmak
Özellik başına tek devasa BLoC oluşturmak, state management kabusuna yol açar. Ekran veya sorumluluk bazında bölün. Giriş, kayıt, şifre sıfırlama ve profil güncellemeyi aynı anda yöneten bir `AuthBloc`, aslında trençkot giymiş dört BLoC'tur.
5. Bağımlılık Kuralını Göz Ardı Etmek
Bağımlılıklar yalnızca içe doğru işaret etmelidir. Presentation, domain'e bağımlıdır. Data, domain'e bağımlıdır. Domain hiçbir şeye bağımlı değildir. Entity'niz data katmanından import yaptığında, mimari çöker. Bu en önemli kuraldır.
Pratik Geçiş Rehberi
Mevcut bir Flutter uygulamanız varsa ve Clean Architecture'ı benimsemek istiyorsanız, birkaç projede başarıyla uyguladığım adım adım yaklaşım aşağıdadır.
Adım 1: Mevcut Kodunuzu Denetleyin
Tüm özelliklerinizi listeleyin. En fazla iş mantığına ve en çok hata içerenleri belirleyin. Önemli ama kritik olmayan bir özellikle başlayın -- kalıpları yüksek riskli baskı olmadan öğrenmek istersiniz.
Adım 2: Klasör Yapısını Oluşturun
`core/` ve `features/` dizinlerini kurun. Seçtiğiniz özellik için `data/`, `domain/` ve `presentation/` alt dizinlerini oluşturun. Henüz hiçbir kodu taşımayın.
Adım 3: Entity'leri Çıkarın
Özelliğinizin kullandığı veri sınıflarını bulun. Domain katmanında temiz entity versiyonlarını oluşturun. Serileştirme mantığını, anotasyonları ve framework importlarını sıyırın. Serileştirme kaygılarını data katmanındaki model sınıflarına taşıyın.
Adım 4: Repository Kontratlarını Tanımlayın
Özelliğinizin veriyi nasıl getirip sakladığına bakın. Domain katmanında bu operasyonları tanımlayan abstract bir repository sınıfı yazın. Hata yönetimini açık hale getirmek için `Either
Adım 5: Use Case'leri Yazın
Özelliğinizin gerçekleştirdiği her eylem için bir use case sınıfı oluşturun. Use case sadece repository'ye delege etse bile, orada olması kalıbı yerleştirir ve ileride iş kuralları eklemek için hazır bir yer sağlar.
Adım 6: Repository'yi İmplemente Edin
Data katmanında somut repository'yi oluşturun. Mevcut API çağrılarınızı ve veritabanı işlemlerinizi data source sınıflarına taşıyın. Repository bunlar arasında koordinasyonu sağlar.
Adım 7: Dependency Injection'ı Bağlayın
Yeni sınıflarınızı DI konteynerine kaydedin. Widget'lardaki doğrudan oluşturmaları inject edilen bağımlılıklarla değiştirin.
Adım 8: Presentation Katmanını Refactor Edin
Widget'larınızı ve state management'ınızı, doğrudan API çağırmak yerine yeni use case'leri kullanacak şekilde güncelleyin. Arayüz benzer kaldığı için bu genellikle en küçük değişikliktir.
Adım 9: Testleri Yazın
Her katman izole olduğuna göre birim testleri yazın: use case'ler için (saf mantık), repository implementasyonları için (data source'ları mockla), BLoC'lar için (use case'leri mockla). Test yazmanın ne kadar kolaylaştığını hemen fark edeceksiniz.
Adım 10: Sonraki Özellik için Tekrarlayın
Öğrendiklerinizi alın ve sonraki özelliğe uygulayın. Kalıpları içselleştirdikçe her geçiş daha hızlı olur.
Prodüksiyon Flutter uygulamalarındaki deneyimlerime göre, orta ölçekli bir uygulama için tam geçiş, normal özellik çalışmasıyla paralel yapıldığında yaklaşık 2-4 hafta sürer. Anahtar nokta, refactor ederken asla değer üretmeyi durdurmamaktır. Taşınan her özellik çalışır durumda yayınlanmalıdır.
Sonuç
Clean Architecture bir yatırımdır. Başlangıç kurulumu zaman alır ve boilerplate kod ilk başta ağır gelebilir. Ancak birkaç aydan uzun yaşayacak herhangi bir Flutter projesi için getiriler bileşik olarak artar. Test edilebilirlik yükselir, yeni geliştiricileri projeye dahil etmek hızlanır ve kod değiştirme korkusu ortadan kalkar.
Tek bir özellikle başlayın, değerini kanıtlayın ve oradan genişletin. Mimari ekibinize hizmet etmelidir, tersi değil.
İsterseniz projenize özel adım adım mimari geçiş planı oluşturabiliriz.
İlgili Makaleler
Flutter State Management: Riverpod, Provider ve Bloc Karşılaştırması
Flutter'da state management yaklaşımlarını karşılaştırın. Riverpod, Provider ve Bloc için kullanım senaryolarını ve karar kriterlerini netleştirin.
Dart Best Practices: Temiz ve Sürdürülebilir Kod Rehberi
Dart projelerinde okunabilir, test edilebilir ve sürdürülebilir kod yazmak için en etkili pratikleri öğrenin.
Flutter Testing Rehberi: Unit, Widget ve Integration Test
Flutter projelerinde test stratejisi kurun. Unit, widget ve integration test katmanlarını doğru sorumluluklarla yapılandırın.
Flutter Projeniz mi Var?
iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.
İletişime Geç