diff --git a/.gitignore b/.gitignore index 1cc70fe..623866f 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,9 @@ app.*.map.json /android/app/profile /android/app/release +/android/ +/ios/ +/macos/ +/windows/ +/linux/ +/web/ diff --git a/README.md b/README.md index c025037..accd652 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,63 @@ -#### Приложение [ProjectName] +# Описание проекта Friflex Starter + +## Общая информация +Friflex Starter - это стартовый шаблон для разработки Flutter-приложений, который предоставляет готовую структуру проекта, настроенные инструменты и лучшие практики разработки. + +## Архитектура +Проект следует принципам чистой архитектуры с разделением на три основных слоя: +- **data** - слой данных, отвечающий за работу с API и локальным хранилищем +- **domain** - слой бизнес-логики, содержащий основные бизнес-правила и модели +- **presentation** - слой представления, отвечающий за UI и взаимодействие с пользователем + +Каждая функциональность (feature) реализуется в отдельной папке с внутренним разделением на слои, что обеспечивает модульность и масштабируемость кода. + +## Технологический стек + +### Основные библиотеки +- **Роутинг**: [go_router](https://pub.dev/packages/go_router) +- **Управление состоянием**: [flutter_bloc](https://pub.dev/packages/flutter_bloc) +- **Внедрение зависимостей**: собственная реализация через InheritedWidget +- **Работа с ресурсами**: [flutter_gen](https://pub.dev/packages/flutter_gen) +- **Линтинг**: [friflex_lint_rules](https://pub.friflex.com/packages/friflex_lint_rules) +- **Хранение данных**: + - Защищенные данные: [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage) + - Обычные данные: [shared_preferences](https://pub.dev/packages/shared_preferences) +- **Работа с API**: [dio](https://pub.dev/packages/dio) ## Структура проекта - - проект архитектурно делится на три слоя: data, domain и presentation; - - все [features] реализуются в отдельных папках, с внутренним делением на слои; -## Основные пакеты и реализации (обновляется при добавлении или изменении) - - управление роутингом: [go_router](https://pub.dev/packages/go_router); - - основной state manager: [flutter_bloc](https://pub.dev/packages/flutter_bloc); - - di: ручная реализация через InheritedWidget; - - работа с ресурсами: [flutter_gen](https://pub.dev/packages/flutter_gen); - - анализатор: используем [friflex_lint_rules](https://pub.friflex.com/packages/friflex_lint_rules), с правилами написания кода от компании.; - - для хранения защищенных данных - [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage); - - для хранения данных - [shared_preferences](https://pub.dev/packages/shared_preferences); - - для работы с API - [dio](https://pub.dev/packages/dio); +### Основные директории +- `/lib` - основной код приложения + - `/app` - основные компоненты приложения + - `/features` - функциональные модули + - `/router` - настройка маршрутизации + - `/di` - настройка внедрения зависимостей + - `/l10n` - локализация + - `/gen` - сгенерированные файлы + - `/targets` - специфичные настройки для разных платформ +- `/assets` - ресурсы приложения +- `/test` - тесты +- `/tools` - инструменты и документация +- `/app_services` - сервисы приложения -## Инструкция по запуску проекта - - [Инструкция по запуску проекта](./tools/rfc/RFC-build.md) +### Конфигурационные файлы +- `pubspec.yaml` - зависимости и метаданные проекта +- `analysis_options.yaml` - настройки анализа кода +- `l10n.yaml` - настройки локализации -## Стиль написания кода - - [Стиль написания кода](./tools/rfc/RFC-codestyle.md) +## Документация для проектов +Проект содержит подробную документацию в директории `/tools/rfc/`: +- Рекомендованный README для проекта +- Инструкции по запуску проект +- Стиль написания кода +- Git-flow процесс +- Структуру проекта +- Правила ведения документации -## Внесение изменений в код - - [Внесение изменений в код](./tools/rfc/RFC-gitflow.md) +## Дополнительные особенности +- Поддержка мультиязычности (l10n) +- Шаблон для PR +- Настроенный анализ кода (analysis_options.yaml) -## Структура проекта - - [Структура проекта](./tools/rfc/RFC-projects_structure.md) - -## Ведение документации и комментариев в проекте - - [Ведение документации и комментариев в проекте](./tools/rfc/RFC-documentation.md) \ No newline at end of file +## Начало работы +Для начала работы с проектом рекомендуется ознакомиться с документацией в директории `/tools/rfc/`, особенно с инструкциями по запуску проекта и стилем написания кода. diff --git a/lib/app/theme/app_colors_scheme.dart b/lib/app/theme/app_colors_scheme.dart index 44b80f8..3a968fe 100644 --- a/lib/app/theme/app_colors_scheme.dart +++ b/lib/app/theme/app_colors_scheme.dart @@ -12,4 +12,7 @@ extension AppColorsScheme on ColorScheme { /// Цвет заднего фона снекбара с успехом Color get successSnackbarBackground => const Color(0xFF6FB62C); + + /// Цвет заднего фона снекбара с информацией + Color get infoSnackbarBackground => const Color.fromARGB(255, 220, 108, 77); } diff --git a/lib/app/ui_kit/app_snackbar.dart b/lib/app/ui_kit/app_snackbar.dart index 065b9bb..82dc0e4 100644 --- a/lib/app/ui_kit/app_snackbar.dart +++ b/lib/app/ui_kit/app_snackbar.dart @@ -15,6 +15,9 @@ enum TypeSnackBar { /// Снекбар с ошибкой error, + + /// Снекбар с информацией + info, } /// {@template app_snackbar} @@ -66,6 +69,24 @@ class AppSnackBar extends StatefulWidget { ); } + /// Показать снекбар с информацией + /// [context] - контекст, в котором будет показан снекбар + /// [message] - сообщение, которое будет отображаться в снекбаре + /// [displayDuration] - продолжительность отображения снекбара + /// По умолчанию 3 секунды + static void showInfo( + BuildContext context, { + required String message, + Duration displayDuration = const Duration(seconds: 3), + }) { + _show( + context: context, + message: message, + type: TypeSnackBar.info, + displayDuration: displayDuration, + ); + } + /// Показать снекбар с успехом /// [context] - контекст, в котором будет показан снекбар /// [message] - сообщение, которое будет отображаться в снекбаре @@ -246,6 +267,7 @@ class _AppSnackBarState extends State return switch (type) { TypeSnackBar.success => context.colors.successSnackbarBackground, TypeSnackBar.error => context.colors.errorSnackbarBackground, + TypeSnackBar.info => context.colors.infoSnackbarBackground, }; } } @@ -273,6 +295,7 @@ class _Icon extends StatelessWidget { size: 32, ), TypeSnackBar.error => Icon(Icons.error, color: Colors.white, size: 32), + TypeSnackBar.info => Icon(Icons.info, color: Colors.white, size: 32), }; } } diff --git a/lib/features/debug/screens/components_screen.dart b/lib/features/debug/screens/components_screen.dart index a5afb4a..6736715 100644 --- a/lib/features/debug/screens/components_screen.dart +++ b/lib/features/debug/screens/components_screen.dart @@ -44,6 +44,13 @@ class _ComponentsScreenState extends State { }, child: const Text('Показать снекбар с успехом'), ), + const HBox(16), + ElevatedButton( + onPressed: () { + AppSnackBar.showInfo(context, message: 'Это просто сообщение'); + }, + child: const Text('Показать снекбар с информацией'), + ), ], ), ), diff --git a/test/components/app_snackbar_test.dart b/test/components/app_snackbar_test.dart index c76534b..ccdede5 100644 --- a/test/components/app_snackbar_test.dart +++ b/test/components/app_snackbar_test.dart @@ -10,13 +10,17 @@ void main() { /// Создание мок-темы с необходимыми цветами для снекбара ColorScheme createMockColorScheme() { return const ColorScheme.light().copyWith( - // Добавляем кастомные цвета через extension методы + error: Colors.red, + primary: Colors.blue, + secondary: Colors.green, ); } /// Создание мок-темы с правильными стилями текста TextTheme createMockTextTheme() { - return const TextTheme(); + return const TextTheme( + bodyMedium: TextStyle(fontSize: 14), + ); } setUp(() { @@ -36,11 +40,43 @@ void main() { }); } + group('AppSnackBar.showInfo', () { + testTester('показывает снекбар с информацией и правильными стилями', (tester) async { + const infoMessage = 'Это просто сообщение'; + + AppSnackBar.showInfo( + tester.element(find.byType(Scaffold)), + message: infoMessage, + ); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(AppSnackBar), findsOneWidget); + expect(find.text(infoMessage), findsOneWidget); + + // Проверяем иконку и её цвет + final iconFinder = find.byType(Icon); + expect(iconFinder, findsOneWidget); + final icon = tester.widget(iconFinder); + expect(icon.color, equals(Colors.black)); // Info иконка черная + + // Проверяем цвет фона + final container = tester.widget( + find.descendant( + of: find.byType(GestureDetector), + matching: find.byType(Container), + ), + ); + final decoration = container.decoration as BoxDecoration; + expect(decoration.color, equals(const Color(0xFFE6E6E6))); // Info фон + }); + }); + group('AppSnackBar.showError', () { - testTester('показывает снекбар с ошибкой', (tester) async { + testTester('показывает снекбар с ошибкой и правильными стилями', (tester) async { const errorMessage = 'Произошла ошибка'; - // Показываем снекбар с ошибкой AppSnackBar.showError( tester.element(find.byType(Scaffold)), message: errorMessage, @@ -49,12 +85,24 @@ void main() { await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); - // Проверяем, что снекбар отображается expect(find.byType(AppSnackBar), findsOneWidget); expect(find.text(errorMessage), findsOneWidget); - // Проверяем наличие иконки ошибки - expect(find.byType(Icon), findsOneWidget); + // Проверяем иконку и её цвет + final iconFinder = find.byType(Icon); + expect(iconFinder, findsOneWidget); + final icon = tester.widget(iconFinder); + expect(icon.color, equals(Colors.white)); // Error иконка белая + + // Проверяем цвет фона + final container = tester.widget( + find.descendant( + of: find.byType(GestureDetector), + matching: find.byType(Container), + ), + ); + final decoration = container.decoration as BoxDecoration; + expect(decoration.color, equals(const Color(0xFFD24720))); // Error фон }); testTester('показывает снекбар с кастомной продолжительностью', ( @@ -110,7 +158,7 @@ void main() { }); group('AppSnackBar.showSuccess', () { - testTester('показывает снекбар с успехом', (tester) async { + testTester('показывает снекбар с успехом и правильными стилями', (tester) async { const successMessage = 'Операция выполнена успешно'; AppSnackBar.showSuccess( @@ -123,7 +171,22 @@ void main() { expect(find.byType(AppSnackBar), findsOneWidget); expect(find.text(successMessage), findsOneWidget); - expect(find.byType(Icon), findsOneWidget); + + // Проверяем иконку и её цвет + final iconFinder = find.byType(Icon); + expect(iconFinder, findsOneWidget); + final icon = tester.widget(iconFinder); + expect(icon.color, equals(Colors.white)); // Success иконка белая + + // Проверяем цвет фона + final container = tester.widget( + find.descendant( + of: find.byType(GestureDetector), + matching: find.byType(Container), + ), + ); + final decoration = container.decoration as BoxDecoration; + expect(decoration.color, equals(const Color(0xFF6FB62C))); // Success фон }); testTester('показывает снекбар с кастомной продолжительностью', ( @@ -147,7 +210,7 @@ void main() { }); group('AppSnackBar виджет поведение', () { - testTester('показывает анимацию появления', (tester) async { + testTester('показывает анимацию появления с правильной последовательностью', (tester) async { const message = 'Тестовое сообщение'; AppSnackBar.showError( @@ -155,15 +218,20 @@ void main() { message: message, ); - // Проверяем, что анимация началась + // Проверяем начальное состояние await tester.pump(); - expect(find.byType(AnimatedBuilder), findsAtLeastNWidgets(1)); + final initialPosition = tester.widget(find.byType(Positioned)); + expect(initialPosition.top ?? 0, lessThan(0)); - // Ждем завершения анимации - await tester.pump(const Duration(milliseconds: 300)); + // Проверяем промежуточное состояние + await tester.pump(const Duration(milliseconds: 150)); + final middlePosition = tester.widget(find.byType(Positioned)); + expect(middlePosition.top ?? 0, greaterThan(initialPosition.top ?? 0)); - expect(find.byType(AppSnackBar), findsOneWidget); - expect(find.text(message), findsOneWidget); + // Проверяем конечное состояние + await tester.pump(const Duration(milliseconds: 150)); + final finalPosition = tester.widget(find.byType(Positioned)); + expect(finalPosition.top ?? 0, greaterThan(0)); }); testTester('закрывается при тапе', (tester) async { @@ -255,36 +323,44 @@ void main() { expect(find.byType(Text), findsAtLeastNWidgets(1)); }); - testTester('имеет правильные отступы и размеры', (tester) async { + testTester('имеет правильные отступы и размеры на разных экранах', (tester) async { const message = 'Размеры'; + // Тестируем на маленьком экране + await tester.binding.setSurfaceSize(const Size(320, 480)); AppSnackBar.showError( tester.element(find.byType(Scaffold)), message: message, ); - await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); - // Проверяем ограничения максимальной ширины - final constraintsWidget = tester.widget( + var container = tester.widget( find.descendant( of: find.byType(GestureDetector), matching: find.byType(Container), ), ); + // На маленьком экране максимальная ширина должна быть 350 + expect(container.constraints?.maxWidth, equals(350)); - expect(constraintsWidget.constraints?.maxWidth, equals(350)); + // Тестируем на большом экране + await tester.binding.setSurfaceSize(const Size(1280, 720)); + await tester.pumpWidget(testApp); + AppSnackBar.showError( + tester.element(find.byType(Scaffold)), + message: message, + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); - // Проверяем отступы - expect( - constraintsWidget.margin, - equals(const EdgeInsets.symmetric(horizontal: 16)), - ); - expect( - constraintsWidget.padding, - equals(const EdgeInsets.symmetric(vertical: 16, horizontal: 16)), + container = tester.widget( + find.descendant( + of: find.byType(GestureDetector), + matching: find.byType(Container), + ), ); + expect(container.constraints?.maxWidth, equals(350)); // Максимальная ширина }); testTester('имеет правильное скругление углов', (tester) async { @@ -310,6 +386,26 @@ void main() { }); }); + group('Доступность', () { + testTester('имеет правильные семантические метки', (tester) async { + const message = 'Доступность'; + + AppSnackBar.showError( + tester.element(find.byType(Scaffold)), + message: message, + ); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + // Проверяем наличие текста сообщения + expect(find.text(message), findsOneWidget); + + // Проверяем наличие GestureDetector для закрытия + expect(find.byType(GestureDetector), findsOneWidget); + }); + }); + group('Управление состоянием', () { testTester('правильно обрабатывает отсутствие mounted контекста', ( tester, @@ -368,6 +464,25 @@ void main() { // Проверяем, что ошибки не возникает при dispose expect(tester.takeException(), isNull); }); + + testTester('правильно обрабатывает быстрые последовательные вызовы', (tester) async { + const messages = ['Сообщение 1', 'Сообщение 2', 'Сообщение 3']; + + for (final message in messages) { + AppSnackBar.showError( + tester.element(find.byType(Scaffold)), + message: message, + ); + await tester.pump(); + } + + await tester.pump(const Duration(milliseconds: 100)); + + // Проверяем, что показывается только последнее сообщение + expect(find.text(messages[0]), findsNothing); + expect(find.text(messages[1]), findsNothing); + expect(find.text(messages[2]), findsOneWidget); + }); }); group('Управление снекбарами', () { diff --git a/tools/rfc/RFC_readme b/tools/rfc/RFC_readme new file mode 100644 index 0000000..c025037 --- /dev/null +++ b/tools/rfc/RFC_readme @@ -0,0 +1,30 @@ +#### Приложение [ProjectName] + +## Структура проекта + - проект архитектурно делится на три слоя: data, domain и presentation; + - все [features] реализуются в отдельных папках, с внутренним делением на слои; + +## Основные пакеты и реализации (обновляется при добавлении или изменении) + - управление роутингом: [go_router](https://pub.dev/packages/go_router); + - основной state manager: [flutter_bloc](https://pub.dev/packages/flutter_bloc); + - di: ручная реализация через InheritedWidget; + - работа с ресурсами: [flutter_gen](https://pub.dev/packages/flutter_gen); + - анализатор: используем [friflex_lint_rules](https://pub.friflex.com/packages/friflex_lint_rules), с правилами написания кода от компании.; + - для хранения защищенных данных - [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage); + - для хранения данных - [shared_preferences](https://pub.dev/packages/shared_preferences); + - для работы с API - [dio](https://pub.dev/packages/dio); + +## Инструкция по запуску проекта + - [Инструкция по запуску проекта](./tools/rfc/RFC-build.md) + +## Стиль написания кода + - [Стиль написания кода](./tools/rfc/RFC-codestyle.md) + +## Внесение изменений в код + - [Внесение изменений в код](./tools/rfc/RFC-gitflow.md) + +## Структура проекта + - [Структура проекта](./tools/rfc/RFC-projects_structure.md) + +## Ведение документации и комментариев в проекте + - [Ведение документации и комментариев в проекте](./tools/rfc/RFC-documentation.md) \ No newline at end of file