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

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

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

  • **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 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.

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

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

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

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

  • 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` 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ç