Flutter

Flutter'da Clean Architecture: Ölçeklenebilir Uygulama Geliştirme

15 dk okuma9 Şubat 2026Güncellendi 9 Mar 2026
Flutter clean architectureFlutter mimariFlutter architectureSOLID principles FlutterClean code FlutterFlutter project structureFlutter best practicesscalable Flutter app

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.

  • Test edilebilirlik: Her katman, uygulamanın tamamını mocklamadan bağımsız olarak test edilebilir.
  • Ölçeklenebilirlik: Birden fazla geliştirici, birbirinin ayağına basmadan farklı özellikler üzerinde çalışabilir.
  • Esneklik: REST API'yi GraphQL ile veya SharedPreferences'ı Hive ile değiştirirken iş mantığına dokunmanıza gerek kalmaz.
  • Düşük risk: Gereksinimler sürekli değişir. Domain katmanınız saf Dart olduğunda, uyum sağlamak ucuzdur.

Üç 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.

  • Entity'ler: Doğrulama kurallarıyla birlikte temel iş nesneleri.
  • Use Case'ler: Tek bir iş eylemini kapsayan, tek sorumluluk prensibine uygun sınıflar.
  • Repository Kontratları: Hangi veri operasyonlarının mevcut olduğunu tanımlayan, nasıl çalıştıklarını belirtmeyen abstract sınıflar.

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.

  • Repository Implementasyonları: Domain kontratlarını yerine getiren somut sınıflar.
  • Data Source'lar: Uzak (API) ve yerel (veritabanı, cache) kaynaklar.
  • Model'ler: Serileştirme ve deserileştirme işlemlerini yürüten veri transfer nesneleri.

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.

dart
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.

dart
import '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.

dart
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.

dart
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['id'] as String,
      email: json['email'] as String,
      displayName: json['display_name'] as String,
      createdAt: DateTime.parse(json['created_at'] as String),
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'email': email,
      'display_name': displayName,
      '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.

dart
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('$baseUrl/auth/login'),
      body: jsonEncode({'email': email, 'password': password}),
      headers: {'Content-Type': 'application/json'},
    );

    if (response.statusCode == 200) {
      return UserModel.fromJson(jsonDecode(response.body));
    } else {
      throw ServerException(message: 'Giris basarisiz');
    }
  }

  @override
  Future<UserModel> register(String email, String password, String name) async {
    final response = await client.post(
      Uri.parse('$baseUrl/auth/register'),
      body: jsonEncode({'email': email, 'password': password, 'name': name}),
      headers: {'Content-Type': 'application/json'},
    );

    if (response.statusCode == 201) {
      return UserModel.fromJson(jsonDecode(response.body));
    } else {
      throw ServerException(message: '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.

dart
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('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.

dart
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.

dart
import 'package:get_it/get_it.dart';

final sl = GetIt.instance;

Future<void> initDependencies() async {
  // Harici bagimliliklar
  sl.registerLazySingleton(() => http.Client());
  sl.registerLazySingleton<NetworkInfo>(() => NetworkInfoImpl());

  // Data Source'lar
  sl.registerLazySingleton<AuthRemoteDataSource>(
    () => AuthRemoteDataSourceImpl(client: sl()),
  );
  sl.registerLazySingleton<AuthLocalDataSource>(
    () => AuthLocalDataSourceImpl(sharedPreferences: sl()),
  );

  // Repository'ler
  sl.registerLazySingleton<AuthRepository>(
    () => AuthRepositoryImpl(
      remoteDataSource: sl(),
      localDataSource: sl(),
      networkInfo: sl(),
    ),
  );

  // Use Case'ler
  sl.registerLazySingleton(() => LoginUser(sl()));
  sl.registerLazySingleton(() => RegisterUser(sl()));
  sl.registerLazySingleton(() => GetCurrentUser(sl()));

  // 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:

  • Hızlı bir prototip veya hackathon projesi geliştiriyorsanız.
  • Uygulama 5'ten az ekrana sahipse ve karmaşık iş mantığı yoksa.
  • Tek geliştiricisiniz ve projenin belirli bir bitiş tarihi varsa.
  • Uygulama, minimum dönüşüm mantığıyla tek bir API'nin ince bir sarmalayıcısıysa.

Benimsemeniz gereken durumlar:

  • Birden fazla geliştirici aynı anda kod tabanı üzerinde çalışacaksa.
  • Uygulama bir yıldan uzun süre bakımda kalacaksa.
  • İş mantığı karmaşıksa (ödeme akışları, çok adımlı formlar, offline senkronizasyon).
  • Veri kaynaklarını veya state management çözümlerini değiştirmeyi öngörüyorsanız.
  • Uyumluluk veya güvenilirlik için kapsamlı test coverage gerekiyorsa.

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<Failure, T> dönüş tipleri kullanın.

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 Projeniz mi Var?

iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.

İletişime Geç