Clean Architecture
با رشد پروژه، بدون معماری مناسب کد بهسرعت غیرقابل نگهداری میشود. در این فصل با Clean Architecture برای Flutter و پیادهسازی عملی آن آشنا میشویم.
۱۴.۱ مقدمه
با رشد پروژه، بدون معماری مناسب کد بهسرعت غیرقابل نگهداری میشود. در این فصل با Clean Architecture برای Flutter و پیادهسازی عملی آن آشنا میشویم.
۱۴.۲ مشکلات بدون معماری
- UI و business logic در هم تنیدهاند
- Test کردن سخت میشود
- تغییر یک بخش روی همه چیز اثر میگذارد
- Navigation logic در widget ها
- API calls مستقیم در UI
- Duplicate code فراوان
۱۴.۳ Clean Architecture
اصول طراحی Clean Architecture (Uncle Bob):
┌─────────────────────────────────────┐
│ Presentation Layer │ (UI، Widgets، State)
│ - Pages, Widgets │
│ - Riverpod/Bloc │
├─────────────────────────────────────┤
│ Domain Layer │ (Pure Dart، بدون Flutter)
│ - Entities │
│ - Use Cases │
│ - Repository Interfaces │
├─────────────────────────────────────┤
│ Data Layer │ (پیادهسازی)
│ - Models (DTO) │
│ - Repository Implementations │
│ - Data Sources (API, Local DB) │
└─────────────────────────────────────┘
⬇ وابستگی فقط به سمت پایین
Presentation → Domain ← Data
قانون Dependency
لایههای بالاتر میتوانند به پایین وابسته باشند، اما هرگز برعکس. Domain هیچ چیز نمیداند از Data یا Presentation.
۱۴.۴ ساختار پوشهها
lib/
├── main.dart
├── app.dart
├── core/
│ ├── error/
│ │ ├── exceptions.dart
│ │ └── failures.dart
│ ├── network/
│ │ ├── api_client.dart
│ │ └── network_info.dart
│ ├── usecases/
│ │ └── usecase.dart
│ └── theme/
│ └── app_theme.dart
├── features/
│ ├── auth/
│ │ ├── data/
│ │ │ ├── datasources/
│ │ │ │ ├── auth_remote_datasource.dart
│ │ │ │ └── auth_local_datasource.dart
│ │ │ ├── models/
│ │ │ │ └── user_model.dart
│ │ │ └── repositories/
│ │ │ └── auth_repository_impl.dart
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ │ └── user.dart
│ │ │ ├── repositories/
│ │ │ │ └── auth_repository.dart
│ │ │ └── usecases/
│ │ │ ├── login.dart
│ │ │ ├── register.dart
│ │ │ └── logout.dart
│ │ └── presentation/
│ │ ├── providers/
│ │ │ └── auth_provider.dart
│ │ ├── pages/
│ │ │ ├── login_page.dart
│ │ │ └── register_page.dart
│ │ └── widgets/
│ │ └── login_form.dart
│ ├── products/
│ │ └── ...
│ └── orders/
│ └── ...
└── shared/
├── widgets/
└── extensions/
۱۴.۵ Domain Layer
Entity (پاک، بدون JSON)
// features/auth/domain/entities/user.dart
class User {
final int id;
final String email;
final String name;
final String? avatarUrl;
const User({
required this.id,
required this.email,
required this.name,
this.avatarUrl,
});
}
// با freezed برای immutability + equals
@freezed
class User with _$User {
const factory User({
required int id,
required String email,
required String name,
String? avatarUrl,
}) = _User;
}
Repository Interface
// features/auth/domain/repositories/auth_repository.dart
abstract class AuthRepository {
Future> login({
required String email,
required String password,
});
Future> register({
required String email,
required String password,
required String name,
});
Future> logout();
Future> getCurrentUser();
}
// Either از package dartz - برای error handling functional
// Left = Failure, Right = Success
Use Case
// core/usecases/usecase.dart
abstract class UseCase {
Future> call(Params params);
}
// features/auth/domain/usecases/login.dart
class Login implements UseCase {
final AuthRepository repository;
Login(this.repository);
@override
Future> call(LoginParams params) async {
// اعتبارسنجی business
if (!_isValidEmail(params.email)) {
return Left(ValidationFailure("ایمیل نامعتبر"));
}
if (params.password.length < 8) {
return Left(ValidationFailure("رمز کوتاه است"));
}
return repository.login(
email: params.email,
password: params.password,
);
}
bool _isValidEmail(String email) {
return RegExp(r"^[w-.]+@([w-]+.)+[w-]{2,4}$").hasMatch(email);
}
}
class LoginParams {
final String email;
final String password;
const LoginParams({required this.email, required this.password});
}
Failures
// core/error/failures.dart
abstract class Failure {
final String message;
const Failure(this.message);
}
class ServerFailure extends Failure {
const ServerFailure(super.message);
}
class CacheFailure extends Failure {
const CacheFailure(super.message);
}
class NetworkFailure extends Failure {
const NetworkFailure(super.message);
}
class ValidationFailure extends Failure {
const ValidationFailure(super.message);
}
class AuthFailure extends Failure {
const AuthFailure(super.message);
}
۱۴.۶ Data Layer
Model (با JSON)
// features/auth/data/models/user_model.dart
@freezed
class UserModel with _$UserModel {
const UserModel._();
const factory UserModel({
required int id,
required String email,
required String name,
@JsonKey(name: "avatar_url") String? avatarUrl,
}) = _UserModel;
factory UserModel.fromJson(Map json) =>
_$UserModelFromJson(json);
// Map to domain entity
User toEntity() => User(
id: id,
email: email,
name: name,
avatarUrl: avatarUrl,
);
}
Data Source
// features/auth/data/datasources/auth_remote_datasource.dart
abstract class AuthRemoteDataSource {
Future login(String email, String password);
Future register(...);
Future logout();
}
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
final Dio dio;
AuthRemoteDataSourceImpl(this.dio);
@override
Future login(String email, String password) async {
try {
final response = await dio.post(
"/auth/login/",
data: {"email": email, "password": password},
);
return UserModel.fromJson(response.data["user"]);
} on DioException catch (e) {
if (e.response?.statusCode == 401) {
throw AuthException("ایمیل یا رمز اشتباه");
}
throw ServerException(e.message ?? "خطا");
}
}
}
Repository Implementation
// features/auth/data/repositories/auth_repository_impl.dart
class AuthRepositoryImpl implements AuthRepository {
final AuthRemoteDataSource remote;
final AuthLocalDataSource local;
final NetworkInfo networkInfo;
AuthRepositoryImpl({
required this.remote,
required this.local,
required this.networkInfo,
});
@override
Future> login({
required String email,
required String password,
}) async {
if (!await networkInfo.isConnected) {
return const Left(NetworkFailure("اتصال اینترنت ندارید"));
}
try {
final userModel = await remote.login(email, password);
await local.cacheUser(userModel);
return Right(userModel.toEntity());
} on AuthException catch (e) {
return Left(AuthFailure(e.message));
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
}
}
}
۱۴.۷ Presentation Layer
// features/auth/presentation/providers/auth_provider.dart
@riverpod
class AuthNotifier extends _$AuthNotifier {
@override
FutureOr build() async {
final getCurrentUser = ref.read(getCurrentUserUseCaseProvider);
final result = await getCurrentUser(NoParams());
return result.fold((_) => null, (user) => user);
}
Future login(String email, String password) async {
state = const AsyncValue.loading();
final login = ref.read(loginUseCaseProvider);
final result = await login(LoginParams(
email: email,
password: password,
));
state = result.fold(
(failure) => AsyncValue.error(failure, StackTrace.current),
(user) => AsyncValue.data(user),
);
}
Future logout() async {
final logout = ref.read(logoutUseCaseProvider);
await logout(NoParams());
state = const AsyncValue.data(null);
}
}
// در UI
class LoginPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authNotifierProvider);
ref.listen(authNotifierProvider, (prev, next) {
next.whenOrNull(
data: (user) {
if (user != null) context.go("/");
},
error: (error, _) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text((error as Failure).message)),
);
},
);
});
return Scaffold(
// form
);
}
}
۱۴.۸ Dependency Injection
با Riverpod (پیشنهاد)
@riverpod
Dio dio(Ref ref) {
return Dio(BaseOptions(baseUrl: "https://api.shop.com/api/v1/"));
}
@riverpod
AuthRemoteDataSource authRemoteDataSource(Ref ref) {
return AuthRemoteDataSourceImpl(ref.read(dioProvider));
}
@riverpod
AuthRepository authRepository(Ref ref) {
return AuthRepositoryImpl(
remote: ref.read(authRemoteDataSourceProvider),
local: ref.read(authLocalDataSourceProvider),
networkInfo: ref.read(networkInfoProvider),
);
}
@riverpod
Login loginUseCase(Ref ref) => Login(ref.read(authRepositoryProvider));
@riverpod
Register registerUseCase(Ref ref) => Register(ref.read(authRepositoryProvider));
با get_it (option جایگزین)
import "package:get_it/get_it.dart";
final getIt = GetIt.instance;
void setupDependencies() {
// Singleton
getIt.registerLazySingleton(() => Dio(...));
getIt.registerLazySingleton(() => NetworkInfoImpl());
// Factory (هر بار جدید)
getIt.registerFactory(
() => AuthRemoteDataSourceImpl(getIt()),
);
getIt.registerLazySingleton(
() => AuthRepositoryImpl(
remote: getIt(),
local: getIt(),
networkInfo: getIt(),
),
);
getIt.registerFactory(() => Login(getIt()));
}
// استفاده
final login = getIt();
۱۴.۹ اصول SOLID در Flutter
- S — Single Responsibility: هر class یک کار کند
- O — Open/Closed: برای extension باز، برای modification بسته
- L — Liskov Substitution: subclass باید جای parent کار کند
- I — Interface Segregation: interface کوچک بهتر از بزرگ
- D — Dependency Inversion: به abstraction وابسته باشید نه concrete
Clean Architecture این اصول را رعایت میکند.
۱۴.۱۰ معماریهای جایگزین
MVVM
Model-View-ViewModel. سادهتر از Clean. مناسب پروژههای متوسط.
BLoC Pattern
سختگیر، با Event/State.
Feature-First (Layer-First)
به جای تقسیم بر اساس layer، بر اساس feature تقسیم کنید (مثل ساختار بالا).
Riverpod Notifier Pattern
سادهترین و Flutter-idiomatic. توصیه برای پروژههای متوسط.
۱۴.۱۱ بهترین تجربیات
- Feature-first folder.
- Domain pure Dart. بدون Flutter import.
- Either برای error handling functional.
- Repository abstract. پیادهسازی در data.
- Use case برای business logic.
- DI با Riverpod یا get_it.
- Test هر لایه جدا.
- Naming consistent. XxxImpl، XxxModel، XxxEntity.
- Code generation. با freezed و json_serializable.
- عملی باشید. برای پروژه کوچک، Clean overkill است.
۱۴.۱۲ خلاصه فصل
- Clean Architecture: Presentation، Domain، Data
- Domain pure Dart با Entity، UseCase، Repository interface
- Data layer با Model، DataSource، Repository impl
- Either برای error handling
- DI با Riverpod یا get_it
- SOLID principles