mirror of
https://github.com/smmarty/friflex_flutter_starter.git
synced 2025-12-22 01:20:46 +00:00
feat(snackbar): Добавить поддержку информационного снекбара и обновить документацию. Обновлены файлы .gitignore и README.md, добавлены новые цвета и методы для отображения снекбара с информацией.
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -44,3 +44,9 @@ app.*.map.json
|
|||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
|
/android/
|
||||||
|
/ios/
|
||||||
|
/macos/
|
||||||
|
/windows/
|
||||||
|
/linux/
|
||||||
|
/web/
|
||||||
|
|||||||
79
README.md
79
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);
|
- `/lib` - основной код приложения
|
||||||
- основной state manager: [flutter_bloc](https://pub.dev/packages/flutter_bloc);
|
- `/app` - основные компоненты приложения
|
||||||
- di: ручная реализация через InheritedWidget;
|
- `/features` - функциональные модули
|
||||||
- работа с ресурсами: [flutter_gen](https://pub.dev/packages/flutter_gen);
|
- `/router` - настройка маршрутизации
|
||||||
- анализатор: используем [friflex_lint_rules](https://pub.friflex.com/packages/friflex_lint_rules), с правилами написания кода от компании.;
|
- `/di` - настройка внедрения зависимостей
|
||||||
- для хранения защищенных данных - [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage);
|
- `/l10n` - локализация
|
||||||
- для хранения данных - [shared_preferences](https://pub.dev/packages/shared_preferences);
|
- `/gen` - сгенерированные файлы
|
||||||
- для работы с API - [dio](https://pub.dev/packages/dio);
|
- `/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/`, особенно с инструкциями по запуску проекта и стилем написания кода.
|
||||||
|
|
||||||
## Ведение документации и комментариев в проекте
|
|
||||||
- [Ведение документации и комментариев в проекте](./tools/rfc/RFC-documentation.md)
|
|
||||||
|
|||||||
@@ -12,4 +12,7 @@ extension AppColorsScheme on ColorScheme {
|
|||||||
|
|
||||||
/// Цвет заднего фона снекбара с успехом
|
/// Цвет заднего фона снекбара с успехом
|
||||||
Color get successSnackbarBackground => const Color(0xFF6FB62C);
|
Color get successSnackbarBackground => const Color(0xFF6FB62C);
|
||||||
|
|
||||||
|
/// Цвет заднего фона снекбара с информацией
|
||||||
|
Color get infoSnackbarBackground => const Color.fromARGB(255, 220, 108, 77);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ enum TypeSnackBar {
|
|||||||
|
|
||||||
/// Снекбар с ошибкой
|
/// Снекбар с ошибкой
|
||||||
error,
|
error,
|
||||||
|
|
||||||
|
/// Снекбар с информацией
|
||||||
|
info,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// {@template app_snackbar}
|
/// {@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] - контекст, в котором будет показан снекбар
|
/// [context] - контекст, в котором будет показан снекбар
|
||||||
/// [message] - сообщение, которое будет отображаться в снекбаре
|
/// [message] - сообщение, которое будет отображаться в снекбаре
|
||||||
@@ -246,6 +267,7 @@ class _AppSnackBarState extends State<AppSnackBar>
|
|||||||
return switch (type) {
|
return switch (type) {
|
||||||
TypeSnackBar.success => context.colors.successSnackbarBackground,
|
TypeSnackBar.success => context.colors.successSnackbarBackground,
|
||||||
TypeSnackBar.error => context.colors.errorSnackbarBackground,
|
TypeSnackBar.error => context.colors.errorSnackbarBackground,
|
||||||
|
TypeSnackBar.info => context.colors.infoSnackbarBackground,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,6 +295,7 @@ class _Icon extends StatelessWidget {
|
|||||||
size: 32,
|
size: 32,
|
||||||
),
|
),
|
||||||
TypeSnackBar.error => Icon(Icons.error, color: Colors.white, size: 32),
|
TypeSnackBar.error => Icon(Icons.error, color: Colors.white, size: 32),
|
||||||
|
TypeSnackBar.info => Icon(Icons.info, color: Colors.white, size: 32),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,13 @@ class _ComponentsScreenState extends State<ComponentsScreen> {
|
|||||||
},
|
},
|
||||||
child: const Text('Показать снекбар с успехом'),
|
child: const Text('Показать снекбар с успехом'),
|
||||||
),
|
),
|
||||||
|
const HBox(16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
AppSnackBar.showInfo(context, message: 'Это просто сообщение');
|
||||||
|
},
|
||||||
|
child: const Text('Показать снекбар с информацией'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -10,13 +10,17 @@ void main() {
|
|||||||
/// Создание мок-темы с необходимыми цветами для снекбара
|
/// Создание мок-темы с необходимыми цветами для снекбара
|
||||||
ColorScheme createMockColorScheme() {
|
ColorScheme createMockColorScheme() {
|
||||||
return const ColorScheme.light().copyWith(
|
return const ColorScheme.light().copyWith(
|
||||||
// Добавляем кастомные цвета через extension методы
|
error: Colors.red,
|
||||||
|
primary: Colors.blue,
|
||||||
|
secondary: Colors.green,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Создание мок-темы с правильными стилями текста
|
/// Создание мок-темы с правильными стилями текста
|
||||||
TextTheme createMockTextTheme() {
|
TextTheme createMockTextTheme() {
|
||||||
return const TextTheme();
|
return const TextTheme(
|
||||||
|
bodyMedium: TextStyle(fontSize: 14),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setUp(() {
|
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<Icon>(iconFinder);
|
||||||
|
expect(icon.color, equals(Colors.black)); // Info иконка черная
|
||||||
|
|
||||||
|
// Проверяем цвет фона
|
||||||
|
final container = tester.widget<Container>(
|
||||||
|
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', () {
|
group('AppSnackBar.showError', () {
|
||||||
testTester('показывает снекбар с ошибкой', (tester) async {
|
testTester('показывает снекбар с ошибкой и правильными стилями', (tester) async {
|
||||||
const errorMessage = 'Произошла ошибка';
|
const errorMessage = 'Произошла ошибка';
|
||||||
|
|
||||||
// Показываем снекбар с ошибкой
|
|
||||||
AppSnackBar.showError(
|
AppSnackBar.showError(
|
||||||
tester.element(find.byType(Scaffold)),
|
tester.element(find.byType(Scaffold)),
|
||||||
message: errorMessage,
|
message: errorMessage,
|
||||||
@@ -49,12 +85,24 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
// Проверяем, что снекбар отображается
|
|
||||||
expect(find.byType(AppSnackBar), findsOneWidget);
|
expect(find.byType(AppSnackBar), findsOneWidget);
|
||||||
expect(find.text(errorMessage), findsOneWidget);
|
expect(find.text(errorMessage), findsOneWidget);
|
||||||
|
|
||||||
// Проверяем наличие иконки ошибки
|
// Проверяем иконку и её цвет
|
||||||
expect(find.byType(Icon), findsOneWidget);
|
final iconFinder = find.byType(Icon);
|
||||||
|
expect(iconFinder, findsOneWidget);
|
||||||
|
final icon = tester.widget<Icon>(iconFinder);
|
||||||
|
expect(icon.color, equals(Colors.white)); // Error иконка белая
|
||||||
|
|
||||||
|
// Проверяем цвет фона
|
||||||
|
final container = tester.widget<Container>(
|
||||||
|
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('показывает снекбар с кастомной продолжительностью', (
|
testTester('показывает снекбар с кастомной продолжительностью', (
|
||||||
@@ -110,7 +158,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('AppSnackBar.showSuccess', () {
|
group('AppSnackBar.showSuccess', () {
|
||||||
testTester('показывает снекбар с успехом', (tester) async {
|
testTester('показывает снекбар с успехом и правильными стилями', (tester) async {
|
||||||
const successMessage = 'Операция выполнена успешно';
|
const successMessage = 'Операция выполнена успешно';
|
||||||
|
|
||||||
AppSnackBar.showSuccess(
|
AppSnackBar.showSuccess(
|
||||||
@@ -123,7 +171,22 @@ void main() {
|
|||||||
|
|
||||||
expect(find.byType(AppSnackBar), findsOneWidget);
|
expect(find.byType(AppSnackBar), findsOneWidget);
|
||||||
expect(find.text(successMessage), findsOneWidget);
|
expect(find.text(successMessage), findsOneWidget);
|
||||||
expect(find.byType(Icon), findsOneWidget);
|
|
||||||
|
// Проверяем иконку и её цвет
|
||||||
|
final iconFinder = find.byType(Icon);
|
||||||
|
expect(iconFinder, findsOneWidget);
|
||||||
|
final icon = tester.widget<Icon>(iconFinder);
|
||||||
|
expect(icon.color, equals(Colors.white)); // Success иконка белая
|
||||||
|
|
||||||
|
// Проверяем цвет фона
|
||||||
|
final container = tester.widget<Container>(
|
||||||
|
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('показывает снекбар с кастомной продолжительностью', (
|
testTester('показывает снекбар с кастомной продолжительностью', (
|
||||||
@@ -147,7 +210,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('AppSnackBar виджет поведение', () {
|
group('AppSnackBar виджет поведение', () {
|
||||||
testTester('показывает анимацию появления', (tester) async {
|
testTester('показывает анимацию появления с правильной последовательностью', (tester) async {
|
||||||
const message = 'Тестовое сообщение';
|
const message = 'Тестовое сообщение';
|
||||||
|
|
||||||
AppSnackBar.showError(
|
AppSnackBar.showError(
|
||||||
@@ -155,15 +218,20 @@ void main() {
|
|||||||
message: message,
|
message: message,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Проверяем, что анимация началась
|
// Проверяем начальное состояние
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.byType(AnimatedBuilder), findsAtLeastNWidgets(1));
|
final initialPosition = tester.widget<Positioned>(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<Positioned>(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<Positioned>(find.byType(Positioned));
|
||||||
|
expect(finalPosition.top ?? 0, greaterThan(0));
|
||||||
});
|
});
|
||||||
|
|
||||||
testTester('закрывается при тапе', (tester) async {
|
testTester('закрывается при тапе', (tester) async {
|
||||||
@@ -255,36 +323,44 @@ void main() {
|
|||||||
expect(find.byType(Text), findsAtLeastNWidgets(1));
|
expect(find.byType(Text), findsAtLeastNWidgets(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
testTester('имеет правильные отступы и размеры', (tester) async {
|
testTester('имеет правильные отступы и размеры на разных экранах', (tester) async {
|
||||||
const message = 'Размеры';
|
const message = 'Размеры';
|
||||||
|
|
||||||
|
// Тестируем на маленьком экране
|
||||||
|
await tester.binding.setSurfaceSize(const Size(320, 480));
|
||||||
AppSnackBar.showError(
|
AppSnackBar.showError(
|
||||||
tester.element(find.byType(Scaffold)),
|
tester.element(find.byType(Scaffold)),
|
||||||
message: message,
|
message: message,
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
// Проверяем ограничения максимальной ширины
|
var container = tester.widget<Container>(
|
||||||
final constraintsWidget = tester.widget<Container>(
|
|
||||||
find.descendant(
|
find.descendant(
|
||||||
of: find.byType(GestureDetector),
|
of: find.byType(GestureDetector),
|
||||||
matching: find.byType(Container),
|
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));
|
||||||
|
|
||||||
// Проверяем отступы
|
container = tester.widget<Container>(
|
||||||
expect(
|
find.descendant(
|
||||||
constraintsWidget.margin,
|
of: find.byType(GestureDetector),
|
||||||
equals(const EdgeInsets.symmetric(horizontal: 16)),
|
matching: find.byType(Container),
|
||||||
);
|
),
|
||||||
expect(
|
|
||||||
constraintsWidget.padding,
|
|
||||||
equals(const EdgeInsets.symmetric(vertical: 16, horizontal: 16)),
|
|
||||||
);
|
);
|
||||||
|
expect(container.constraints?.maxWidth, equals(350)); // Максимальная ширина
|
||||||
});
|
});
|
||||||
|
|
||||||
testTester('имеет правильное скругление углов', (tester) async {
|
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('Управление состоянием', () {
|
group('Управление состоянием', () {
|
||||||
testTester('правильно обрабатывает отсутствие mounted контекста', (
|
testTester('правильно обрабатывает отсутствие mounted контекста', (
|
||||||
tester,
|
tester,
|
||||||
@@ -368,6 +464,25 @@ void main() {
|
|||||||
// Проверяем, что ошибки не возникает при dispose
|
// Проверяем, что ошибки не возникает при dispose
|
||||||
expect(tester.takeException(), isNull);
|
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('Управление снекбарами', () {
|
group('Управление снекбарами', () {
|
||||||
|
|||||||
30
tools/rfc/RFC_readme
Normal file
30
tools/rfc/RFC_readme
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user