Bir Flutter projesi buyudukce, dogru bir mimari olmadan kod tabani karmasik ve yonetilemez hale gelir. Feature eklemeye calisirsin ama her degisiklik baska bir yeri bozar. Testler yazilmamistir cunku kodlar birbirine bagimlidir. Clean Architecture, bu kaosu duzenli ve test edilebilir bir yapiya donusturur.
Bu rehberde Robert C. Martin'in (Uncle Bob) Clean Architecture prensiplerini Flutter dnyasina uyarlayacagiz. Gercek dunya ornekleri ve production-ready pattern'ler ile katmanli mimarinin gucunu kesfedeceksin.
Not: Bu rehberdeki mimari, bircok buyuk olcekli Flutter projesinde (100K+ kullanici) basariyla kullanilmaktadir.
Icindekiler
- Clean Architecture Nedir?
- Katmanlar ve Sorumluluklar
- Domain Katmani
- Data Katmani
- Presentation Katmani
- Dependency Injection
- Error Handling Stratejisi
- Proje Yapisi
- Gercek Dunya Ornegi
- Test Stratejileri
- Best Practices
- Sonuc ve Oneriler
1. Clean Architecture Nedir?
Clean Architecture, yazilim sistemlerini bagimsiz katmanlara ayiran bir mimari yaklasimdir. Temel prensibi: ic katmanlar dis katmanlardan habersiz olmalidir.
Katman Karsilastirmasi
Katman | Sorumluluk | Bagimlilik | Degisim Sikligi |
|---|---|---|---|
**Domain** | Is kurallari, entity'ler | Hicbir sey | Nadir |
**Data** | Veri erisimi, API, cache | Domain | Orta |
**Presentation** | UI, state management | Domain | Sik |
**Core** | Ortak utility'ler | Hicbir sey | Nadir |
SOLID Prensipleri
- S: ingle Responsibility: Her sinifin tek bir sorumlulugu
- O: pen/Closed: Genislemeye acik, degisiklige kapali
- L: iskov Substitution: Alt siniflar ust sinifin yerine gecebilmeli
- I: nterface Segregation: Kucuk, ozel arayuzler
- D: ependency Inversion: Soyutlamalara bagimli ol, somutlara degil
2. Katmanlar ve Sorumluluklar
swift
1lib/2 core/3 error/4 exceptions.dart5 failures.dart6 network/7 network_info.dart8 usecases/9 usecase.dart10 utils/11 input_converter.dart12 features/13 auth/14 domain/15 entities/16 user.dart17 repositories/18 auth_repository.dart19 usecases/20 login.dart21 register.dart22 logout.dart23 data/24 models/25 user_model.dart26 datasources/27 auth_remote_datasource.dart28 auth_local_datasource.dart29 repositories/30 auth_repository_impl.dart31 presentation/32 providers/33 auth_provider.dart34 pages/35 login_page.dart36 register_page.dart37 widgets/38 login_form.dart3. Domain Katmani
Domain katmani, uygulamanin kalbidir. Hicbir dis bagimliligi yoktur — saf Dart kodudur.
Entity
dart
1// domain/entities/user.dart2class User {3 final String id;4 final String name;5 final String email;6 final DateTime createdAt;7 final UserRole role;8 9 const User({10 required this.id,11 required this.name,12 required this.email,13 required this.createdAt,14 this.role = UserRole.user,15 });16}17 18enum UserRole { user, admin, moderator }Repository Interface (Abstraction)
dart
1// domain/repositories/auth_repository.dart2import 'package:dartz/dartz.dart';3 4abstract class AuthRepository {5 Future> login(String email, String password); 6 Future> register(String name, String email, String password); 7 Future> logout(); 8 Future> getCurrentUser(); 9 Stream get authStateChanges; 10}Use Case
dart
1// core/usecases/usecase.dart2import 'package:dartz/dartz.dart';3 4abstract class UseCase { 5 Future> call(Params params); 6}7 8class NoParams {9 const NoParams();10}11 12// domain/usecases/login.dart13class LoginUseCase implements UseCase { 14 final AuthRepository repository;15 16 LoginUseCase(this.repository);17 18 @override19 Future> call(LoginParams params) { 20 return repository.login(params.email, params.password);21 }22}23 24class LoginParams {25 final String email;26 final String password;27 28 const LoginParams({required this.email, required this.password});29}Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
4. Data Katmani
Data katmani, domain katmanindaki soyutlamalari somut hale getirir:
Model (Entity'nin JSON donusumu)
dart
1// data/models/user_model.dart2class UserModel extends User {3 const UserModel({4 required super.id,5 required super.name,6 required super.email,7 required super.createdAt,8 super.role,9 });10 11 factory UserModel.fromJson(Map json) { 12 return UserModel(13 id: json['id'] as String,14 name: json['name'] as String,15 email: json['email'] as String,16 createdAt: DateTime.parse(json['created_at'] as String),17 role: UserRole.values.firstWhere(18 (r) => r.name == json['role'],19 orElse: () => UserRole.user,20 ),21 );22 }23 24 Map toJson() { 25 return {26 'id': id,27 'name': name,28 'email': email,29 'created_at': createdAt.toIso8601String(),30 'role': role.name,31 };32 }33}DataSource
dart
1// data/datasources/auth_remote_datasource.dart2abstract class AuthRemoteDataSource {3 Future login(String email, String password); 4 Future register(String name, String email, String password); 5 Future logout(); 6}7 8class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {9 final HttpClient client;10 11 AuthRemoteDataSourceImpl({required this.client});12 13 @override14 Future login(String email, String password) async { 15 final response = await client.post(16 '/auth/login',17 body: {'email': email, 'password': password},18 );19 20 if (response.statusCode == 200) {21 return UserModel.fromJson(response.data);22 } else {23 throw ServerException(message: response.message);24 }25 }26 27 @override28 Future register(String name, String email, String password) async { 29 final response = await client.post(30 '/auth/register',31 body: {'name': name, 'email': email, 'password': password},32 );33 34 if (response.statusCode == 201) {35 return UserModel.fromJson(response.data);36 } else {37 throw ServerException(message: response.message);38 }39 }40 41 @override42 Future logout() async { 43 await client.post('/auth/logout');44 }45}Repository Implementation
dart
1// data/repositories/auth_repository_impl.dart2import 'package:dartz/dartz.dart';3 4class AuthRepositoryImpl implements AuthRepository {5 final AuthRemoteDataSource remoteDataSource;6 final AuthLocalDataSource localDataSource;7 final NetworkInfo networkInfo;8 9 AuthRepositoryImpl({10 required this.remoteDataSource,11 required this.localDataSource,12 required this.networkInfo,13 });14 15 @override16 Future> login(String email, String password) async { 17 if (await networkInfo.isConnected) {18 try {19 final user = await remoteDataSource.login(email, password);20 await localDataSource.cacheUser(user);21 return Right(user);22 } on ServerException catch (e) {23 return Left(ServerFailure(message: e.message));24 }25 } else {26 return const Left(NetworkFailure(message: 'Internet baglantisi yok'));27 }28 }29 30 @override31 Future> getCurrentUser() async { 32 try {33 final cachedUser = await localDataSource.getCachedUser();34 if (cachedUser != null) return Right(cachedUser);35 return const Left(CacheFailure(message: 'Kullanici cache bulunamadi'));36 } on CacheException catch (e) {37 return Left(CacheFailure(message: e.message));38 }39 }40 41 // diger metodlar...42 @override43 Future> register( 44 String name, String email, String password) async {45 if (await networkInfo.isConnected) {46 try {47 final user = await remoteDataSource.register(name, email, password);48 await localDataSource.cacheUser(user);49 return Right(user);50 } on ServerException catch (e) {51 return Left(ServerFailure(message: e.message));52 }53 } else {54 return const Left(NetworkFailure(message: 'Internet baglantisi yok'));55 }56 }57 58 @override59 Future> logout() async { 60 try {61 await remoteDataSource.logout();62 await localDataSource.clearCache();63 return const Right(null);64 } catch (e) {65 return Left(ServerFailure(message: e.toString()));66 }67 }68 69 @override70 Stream get authStateChanges => localDataSource.watchUser(); 71}5. Presentation Katmani
dart
1// presentation/providers/auth_provider.dart2import 'package:flutter_riverpod/flutter_riverpod.dart';3 4class AuthState {5 final User? user;6 final bool isLoading;7 final String? errorMessage;8 9 const AuthState({this.user, this.isLoading = false, this.errorMessage});10 11 AuthState copyWith({User? user, bool? isLoading, String? errorMessage}) {12 return AuthState(13 user: user ?? this.user,14 isLoading: isLoading ?? this.isLoading,15 errorMessage: errorMessage,16 );17 }18}19 20class AuthNotifier extends StateNotifier { 21 final LoginUseCase _loginUseCase;22 final LogoutUseCase _logoutUseCase;23 24 AuthNotifier({25 required LoginUseCase loginUseCase,26 required LogoutUseCase logoutUseCase,27 }) : _loginUseCase = loginUseCase,28 _logoutUseCase = logoutUseCase,29 super(const AuthState());30 31 Future login(String email, String password) async { 32 state = state.copyWith(isLoading: true, errorMessage: null);33 34 final result = await _loginUseCase(35 LoginParams(email: email, password: password),36 );37 38 result.fold(39 (failure) => state = state.copyWith(40 isLoading: false,41 errorMessage: failure.message,42 ),43 (user) => state = state.copyWith(44 isLoading: false,45 user: user,46 ),47 );48 }49 50 Future logout() async { 51 await _logoutUseCase(const NoParams());52 state = const AuthState();53 }54}6. Dependency Injection
dart
1// injection_container.dart2import 'package:get_it/get_it.dart';3 4final sl = GetIt.instance;5 6Future initDependencies() async { 7 // Core8 sl.registerLazySingleton(() => NetworkInfoImpl()); 9 sl.registerLazySingleton(() => DioHttpClient()); 10 11 // Auth Feature12 // Data Sources13 sl.registerLazySingleton( 14 () => AuthRemoteDataSourceImpl(client: sl()),15 );16 sl.registerLazySingleton( 17 () => AuthLocalDataSourceImpl(),18 );19 20 // Repository21 sl.registerLazySingleton( 22 () => AuthRepositoryImpl(23 remoteDataSource: sl(),24 localDataSource: sl(),25 networkInfo: sl(),26 ),27 );28 29 // Use Cases30 sl.registerLazySingleton(() => LoginUseCase(sl()));31 sl.registerLazySingleton(() => LogoutUseCase(sl()));32 sl.registerLazySingleton(() => RegisterUseCase(sl()));33}7. Error Handling Stratejisi
dart
1// core/error/failures.dart2abstract class Failure {3 final String message;4 const Failure({required this.message});5}6 7class ServerFailure extends Failure {8 const ServerFailure({required super.message});9}10 11class NetworkFailure extends Failure {12 const NetworkFailure({required super.message});13}14 15class CacheFailure extends Failure {16 const CacheFailure({required super.message});17}18 19class ValidationFailure extends Failure {20 final Map fieldErrors; 21 const ValidationFailure({required super.message, this.fieldErrors = const {}});22}23 24// core/error/exceptions.dart25class ServerException implements Exception {26 final String message;27 final int? statusCode;28 const ServerException({required this.message, this.statusCode});29}30 31class CacheException implements Exception {32 final String message;33 const CacheException({required this.message});34}8. Test Stratejileri
Clean Architecture Test Piramidi
Test Tipi | Katman | Aracllar | Hiz |
|---|---|---|---|
**Unit Test** | Domain (UseCase) | mockito, dartz | Cok Hizli |
**Unit Test** | Data (Repository) | mockito, fake | Hizli |
**Widget Test** | Presentation | flutter_test | Orta |
**Integration Test** | Tum katmanlar | integration_test | Yavas |
dart
1// test/features/auth/domain/usecases/login_test.dart2import 'package:flutter_test/flutter_test.dart';3import 'package:mockito/mockito.dart';4import 'package:dartz/dartz.dart';5 6class MockAuthRepository extends Mock implements AuthRepository {}7 8void main() {9 late LoginUseCase useCase;10 late MockAuthRepository mockRepository;11 12 setUp(() {13 mockRepository = MockAuthRepository();14 useCase = LoginUseCase(mockRepository);15 });16 17 const testUser = User(18 id: '1',19 name: 'Test User',20 email: '[email protected]',21 createdAt: DateTime(2024, 1, 1),22 );23 24 test('basarili giris yaptiginda kullanici dondurulmeli', () async {25 when(mockRepository.login('[email protected]', 'password123'))26 .thenAnswer((_) async => const Right(testUser));27 28 final result = await useCase(29 const LoginParams(email: '[email protected]', password: 'password123'),30 );31 32 expect(result, const Right(testUser));33 verify(mockRepository.login('[email protected]', 'password123'));34 verifyNoMoreInteractions(mockRepository);35 });36 37 test('hatali giris yaptiginda failure donmeli', () async {38 when(mockRepository.login('[email protected]', 'wrong'))39 .thenAnswer((_) async => const Left(40 ServerFailure(message: 'Hatali sifre'),41 ));42 43 final result = await useCase(44 const LoginParams(email: '[email protected]', password: 'wrong'),45 );46 47 expect(result, isA()); 48 });49}9. Best Practices
- Feature-first: klasor yapisi kullanin (katman-first degil)
- Her feature'in kendi domain/data/presentation klasorleri olmali
- Either: tipi ile hata yonetimi yapin (exception firlatma yerine)
- UseCase: her zaman tek bir is yapmali
- Repository: interface domain'de, implementation data'da olmali
Sonuc ve Oneriler
Clean Architecture, Flutter projelerinde uzun vadeli surdurulebilirlik ve test edilebilirlik saglar. Baslangicta daha fazla kod yazilir ama bu yatirim, proje buyudukce kendini katlayarak geri oder.
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.
Okuyucu Ödülü
Tebrikler! Bu yazıyı sonuna kadar okuduğun için sana özel bir hediyem var:

