Хорошая архитектура — залог успеха проекта. Разберём Clean Architecture для Flutter приложений.
Что такое Clean Architecture?
Clean Architecture — это подход, разделяющий приложение на слои с зависимостями направленными внутрь.
Слои архитектуры
┌─────────────────────────────┐
│ Presentation (UI) │
│ - Widgets │
│ - Controllers/BLoC │
├─────────────────────────────┤
│ Domain (Business Logic) │
│ - Entities │
│ - Use Cases │
├─────────────────────────────┤
│ Data (Data Sources) │
│ - Repositories │
│ - DTOs │
│ - Data Sources │
└─────────────────────────────┘
Структура проекта
lib/
├── core/
│ ├── error/
│ │ └── failures.dart
│ ├── network/
│ │ └── network_info.dart
│ ├── usecases/
│ │ └── usecase.dart
│ └── constants.dart
├── features/
│ └── auth/
│ ├── data/
│ │ ├── models/
│ │ ├── repositories/
│ │ └── datasources/
│ ├── domain/
│ │ ├── entities/
│ │ ├── repositories/
│ │ └── usecases/
│ └── presentation/
│ ├── bloc/
│ ├── pages/
│ └── widgets/
├── config/
│ └── routes.dart
└── main.dart
Domain слой
Entities (Сущности)
// features/auth/domain/entities/user.dart
class User {
final String id;
final String name;
final String email;
User({
required this.id,
required this.name,
required this.email,
});
}
Repository интерфейсы
// features/auth/domain/repositories/auth_repository.dart
abstract class AuthRepository {
Future<Either<Failure, User>> login(String email, String password);
Future<Either<Failure, User>> register(String email, String password);
Future<Either<Failure, void>> logout();
Stream<User?> get authStateChanges;
}
Use Cases
// core/usecases/usecase.dart
abstract class UseCase<Type, Params> {
Future<Either<Failure, Type>> call(Params params);
}
class NoParams extends Equatable {
@override
List<Object?> get props => [];
}
// features/auth/domain/usecases/login_usecase.dart
class LoginUseCase implements UseCase<User, LoginParams> {
final AuthRepository repository;
LoginUseCase(this.repository);
@override
Future<Either<Failure, User>> call(LoginParams params) async {
if (!params.email.contains('@')) {
return Left(InvalidEmailFailure());
}
return await repository.login(params.email, params.password);
}
}
class LoginParams extends Equatable {
final String email;
final String password;
const LoginParams({
required this.email,
required this.password,
});
@override
List<Object?> get props => [email, password];
}
Failures
// core/error/failures.dart
abstract class Failure extends Equatable {
final String message;
const Failure(this.message);
@override
List<Object?> get props => [message];
}
class ServerFailure extends Failure {
const ServerFailure(String message) : super(message);
}
class NetworkFailure extends Failure {
const NetworkFailure(String message) : super(message);
}
class InvalidEmailFailure extends Failure {
const InvalidEmailFailure([String message = 'Invalid email'])
: super(message);
}
class CacheFailure extends Failure {
const CacheFailure(String message) : super(message);
}
Data слой
DTOs (Data Transfer Objects)
// features/auth/data/models/user_model.dart
class UserModel extends User {
UserModel({
required String id,
required String name,
required String email,
}) : super(id: id, name: name, email: email);
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json['id'],
name: json['name'],
email: json['email'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
};
}
User toEntity() {
return User(id: id, name: name, email: email);
}
}
Data Sources
// features/auth/data/datasources/auth_remote_datasource.dart
abstract class AuthRemoteDataSource {
Future<UserModel> login(String email, String password);
Future<UserModel> register(String email, String password);
Future<void> logout();
}
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('https://api.example.com/login'),
body: {'email': email, 'password': password},
);
if (response.statusCode == 200) {
return UserModel.fromJson(json.decode(response.body));
} else {
throw ServerException();
}
}
@override
Future<UserModel> register(String email, String password) async {
final response = await client.post(
Uri.parse('https://api.example.com/register'),
body: {'email': email, 'password': password},
);
if (response.statusCode == 201) {
return UserModel.fromJson(json.decode(response.body));
} else {
throw ServerException();
}
}
@override
Future<void> logout() async {
await client.post(Uri.parse('https://api.example.com/logout'));
}
}
Repository Implementation
// features/auth/data/repositories/auth_repository_impl.dart
class AuthRepositoryImpl implements AuthRepository {
final AuthRemoteDataSource remoteDataSource;
final NetworkInfo networkInfo;
AuthRepositoryImpl({
required this.remoteDataSource,
required this.networkInfo,
});
@override
Future<Either<Failure, User>> login(String email, String password) async {
if (await networkInfo.isConnected) {
try {
final user = await remoteDataSource.login(email, password);
return Right(user.toEntity());
} on ServerException {
return Left(ServerFailure('Server error'));
}
} else {
return Left(NetworkFailure('No internet connection'));
}
}
@override
Future<Either<Failure, User>> register(String email, String password) async {
if (await networkInfo.isConnected) {
try {
final user = await remoteDataSource.register(email, password);
return Right(user.toEntity());
} on ServerException {
return Left(ServerFailure('Server error'));
}
} else {
return Left(NetworkFailure('No internet connection'));
}
}
@override
Future<Either<Failure, void>> logout() async {
try {
await remoteDataSource.logout();
return Right(null);
} on ServerException {
return Left(ServerFailure('Server error'));
}
}
@override
Stream<User?> get authStateChanges {
// Реализация stream изменений
return const Stream.empty();
}
}
Presentation слой
BLoC
// features/auth/presentation/bloc/auth_bloc.dart
abstract class AuthEvent extends Equatable {
const AuthEvent();
@override
List<Object?> get props => [];
}
class LoginEvent extends AuthEvent {
final String email;
final String password;
const LoginEvent({required this.email, required this.password});
@override
List<Object?> get props => [email, password];
}
class LogoutEvent extends AuthEvent {}
abstract class AuthState extends Equatable {
const AuthState();
@override
List<Object?> get props => [];
}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthAuthenticated extends AuthState {
final User user;
const AuthAuthenticated(this.user);
@override
List<Object?> get props => [user];
}
class AuthUnauthenticated extends AuthState {}
class AuthError extends AuthState {
final String message;
const AuthError(this.message);
@override
List<Object?> get props => [message];
}
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final LoginUseCase loginUseCase;
final LogoutUseCase logoutUseCase;
AuthBloc({
required this.loginUseCase,
required this.logoutUseCase,
}) : super(AuthInitial()) {
on<LoginEvent>(_onLogin);
on<LogoutEvent>(_onLogout);
}
Future<void> _onLogin(LoginEvent event, Emitter<AuthState> emit) async {
emit(AuthLoading());
final result = await loginUseCase(
LoginParams(email: event.email, password: event.password),
);
result.fold(
(failure) => emit(AuthError(failure.message)),
(user) => emit(AuthAuthenticated(user)),
);
}
Future<void> _onLogout(LogoutEvent event, Emitter<AuthState> emit) async {
emit(AuthLoading());
final result = await logoutUseCase(NoParams());
result.fold(
(failure) => emit(AuthError(failure.message)),
(_) => emit(AuthUnauthenticated()),
);
}
}
Pages
// features/auth/presentation/pages/login_page.dart
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is AuthLoading) {
return Center(child: CircularProgressIndicator());
}
return LoginForm();
},
),
);
}
}
Widgets
// features/auth/presentation/widgets/login_form.dart
class LoginForm extends StatefulWidget {
const LoginForm({super.key});
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message)),
);
}
},
child: Form(
key: _formKey,
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
controller: _emailController,
decoration: InputDecoration(labelText: 'Email'),
validator: (value) {
if (value == null || !value.contains('@')) {
return 'Invalid email';
}
return null;
},
),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
validator: (value) {
if (value == null || value.length < 6) {
return 'Password too short';
}
return null;
},
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
context.read<AuthBloc>().add(
LoginEvent(
email: _emailController.text,
password: _passwordController.text,
),
);
}
},
child: Text('Login'),
),
],
),
),
),
);
}
}
Dependency Injection
GetIt
// core/locator/locator.dart
final sl = GetIt.instance;
Future<void> init() async {
// External
sl.registerLazySingleton(() => http.Client());
sl.registerLazySingleton(() => NetworkInfoImpl());
// Data sources
sl.registerLazySingleton<AuthRemoteDataSource>(
() => AuthRemoteDataSourceImpl(client: sl()),
);
// Repositories
sl.registerLazySingleton<AuthRepository>(
() => AuthRepositoryImpl(
remoteDataSource: sl(),
networkInfo: sl(),
),
);
// Use cases
sl.registerLazySingleton(() => LoginUseCase(sl()));
sl.registerLazySingleton(() => LogoutUseCase(sl()));
// BLoC
sl.registerFactory(() => AuthBloc(
loginUseCase: sl(),
logoutUseCase: sl(),
));
}
Заключение
Clean Architecture помогает создавать поддерживаемые и тестируемые приложения. Используйте её для средних и больших проектов.