Flutter'da Temiz Mimari ile Özellik Öncelikli Mimari
Flutter uygulaması büyüdükçe kod yapısı faktör haline geliyor ölçeklenen bir uygulama ile teknik borç biriktiren bir uygulama arasındaki belirleyici faktör. organizasyon Özellik-Önce — her üst düzey dizinin bir temsil ettiği yer bağımsız işlevsellik ve büyük kurumsal Flutter uygulamalarının benimsediği model ilkeleriyle birleşen 2026 yılındaki başarı Temiz Mimari Robert C. Martin tarafından her özellikteki kaygıların kesin bir şekilde ayrılması.
Bu makale sıfırdan eksiksiz bir mimari oluşturuyor: yapı dizinlerin oluşturulması, üç katmanın uygulanması (veri, etki alanı, sunum), Riverpod ile bağımlılık enjeksiyonu ve modülleri çıkarılabilir tutmak için stratejiler ve bağımsız olarak test edilebilir.
Ne Öğreneceksiniz
- Özellik-Öncelik ve Katman-Öncelik: İlki neden büyük ekiplerde daha iyi ölçeklenir?
- Temiz Mimarinin üç katmanı: veri, etki alanı, sunum
- Depo Modeli: Dart arayüzleriyle veri erişimini soyutlama
- Kullanım Örneği (Etkileşimci): İş mantığını test edilebilir sınıflarda kapsülleyin
- Riverpod ile Dependency Injection: Katmanları birleştirmeden bağlama
- Kurumsal Flutter projesi için dizin yapısı
- Katmana göre test etme: birim test alanı, widget test sunumu
- Bir özellik ayrı bir Dart paketine nasıl çıkarılır?
Önce Özellik ve Önce Katman: Temel Karşılaştırma
Çoğu Flutter dersi, kodu türe göre düzenler:
models/, repositories/, screens/.
Bu Birinci Katman yapısı oldukça küçük görünüyor ancak ölçeklenmiyor.
# Layer-First (NON SCALABILE per progetti grandi)
lib/
models/
user.dart
product.dart
cart.dart
order.dart
repositories/
user_repository.dart
product_repository.dart
cart_repository.dart
screens/
login_screen.dart
product_list_screen.dart
cart_screen.dart
blocs/
auth_bloc.dart
product_bloc.dart
cart_bloc.dart
# Problema: per aggiungere la feature "Cart" devi toccare
# 4 directory diverse. Impossibile estrarre il modulo.
# Team A e Team B modificano gli stessi file contemporaneamente.
# Feature-First (SCALABILE)
lib/
features/
auth/
data/
domain/
presentation/
products/
data/
domain/
presentation/
cart/
data/
domain/
presentation/
core/
network/
storage/
theme/
# La feature "Cart" e completamente autonoma in lib/features/cart/
# Un team lavora su cart/ senza toccare le altre feature.
# In futuro: dart create --template=package cart e sposta la directory.
Bir Özelliğin Anatomisi: Üç Katman
Temiz Mimari, her özelliğin içinde üç katman uygular: tek yönlü bağımlılıklar: Sunum buna bağlı İhtisas, Tarih buna bağlı İhtisas, Ancak Alan adı kimseye bağlı değildir. Bu kural kalptir mimari.
# Struttura completa della feature "products"
lib/features/products/
# DOMAIN LAYER: entita pure Dart, nessuna dipendenza esterna
domain/
entities/
product.dart # Entita del dominio (pure Dart class)
product_filter.dart # Value object per i filtri
repositories/
products_repository.dart # Interfaccia (abstract class)
usecases/
get_products_usecase.dart # Recupera lista prodotti
search_products_usecase.dart # Cerca prodotti per query
get_product_detail_usecase.dart
# DATA LAYER: implementa le interfacce domain con source concrete
data/
models/
product_dto.dart # DTO: mappa JSON API -> entita domain
sources/
products_remote_source.dart # HTTP calls
products_local_source.dart # SQLite / Hive cache
repositories/
products_repository_impl.dart # Implementa ProductsRepository
# PRESENTATION LAYER: UI e state management
presentation/
providers/
products_provider.dart # Riverpod providers
pages/
products_list_page.dart
product_detail_page.dart
widgets/
product_card.dart
product_filter_bar.dart
Etki Alanı Katmanı: Varlık ve Depo Arayüzü
# domain/entities/product.dart
// Entita: pure Dart, zero dipendenze da Flutter o pacchetti esterni
// Immutabile per convenzione (const constructor + final fields)
class Product {
const Product({
required this.id,
required this.name,
required this.price,
required this.imageUrl,
required this.category,
this.description = '',
this.rating = 0.0,
this.inStock = true,
});
final String id;
final String name;
final double price;
final String imageUrl;
final String category;
final String description;
final double rating;
final bool inStock;
// copyWith: aggiorna campi mantenendo l'immutabilita
Product copyWith({
String? id,
String? name,
double? price,
String? imageUrl,
String? category,
String? description,
double? rating,
bool? inStock,
}) {
return Product(
id: id ?? this.id,
name: name ?? this.name,
price: price ?? this.price,
imageUrl: imageUrl ?? this.imageUrl,
category: category ?? this.category,
description: description ?? this.description,
rating: rating ?? this.rating,
inStock: inStock ?? this.inStock,
);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Product && runtimeType == other.runtimeType && id == other.id;
@override
int get hashCode => id.hashCode;
}
// domain/repositories/products_repository.dart
// Interfaccia: il Domain layer definisce IL CONTRATTO
// Il Data layer lo implementa. Il Domain non sa COME vengono presi i dati.
abstract interface class ProductsRepository {
Future<List<Product>> getProducts({
String? category,
int page = 1,
int limit = 20,
});
Future<Product> getProductById(String id);
Future<List<Product>> searchProducts(String query);
Future<void> refreshCache();
}
Etki Alanı Katmanı: Kullanım Örneği
Kullanım Durumları (veya Etkileşimciler) tek bir iş mantığı eylemini kapsar. ben küçük, test edilebilir, tek sorumlu sınıflar. Kullanım Senaryosu büyürse çok fazla ve bölünmesi gereken bir sinyal.
# domain/usecases/get_products_usecase.dart
import 'package:my_app/features/products/domain/entities/product.dart';
import 'package:my_app/features/products/domain/repositories/products_repository.dart';
// Parametri del Use Case: value object per type safety
class GetProductsParams {
const GetProductsParams({
this.category,
this.page = 1,
this.limit = 20,
});
final String? category;
final int page;
final int limit;
}
// Use Case: dipende SOLO dall'interfaccia repository (domain)
// Non sa nulla di HTTP, Dio, SQLite o Riverpod
class GetProductsUseCase {
const GetProductsUseCase(this._repository);
final ProductsRepository _repository;
Future<List<Product>> call(GetProductsParams params) {
return _repository.getProducts(
category: params.category,
page: params.page,
limit: params.limit,
);
}
}
// Test del Use Case: zero dipendenze da Flutter
// test/features/products/domain/get_products_usecase_test.dart
class MockProductsRepository extends Mock implements ProductsRepository {}
void main() {
late GetProductsUseCase useCase;
late MockProductsRepository mockRepo;
setUp(() {
mockRepo = MockProductsRepository();
useCase = GetProductsUseCase(mockRepo);
});
test('chiama repository con i parametri corretti', () async {
// Arrange
const params = GetProductsParams(category: 'electronics', page: 2);
when(() => mockRepo.getProducts(
category: 'electronics',
page: 2,
limit: 20,
)).thenAnswer((_) async => []);
// Act
await useCase(params);
// Assert
verify(() => mockRepo.getProducts(
category: 'electronics',
page: 2,
limit: 20,
)).called(1);
});
}
Veri Katmanı: DTO ve Depo Uygulaması
# data/models/product_dto.dart
// DTO (Data Transfer Object): sa come mappare JSON <-> entita domain
// Dipende dal domain (Product entity) ma non viceversa
class ProductDto {
const ProductDto({
required this.id,
required this.name,
required this.price,
required this.imageUrl,
required this.category,
this.description,
this.rating,
this.inStock,
});
final String id;
final String name;
final double price;
final String imageUrl;
final String category;
final String? description;
final double? rating;
final bool? inStock;
// Factory: converte Map JSON in DTO
factory ProductDto.fromJson(Map<String, dynamic> json) {
return ProductDto(
id: json['id'] as String,
name: json['name'] as String,
price: (json['price'] as num).toDouble(),
imageUrl: json['image_url'] as String,
category: json['category'] as String,
description: json['description'] as String?,
rating: (json['rating'] as num?)?.toDouble(),
inStock: json['in_stock'] as bool?,
);
}
// toEntity: converte DTO in entita domain
Product toEntity() {
return Product(
id: id,
name: name,
price: price,
imageUrl: imageUrl,
category: category,
description: description ?? '',
rating: rating ?? 0.0,
inStock: inStock ?? true,
);
}
}
# data/repositories/products_repository_impl.dart
class ProductsRepositoryImpl implements ProductsRepository {
const ProductsRepositoryImpl({
required this.remoteSource,
required this.localSource,
});
final ProductsRemoteSource remoteSource;
final ProductsLocalSource localSource;
@override
Future<List<Product>> getProducts({
String? category,
int page = 1,
int limit = 20,
}) async {
try {
// Prima prova la cache locale
final cached = await localSource.getProducts(
category: category,
page: page,
limit: limit,
);
if (cached.isNotEmpty) {
return cached.map((dto) => dto.toEntity()).toList();
}
// Se la cache e vuota, chiama l'API remota
final dtos = await remoteSource.getProducts(
category: category,
page: page,
limit: limit,
);
// Salva in cache per la prossima volta
await localSource.saveProducts(dtos);
return dtos.map((dto) => dto.toEntity()).toList();
} catch (e) {
throw ProductsException('Impossibile caricare i prodotti: $e');
}
}
@override
Future<Product> getProductById(String id) async {
final dto = await remoteSource.getProductById(id);
return dto.toEntity();
}
@override
Future<List<Product>> searchProducts(String query) async {
final dtos = await remoteSource.searchProducts(query);
return dtos.map((dto) => dto.toEntity()).toList();
}
@override
Future<void> refreshCache() => localSource.clearAll();
}
Riverpod ile Bağımlılık Enjeksiyonu
Riverpod, üç katmanı doğrudan bağlantı olmadan birbirine bağlayan yapıştırıcıdır:
her sağlayıcı bağımlılıklarını aracılığıyla bildirir ref.watch e
Riverpod, bağımlılık grafiğinin tamamını tembel ve tür açısından güvenli bir şekilde çözer.
# presentation/providers/products_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'products_provider.g.dart';
// DATA LAYER providers
@riverpod
ProductsRemoteSource productsRemoteSource(Ref ref) {
return ProductsRemoteSource(dio: ref.watch(dioProvider));
}
@riverpod
ProductsLocalSource productsLocalSource(Ref ref) {
return ProductsLocalSource(db: ref.watch(databaseProvider));
}
// DOMAIN LAYER providers
@riverpod
ProductsRepository productsRepository(Ref ref) {
return ProductsRepositoryImpl(
remoteSource: ref.watch(productsRemoteSourceProvider),
localSource: ref.watch(productsLocalSourceProvider),
);
}
@riverpod
GetProductsUseCase getProductsUseCase(Ref ref) {
return GetProductsUseCase(ref.watch(productsRepositoryProvider));
}
// PRESENTATION LAYER: AsyncNotifier che usa il use case
@riverpod
class ProductsList extends _$ProductsList {
@override
Future<List<Product>> build({String? category}) async {
return ref.watch(getProductsUseCaseProvider).call(
GetProductsParams(category: category),
);
}
Future<void> refresh() async {
await ref.read(productsRepositoryProvider).refreshCache();
ref.invalidateSelf();
}
}
// Nel widget: nessun accoppiamento con i layer sottostanti
class ProductsListPage extends ConsumerWidget {
const ProductsListPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final productsAsync = ref.watch(productsListProvider());
return Scaffold(
appBar: AppBar(title: const Text('Prodotti')),
body: productsAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Errore: $e')),
data: (products) => ListView.builder(
itemCount: products.length,
itemBuilder: (context, i) => ProductCard(product: products[i]),
),
),
);
}
}
Katmana Göre Test Etme
Temiz Mimari, optimal bir test stratejisi sağlar: her katmanın Gerekli minimum kurulumla kendi test türü.
# Struttura test allineata alla struttura feature
test/
features/
products/
domain/
get_products_usecase_test.dart # Unit test puri (no Flutter)
product_entity_test.dart
data/
product_dto_test.dart # Unit test con JSON fixtures
products_repository_impl_test.dart # Mock remote + local source
presentation/
products_list_page_test.dart # Widget test con provider override
# Widget test con override del provider per mock
testWidgets('Mostra lista prodotti', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
// Override del use case con dati mock
getProductsUseCaseProvider.overrideWithValue(
FakeGetProductsUseCase(products: [
const Product(
id: '1',
name: 'iPhone 15',
price: 999.0,
imageUrl: 'https://example.com/iphone.jpg',
category: 'electronics',
),
]),
),
],
child: const MaterialApp(home: ProductsListPage()),
),
);
await tester.pumpAndSettle();
// Verifica che il prodotto mock sia visibile
expect(find.text('iPhone 15'), findsOneWidget);
expect(find.text('999.00 EUR'), findsOneWidget);
});
class FakeGetProductsUseCase extends Fake implements GetProductsUseCase {
FakeGetProductsUseCase({required this.products});
final List<Product> products;
@override
Future<List<Product>> call(GetProductsParams params) async => products;
}
Bir Paketteki Özellik Ne Zaman Çıkartılır?
Bir özellik birden fazla uygulamada yeniden kullanıldığında (ör. auth arasında paylaşılan
tüketici ve yönetici uygulaması) ve ayrı bir Dart paketi oluşturmanın zamanı geldi:
dart create --template=package packages/auth_feature. Temiz
Mimari bu çıkarmayı neredeyse mekanik hale getirir: dizinin dizinini kopyalarsınız.
Yeni paketteki özelliği ve içe aktarmaları güncelleyin. Sıfır mimari yeniden düzenleme.
Sonuçlar
Temiz Mimari ile Özellik Öncelikli Mimari en basit çözüm değildir küçük bir uygulama için ve büyüyüp gelişmesi gereken bir uygulama için doğru çözüm ve takım değişikliğinden sağ kurtulun. İlk kurulum maliyeti (dizin yapısı, arayüzler, DTO'lar, kullanım durumları) hızla amorti edilir: her yeni özellik birbirini takip eder Aynı modelde, her geliştirici kodu tam olarak nerede bulacağını ve nereye koyacağını bilir, ve her katman bağımsız olarak test edilip değiştirilebilir.
2026'da Nubank'tan eBay'e ve ByteDance'a kadar en güçlü kurumsal Flutter uygulamaları - hepsi bu yapının varyantlarına doğru birleşiyor. Bu bir trend değil: cevap bu Flutter'ın 3 kişilik ekibin ötesine geçmesiyle ortaya çıkan gerçek sorunların pratiğini yapın.







