← Назад к вопросам

Как организуешь архитектуру виджетов в проекте?

2.0 Middle🔥 221 комментариев
#Архитектура Flutter

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Архитектура виджетов в 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',
      ));
    });
  });
}

Резюме

Моя архитектура строится на:

  1. Модульности — каждая feature независима
  2. Слоистости — presentation → domain → data
  3. Разделении ответственности — Page, Screen, Widget
  4. DI (Dependency Injection) — через getIt
  5. Тестируемости — изолированная бизнес-логика
  6. Переиспользовании — shared компоненты

Это позволяет быстро разрабатывать, легко масштабировать и безболезненно тестировать код.