Как организуешь архитектуру виджетов в проекте?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Архитектура виджетов в Flutter проекте
Организация виджетов — это ключ к масштабируемости и поддерживаемости проекта. Качественная архитектура позволяет быстро добавлять функции, переиспользовать компоненты и минимизировать баги. Расскажу о структуре, которую использую в своей практике.
1. Структура папок
Основной принцип — модульная организация. Каждый модуль (feature) — независимая единица.
lib/
├── main.dart
├── config/ # Конфигурация приложения
│ ├── theme.dart
│ ├── routes.dart
│ └── constants.dart
│
├── core/ # Общая логика для всех модулей
│ ├── models/
│ │ └── user.dart
│ ├── exceptions/
│ │ └── app_exception.dart
│ ├── utils/
│ │ └── validators.dart
│ └── extensions/
│ └── context_extension.dart
│
├── features/ # Отдельные фичи/экраны
│ ├── authentication/
│ │ ├── presentation/
│ │ │ ├── pages/
│ │ │ │ ├── login_page.dart
│ │ │ │ └── signup_page.dart
│ │ │ ├── widgets/
│ │ │ │ ├── email_input.dart
│ │ │ │ ├── password_input.dart
│ │ │ │ └── login_button.dart
│ │ │ └── bloc/
│ │ │ ├── auth_bloc.dart
│ │ │ ├── auth_event.dart
│ │ │ └── auth_state.dart
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ │ └── user_entity.dart
│ │ │ └── repositories/
│ │ │ └── auth_repository.dart
│ │ └── data/
│ │ ├── datasources/
│ │ │ ├── remote/
│ │ │ │ └── auth_api.dart
│ │ │ └── local/
│ │ │ └── token_storage.dart
│ │ ├── models/
│ │ │ └── user_model.dart
│ │ └── repositories/
│ │ └── auth_repository_impl.dart
│ │
│ └── home/
│ ├── presentation/
│ ├── domain/
│ └── data/
│
└── shared/ # Общие компоненты (если не feature-specific)
├── widgets/
│ ├── app_button.dart
│ ├── app_text_field.dart
│ └── loading_indicator.dart
└── styles/
└── app_text_styles.dart
2. Слоистая архитектура (Clean Architecture)
Для каждой feature применяю трёхслойную архитектуру:
Presentation слой (UI, состояние, навигация) ↓ Domain слой (бизнес-логика, интерфейсы) ↓ Data слой (API, кэширование, БД)
// Domain слой: интерфейс репозитория
abstract class UserRepository {
Future<User> getUser(String id);
Future<void> saveUser(User user);
}
// Data слой: реализация
class UserRepositoryImpl implements UserRepository {
final UserRemoteDataSource remoteDataSource;
final UserLocalDataSource localDataSource;
@override
Future<User> getUser(String id) async {
try {
final remoteUser = await remoteDataSource.getUser(id);
await localDataSource.cacheUser(remoteUser);
return remoteUser;
} catch (e) {
return await localDataSource.getCachedUser(id);
}
}
}
// Presentation слой: использование
class UserBloc extends Bloc<UserEvent, UserState> {
final UserRepository userRepository;
UserBloc(this.userRepository);
Future<void> _onUserRequested(UserRequested event, Emitter emit) async {
emit(UserLoading());
try {
final user = await userRepository.getUser(event.userId);
emit(UserLoaded(user));
} catch (e) {
emit(UserError(e.toString()));
}
}
}
3. Разделение виджетов по типам
Page — полный экран с навигацией Screen — содержимое экрана без навигации Widget — переиспользуемые компоненты
// Page: управляет навигацией и состоянием
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => AuthBloc(context.read<AuthRepository>()),
child: LoginScreen(),
);
}
}
// Screen: экран без BLoC
class LoginScreen extends StatefulWidget {
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final emailController = TextEditingController();
final passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Login')),
body: BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthSuccess) {
Navigator.of(context).pushReplacementNamed('/home');
}
},
child: SingleChildScrollView(
child: Column(
children: [
EmailInput(controller: emailController),
PasswordInput(controller: passwordController),
LoginButton(
onPressed: () => _handleLogin(context),
),
],
),
),
),
);
}
void _handleLogin(BuildContext context) {
context.read<AuthBloc>().add(
LoginRequested(
email: emailController.text,
password: passwordController.text,
),
);
}
}
// Widget: переиспользуемый компонент
class EmailInput extends StatelessWidget {
final TextEditingController controller;
final String? error;
const EmailInput({
required this.controller,
this.error,
});
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
decoration: InputDecoration(
labelText: 'Email',
errorText: error,
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
);
}
}
4. Управление состоянием
БЛоК (Business Logic Component) отделяет бизнес-логику от UI.
// Event и State
sealed class AuthEvent {}
class LoginRequested extends AuthEvent {
final String email;
final String password;
LoginRequested({required this.email, required this.password});
}
sealed class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthSuccess extends AuthState {
final User user;
AuthSuccess(this.user);
}
class AuthError extends AuthState {
final String message;
AuthError(this.message);
}
// BLoC
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final AuthRepository authRepository;
AuthBloc(this.authRepository) : super(AuthInitial()) {
on<LoginRequested>(_onLoginRequested);
}
Future<void> _onLoginRequested(LoginRequested event, Emitter emit) async {
emit(AuthLoading());
try {
final user = await authRepository.login(
email: event.email,
password: event.password,
);
emit(AuthSuccess(user));
} catch (e) {
emit(AuthError(e.toString()));
}
}
}
5. Dependency Injection
Использую get_it для инъекции зависимостей.
final getIt = GetIt.instance;
void setupLocator() {
// Data sources
getIt.registerSingleton<UserRemoteDataSource>(
UserRemoteDataSourceImpl(dio: getIt()),
);
// Repositories
getIt.registerSingleton<UserRepository>(
UserRepositoryImpl(
remoteDataSource: getIt(),
localDataSource: getIt(),
),
);
// BLoCs
getIt.registerFactory<AuthBloc>(
() => AuthBloc(getIt<AuthRepository>()),
);
}
// В main.dart
void main() {
setupLocator();
runApp(const MyApp());
}
6. Переиспользование компонентов
Визуальные компоненты группирую в shared/widgets.
class AppButton extends StatelessWidget {
final String label;
final VoidCallback onPressed;
final bool isLoading;
final ButtonStyle variant;
const AppButton({
required this.label,
required this.onPressed,
this.isLoading = false,
this.variant = ButtonStyle.primary,
});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: _getStyle(variant),
child: isLoading
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: Text(label),
);
}
ButtonStyle _getStyle(ButtonStyle variant) {
// Логика стилей
}
}
7. Тестируемость
Хорошая архитектура предполагает лёгкое тестирование.
void main() {
group('AuthBloc', () {
late AuthBloc authBloc;
late MockAuthRepository mockAuthRepository;
setUp(() {
mockAuthRepository = MockAuthRepository();
authBloc = AuthBloc(mockAuthRepository);
});
test('emits [AuthLoading, AuthSuccess] when login succeeds', () {
final user = User(id: '1', name: 'John');
when(mockAuthRepository.login(
email: 'test@example.com',
password: 'password',
)).thenAnswer((_) async => user);
expect(
authBloc.stream,
emitsInOrder([AuthLoading(), AuthSuccess(user)]),
);
authBloc.add(LoginRequested(
email: 'test@example.com',
password: 'password',
));
});
});
}
Резюме
Моя архитектура строится на:
- Модульности — каждая feature независима
- Слоистости — presentation → domain → data
- Разделении ответственности — Page, Screen, Widget
- DI (Dependency Injection) — через getIt
- Тестируемости — изолированная бизнес-логика
- Переиспользовании — shared компоненты
Это позволяет быстро разрабатывать, легко масштабировать и безболезненно тестировать код.