Tüm Yazılar
KategoriFlutter
Okuma Süresi
25 dk okuma
Yayın Tarihi
...
Kelime Sayısı
1.755kelime

Kahveni hazırla - bu içerikli bir makale!

Flutter projelerinde Clean Architecture uygulamasi. Domain, Data ve Presentation katmanlari, Dependency Injection, Repository pattern ve test stratejileri.

Flutter Clean Architecture: Katmanli Mimari Rehberi

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


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.dart
5 failures.dart
6 network/
7 network_info.dart
8 usecases/
9 usecase.dart
10 utils/
11 input_converter.dart
12 features/
13 auth/
14 domain/
15 entities/
16 user.dart
17 repositories/
18 auth_repository.dart
19 usecases/
20 login.dart
21 register.dart
22 logout.dart
23 data/
24 models/
25 user_model.dart
26 datasources/
27 auth_remote_datasource.dart
28 auth_local_datasource.dart
29 repositories/
30 auth_repository_impl.dart
31 presentation/
32 providers/
33 auth_provider.dart
34 pages/
35 login_page.dart
36 register_page.dart
37 widgets/
38 login_form.dart

3. Domain Katmani

Domain katmani, uygulamanin kalbidir. Hicbir dis bagimliligi yoktur — saf Dart kodudur.

Entity

dart
1// domain/entities/user.dart
2class 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.dart
2import '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.dart
2import '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.dart
13class LoginUseCase implements UseCase {
14 final AuthRepository repository;
15 
16 LoginUseCase(this.repository);
17 
18 @override
19 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.dart
2class 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.dart
2abstract 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 @override
14 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 @override
28 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 @override
42 Future logout() async {
43 await client.post('/auth/logout');
44 }
45}

Repository Implementation

dart
1// data/repositories/auth_repository_impl.dart
2import '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 @override
16 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 @override
31 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 @override
43 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 @override
59 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 @override
70 Stream get authStateChanges => localDataSource.watchUser();
71}

5. Presentation Katmani

dart
1// presentation/providers/auth_provider.dart
2import '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.dart
2import 'package:get_it/get_it.dart';
3 
4final sl = GetIt.instance;
5 
6Future initDependencies() async {
7 // Core
8 sl.registerLazySingleton(() => NetworkInfoImpl());
9 sl.registerLazySingleton(() => DioHttpClient());
10 
11 // Auth Feature
12 // Data Sources
13 sl.registerLazySingleton(
14 () => AuthRemoteDataSourceImpl(client: sl()),
15 );
16 sl.registerLazySingleton(
17 () => AuthLocalDataSourceImpl(),
18 );
19 
20 // Repository
21 sl.registerLazySingleton(
22 () => AuthRepositoryImpl(
23 remoteDataSource: sl(),
24 localDataSource: sl(),
25 networkInfo: sl(),
26 ),
27 );
28 
29 // Use Cases
30 sl.registerLazySingleton(() => LoginUseCase(sl()));
31 sl.registerLazySingleton(() => LogoutUseCase(sl()));
32 sl.registerLazySingleton(() => RegisterUseCase(sl()));
33}

7. Error Handling Stratejisi

dart
1// core/error/failures.dart
2abstract 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.dart
25class 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.dart
2import '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:

Etiketler

#Flutter#Clean Architecture#SOLID#Dart#Design Patterns#Testing
Muhittin Çamdalı

Muhittin Çamdalı

Senior iOS Developer

12+ yıllık deneyime sahip iOS Developer. Swift, SwiftUI ve modern iOS mimarileri konusunda uzman. Apple platformlarında performanslı ve kullanıcı dostu uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.

Paylaş

Bunu da begenebilirsiniz