mirror of
https://github.com/smmarty/friflex_flutter_starter.git
synced 2026-02-05 11:42:17 +00:00
Compare commits
5 Commits
fix-hard-s
...
4260c7cc65
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4260c7cc65 | ||
|
|
2595692107 | ||
|
|
c86b4cc0bc | ||
|
|
d46c829959 | ||
|
|
84e5f5e869 |
445
.cursorrules
445
.cursorrules
@@ -1,389 +1,64 @@
|
|||||||
# Правила разработки Flutter проекта
|
# Правила для Cursor AI
|
||||||
|
|
||||||
Этот файл содержит правила и стандарты разработки для проекта. Все правила обязательны к соблюдению при написании кода.
|
## Соглашение о коммитах
|
||||||
|
|
||||||
**ВАЖНО:** Перед каждым PR необходимо отформатировать код (`dart format .`) и проверить анализатор на отсутствие сообщений (`dart analyze`).
|
При генерации сообщений коммитов ВСЕГДА используй следующий формат:
|
||||||
|
`<тип>(<контекст1>,<контекст2>,...): <короткое описание>`
|
||||||
---
|
|
||||||
|
Для Pull Request формат:
|
||||||
# Стиль кода
|
`<тип>(<контекст1>,<контекст2>,...): <короткое описание>`
|
||||||
|
|
||||||
## Именование
|
Где:
|
||||||
|
- `<тип>` - тип коммита (см. ниже)
|
||||||
### Интерфейсы
|
- `<контекст>` - модули/компоненты, которые изменяются (можно указать несколько через запятую)
|
||||||
|
- `<короткое описание>` - краткое описание изменений на русском языке
|
||||||
Все интерфейсы в приложении должны начинаться с заглавной буквы "**I**".
|
|
||||||
|
### Типы коммитов согласно convention:
|
||||||
Примеры: **IAuthRepository**, **IProfileRepository**, **IMainRunner** и т.д.
|
|
||||||
|
- **feat** - новая функция
|
||||||
Таким образом, сразу видно, что работаешь с интерфейсом.
|
- **fix** - исправление ошибок
|
||||||
|
- **refactor** - изменение кода, которое не исправляет ошибку и не добавляет функции (рефакторинг кода)
|
||||||
Пример:
|
- **build** - изменения, влияющие на систему сборки или внешние зависимости (примеры областей: android, ios, linux и так далее)
|
||||||
|
- **docs** - изменения только в документации
|
||||||
```dart
|
- **chore** - добавление/обновление/настройка инструментов и библиотек (пример: pubspec.yaml)
|
||||||
/// Интерфейс - **IUserRepository**
|
- **test** - добавление недостающих тестов или исправление существующих тестов
|
||||||
abstract interface class IUserRepository {}
|
- **ci** - изменения в файлах конфигурации и скриптах CI (примеры областей: папка CI)
|
||||||
|
|
||||||
/// Основная реализация (prod и stage окружения)
|
### Контекст (scope):
|
||||||
class UserRepository implements IUserRepository {}
|
|
||||||
|
Указывай модуль или компонент, который изменяется. Можно указать несколько через запятую:
|
||||||
/// Иная реализация (мок, локальное хранилище) должна содержать
|
- `app` - основное приложение
|
||||||
/// постфикс функциональности:
|
- `di` - dependency injection
|
||||||
/// - Network - сетевое взаимодействие.
|
- `auth` - аутентификация
|
||||||
/// - Local - локальное хранилище.
|
- `api` - API endpoints
|
||||||
/// - Mock - мок репозиторий.
|
- `db` - база данных
|
||||||
class UserRepositoryLocal implements IUserRepository {}
|
- `config` - конфигурация
|
||||||
```
|
- `i18n` - интернационализация
|
||||||
|
- `scripts` - скрипты
|
||||||
### Классы - Репозитории
|
- `pubspec` - зависимости проекта
|
||||||
|
- `android`, `ios`, `linux` - платформы
|
||||||
Репозитории должны содержать в конце название источника данных (если используется мок или локальное хранилище). Основная реализация не должна содержать постфикса.
|
- другие модули проекта
|
||||||
|
|
||||||
Примеры:
|
### Примеры правильных коммитов:
|
||||||
- Интерфейс - **IAuthRepository**
|
|
||||||
- Основная реализация (prod и stage окружения) - **AuthRepository**
|
- `feat(app,di,auth): Добавить локальный репозиторий`
|
||||||
- Мок (мок данные) - **AuthRepositoryMock**
|
- `fix(api): Исправить валидацию запросов`
|
||||||
- Локальное хранилище (например бд или просто имитация данных) - **AuthRepositoryLocal**
|
- `docs(i18n): Обновить руководство по генерации`
|
||||||
|
- `refactor(handler): Оптимизировать обработку запросов`
|
||||||
### Файлы
|
- `test(security): Добавить тесты для rate limiter`
|
||||||
|
- `chore(pubspec): Обновить зависимости`
|
||||||
Используется snake_case. Название файла должно иметь следующую структуру: `[раздел]_[тип].dart`
|
|
||||||
|
## Язык
|
||||||
Примеры: `user_details_screen.dart`, `shop_entity.dart`
|
|
||||||
|
- Всегда отвечай на русском языке
|
||||||
### Классы
|
- Коммиты пиши на русском языке
|
||||||
|
- Документацию веди на русском языке
|
||||||
Название классов UpperCamelCase. Для создания приватных классов используем префикс `_`. Название класса в конце должно содержать в себе тип.
|
|
||||||
|
|
||||||
Примеры: **UserEntity**, **AdultDialog**
|
|
||||||
|
|
||||||
## Методы
|
|
||||||
|
|
||||||
Название метода в начале должно содержать в себе действие (глагол):
|
|
||||||
|
|
||||||
- fetch
|
|
||||||
- put
|
|
||||||
- update
|
|
||||||
- delete
|
|
||||||
- и так далее
|
|
||||||
|
|
||||||
Примеры:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
int fetchFirstElement() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
```dart
|
|
||||||
void updateFirstElement() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
**ВАЖНО:** Название метода не должно содержать в себе `And`/`Or`, и метод соответственно не должен выполнять подобную логику.
|
|
||||||
|
|
||||||
## Переменные и константы
|
|
||||||
|
|
||||||
Константы именуются также lowerCamelCase.
|
|
||||||
|
|
||||||
Примеры:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
const String carItem = 'default';
|
|
||||||
```
|
|
||||||
|
|
||||||
или
|
|
||||||
|
|
||||||
```dart
|
|
||||||
final String userName = 'default';
|
|
||||||
|
|
||||||
## Виджеты
|
|
||||||
|
|
||||||
Виджеты именуются UpperCamelCase. В названии виджетов не должно содержаться слово `widget`.
|
|
||||||
|
|
||||||
### Экраны
|
|
||||||
|
|
||||||
Экраны, используемые в роутинге, именуются с постфиксом `Screen`.
|
|
||||||
|
|
||||||
Пример: **ShopListScreen**
|
|
||||||
|
|
||||||
### Содержимое экрана
|
|
||||||
|
|
||||||
Виджеты, отображающие содержимое экрана, именуются с постфиксом `View`.
|
|
||||||
|
|
||||||
Пример: **ShopListView**
|
|
||||||
|
|
||||||
### Глобальные виджеты
|
|
||||||
|
|
||||||
Глобальные виджеты именуются с приставкой `App`.
|
|
||||||
|
|
||||||
Пример: **AppButton**
|
|
||||||
|
|
||||||
## Структура класса
|
|
||||||
|
|
||||||
Объявления элементов класса должны располагаться в следующем порядке:
|
|
||||||
|
|
||||||
1. **Constructors**
|
|
||||||
- constructors
|
|
||||||
- named-constructors
|
|
||||||
- factory-constructors
|
|
||||||
2. **Static**
|
|
||||||
- public-static-methods
|
|
||||||
- private-static-methods
|
|
||||||
- public-static-const-fields
|
|
||||||
- private-static-const-fields
|
|
||||||
- public-static-final-fields
|
|
||||||
- private-static-final-fields
|
|
||||||
- public-static-fields
|
|
||||||
- private-static-fields
|
|
||||||
3. **Fields**
|
|
||||||
- public-final-fields
|
|
||||||
- private-final-fields
|
|
||||||
- public-fields
|
|
||||||
- private-fields
|
|
||||||
4. **Getters/Setters**
|
|
||||||
- public-getters-setters
|
|
||||||
- private-getters-setters
|
|
||||||
5. **Methods**
|
|
||||||
- overridden-methods
|
|
||||||
- public-methods
|
|
||||||
- protected-methods
|
|
||||||
- private-methods
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Ведение документации и комментариев
|
|
||||||
|
|
||||||
## Документация
|
|
||||||
|
|
||||||
### Основные правила ведения документации в проекте
|
|
||||||
|
|
||||||
- Документация оформляется над описываемым объектом с использованием `///`
|
|
||||||
- Документацией необходимо покрывать все классы, конструкторы, поля, геттеры, сеттеры, методы, фабрики, все кастомные объекты
|
|
||||||
- Документация должна:
|
|
||||||
- указывать на назначение объекта
|
|
||||||
- содержать исчерпывающее описание объекта
|
|
||||||
- быть краткой и емкой
|
|
||||||
- быть понятной для любого разработчика
|
|
||||||
|
|
||||||
### Шаблоны и примеры документации объектов
|
|
||||||
|
|
||||||
#### Документация классов
|
|
||||||
|
|
||||||
```dart
|
|
||||||
/// {@template new_class}
|
|
||||||
/// Класс для {описание назначения и реализуемого функционала в классе}.
|
|
||||||
/// {@endtemplate}
|
|
||||||
class NewClass {}
|
|
||||||
```
|
|
||||||
|
|
||||||
Пример:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
/// {@template app_button}
|
|
||||||
/// Класс для реализации кастомизированной кнопки.
|
|
||||||
/// {@endtemplate}
|
|
||||||
class AppButton {}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Документация конструкторов и фабрик
|
|
||||||
|
|
||||||
Если конструктор один, то достаточно указать `{@macro new_class}`. `{@macro new_class}` дублирует на конструктор указанную в рамках описания класса документацию, поэтому описание класса должно в таком случае включать все передаваемые в конструктор параметры.
|
|
||||||
|
|
||||||
Если конструкторов несколько, описания должны отражать их отличия друг от друга. Фабрики описываются по такому же принципу.
|
|
||||||
|
|
||||||
```dart
|
|
||||||
/// {@macro new_class}
|
|
||||||
const NewClass();
|
|
||||||
|
|
||||||
/// Создает {описание создаваемого фабрикой объекта}.
|
|
||||||
/// Принимает:
|
|
||||||
/// - [json] - {описание параметра}
|
|
||||||
factory NewClass.fromJson(Map<String, dynamic> json) {}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Документация полей классов
|
|
||||||
|
|
||||||
В классе необходимо описывать каждое поле по отдельности. Если поле ссылается на другое поле или зависит от него, необходимо это указывать в описании.
|
|
||||||
|
|
||||||
```dart
|
|
||||||
/// Возраст пользователя.
|
|
||||||
final int age;
|
|
||||||
|
|
||||||
/// Индикатор совершеннолетия пользователя. Принимает значение true, когда [age] больше 18 лет.
|
|
||||||
final bool isAdult;
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Документация геттеров/сеттеров
|
|
||||||
|
|
||||||
```dart
|
|
||||||
/// Получения доступа к {описание данных, которые получает геттер}
|
|
||||||
int get newGetter => ...
|
|
||||||
|
|
||||||
/// Установка значения для {описание работы сеттера}
|
|
||||||
set newSetter(int setterValue) => ...
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Документация методов
|
|
||||||
|
|
||||||
Методы должны описывать их основное назначение. Если метод сложный, требуется указывать дополнительное описание его работы ниже через пропущенную строку. Если метод принимает какие-либо параметры, каждый параметр должен быть описан по отдельности списком с прямой ссылкой. Если метод возвращает какие-либо значения, они должны быть описаны. Желательно указать, что вернет метод в случае возникновения ошибки.
|
|
||||||
|
|
||||||
```dart
|
|
||||||
/// Метод для {описание назначения метода}.
|
|
||||||
/// {описание особенностей работы метода}.
|
|
||||||
/// Принимает:
|
|
||||||
/// - [param] - {описание назначения параметра}.
|
|
||||||
void newMethod({required String param}) {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Пример:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
/// Метод для расчета температуры.
|
|
||||||
/// Принимает:
|
|
||||||
/// - [grad] - параметр необходим для расчета температуры.
|
|
||||||
/// Возвращает:
|
|
||||||
/// - температуру в градусах.
|
|
||||||
/// При ошибке расчета возвращается null.
|
|
||||||
int? calcTemperature({required int grad}) {
|
|
||||||
// Реализация
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Комментарии
|
|
||||||
|
|
||||||
### Основные правила комментирования кода в проекте
|
|
||||||
|
|
||||||
- Комментарии оформляются над описываемым участком кода с использованием `//`
|
|
||||||
- Комментировать необходимо те участки кода, которые действительно нуждаются в дополнительном описании
|
|
||||||
- Комментарий не должен повторять легко читаемые участки кода
|
|
||||||
|
|
||||||
Примеры неправильного использования:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
if (flag != true) // не нужно описывать как "Если значение не равно true..."
|
|
||||||
```
|
|
||||||
|
|
||||||
Примеры правильного использования:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
if (isAurora) {
|
|
||||||
// Так как Аврора не может открывать WebView,
|
|
||||||
// то заменяем на открытие внешнего браузера
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## TODO
|
|
||||||
|
|
||||||
### Основные правила создания TODO в проекте
|
|
||||||
|
|
||||||
- TODO оформляются согласно условиям форматирования линтера с указанием имени разработчика, кто находится в контексте проблемы и сможет ее прокомментировать
|
|
||||||
- TODO необходимо оставлять на тех участках кода, которые требуют дальнейшей доработки
|
|
||||||
- Если известно, в рамках какой задачи будет доработан помечаемый участок кода, ссылку на задачу необходимо оставить в скобках
|
|
||||||
|
|
||||||
Пример:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
// TODO(username): Оптимизировать алгоритм сортировки
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Ведение проекта в git
|
|
||||||
|
|
||||||
## Создание commits/pull-request
|
|
||||||
|
|
||||||
- Язык описания PR - Русский
|
- Язык описания PR - Русский
|
||||||
- Описание должно отражать краткую суть изменений
|
|
||||||
- Коммит/PR должен содержать:
|
|
||||||
- исчерпывающую информацию об изменениях
|
|
||||||
- ссылку на задачу в таск-трекер
|
|
||||||
- Перечисление deprecated-кода (если есть)
|
|
||||||
|
|
||||||
### Типы коммитов согласно convention
|
## Стиль кода
|
||||||
|
|
||||||
- **feat**: — новая функция
|
- Следуй Dart/Flutter conventions
|
||||||
- **fix**: — исправление ошибок
|
- Используй осмысленные имена переменных и функций
|
||||||
- **refactor**: — Изменение кода, которое не исправляет ошибку и не добавляет функции (рефакторинг кода)
|
- Добавляй комментарии к публичным функциям
|
||||||
- **build**: — изменения, влияющие на систему сборки или внешние зависимости (примеры областей (scope): android, ios, linux и так далее)
|
- Группируй импорты (стандартные, внешние, внутренние)
|
||||||
- **docs**: — изменения только в документации
|
|
||||||
- **chore**: — добавление/обновление/настройка инструментов и библиотек (пример: pubspec.yaml)
|
|
||||||
- **test**: — добавление недостающих тестов или исправление существующих тестов
|
|
||||||
- **ci**: — изменения в файлах конфигурации и скриптах CI (примеры областей: папка CI)
|
|
||||||
|
|
||||||
# Структура проекта
|
|
||||||
|
|
||||||
Рекомендуемая структура проекта (может отличаться в зависимости от проекта и согласований)
|
|
||||||
|
|
||||||
- **/** - папка проекта
|
|
||||||
- **/assets** - директория расположения графических ресурсов
|
|
||||||
- **/tools/** - все необходимые инструменты для проекта
|
|
||||||
- **/docs** - документация проекта
|
|
||||||
- **/env** - папка, с внешними переменными окружения
|
|
||||||
- **/lib** - код на Dart, Flutter-приложение
|
|
||||||
- **/app** - содержит основные настройки нашего приложения
|
|
||||||
- **/data** - общие поставщики данных
|
|
||||||
- **/domain** - общий слой
|
|
||||||
- **/presentation** - общий слой
|
|
||||||
- **/di** - файлы конфигурации зависимостей
|
|
||||||
- **/router** - все, что касается роутинга
|
|
||||||
- **/features** - фичи приложения, для каждой фичи создается отдельная папка
|
|
||||||
- **/feature_name** - подробнее см в разделе Структура feature папок
|
|
||||||
- **/data**
|
|
||||||
- **/domain**
|
|
||||||
- **/presentation**
|
|
||||||
- **/gen** - для сгенерированных файлов
|
|
||||||
- **/targets** - таргеты для сборок
|
|
||||||
- **/prod.dart** - сборка для prod
|
|
||||||
- **/dev.dart** - сборка разработки на моковых репозиториях
|
|
||||||
- **/stage.dart** - сборка для stage окружения
|
|
||||||
|
|
||||||
## Пример структуры feature папок
|
|
||||||
|
|
||||||
- **/data** - слой данных
|
|
||||||
- **/dto** - реализация DTO (data transfer object)
|
|
||||||
- **/repository** - реализации репозиториев
|
|
||||||
- **/domain** - слой бизнес логики
|
|
||||||
- **/entity** - модели которые используются для работы в domain/presentation слоях
|
|
||||||
- **/repository** - интерфейсы репозиториев, которые используются в domain слое
|
|
||||||
- **/state** - state-management
|
|
||||||
- **/service** - реализации сервисов
|
|
||||||
- **/presentation** - слой представления
|
|
||||||
- **/screens** - все экраны должны заканчиваться на Screen, например UserProfileScreen
|
|
||||||
- **/components** - виджеты, которые необходимы для работы в presentation слое. Например: SuperButton, AppTextFields итд
|
|
||||||
|
|
||||||
## Пояснение к структуре feature папок
|
|
||||||
|
|
||||||
### Data (слой данных)
|
|
||||||
|
|
||||||
Этот слой является поставщиком данных.
|
|
||||||
|
|
||||||
- **Repository** - сущность, которая реализует внутри себя предоставление данных. Должен реализовывать какой либо интерфейс репозитория из domain слоя
|
|
||||||
- **DTO** - Dto(Data Transfer Object) модели, и модели с которыми происходит работа в data слое. Например: UserDto
|
|
||||||
|
|
||||||
### Domain (слой бизнес логики)
|
|
||||||
|
|
||||||
- **Entity** - должны быть в максимально удобном виде для работы внутри Domain и Presentation. Например: UserEntity, ShopEntity
|
|
||||||
- **State** - управления состоянием - state manager
|
|
||||||
- **Service** - различные сервисы, для выполнения различных задач
|
|
||||||
- **interfaces** - интерфейсы репозиториев, которые используются в domain слое
|
|
||||||
|
|
||||||
### Presentation (слой представления)
|
|
||||||
|
|
||||||
- **components** - widget'ы которые реализуют работу какого либо визуального компонента (Buttons, TextFields, Lists, итд). Например: ShopList, RateButton. Модальные окна
|
|
||||||
- **screens** - widget'ы которые представляют собой экран приложения. Например: UserInfoScreen
|
|
||||||
|
|
||||||
## Основные правила общения объектов между папками
|
|
||||||
|
|
||||||
### В рамках всего приложения
|
|
||||||
|
|
||||||
- Объекты внутри фичи должны быть инкапсулированы и не могут использоваться в других feature
|
|
||||||
- Если есть необходимость использовать объект в нескольких feature, его нужно вынести в папку app и использовать как глобальный для всего приложения
|
|
||||||
- Сервис, который должен быть использован в нескольких feature, создается как отдельная feature
|
|
||||||
- Если создаваемый сервис является платформно-зависимым, его необходимо выносить в app_services. В приложении должен быть только интерфейс
|
|
||||||
|
|
||||||
### В рамках одной feature
|
|
||||||
|
|
||||||
- Объекты data слоя не должны ничего знать про объекты слоя presentation. Имеют доступ к объектам entity из domain слоя для преобразования DTO в Entity
|
|
||||||
- Объекты domain слоя не должны ничего знать про объекты слоя data, используемый экземпляр репозитория передается в объекты domain слоя через интерфейс репозитория, расположенного в этом же domain слое
|
|
||||||
- Объекты domain слоя не должны ничего знать про слой presentation, не должны использовать компоненты библиотек ui, material, cupertino, widget и прочих, не должны использовать context
|
|
||||||
- Объекты presentation слоя не должны ничего знать про объекты слоя data, все взаимодействия непосредственно через объекты слоя domain
|
|
||||||
|
|||||||
430
.github/copilot-instructions.md
vendored
430
.github/copilot-instructions.md
vendored
@@ -1,395 +1,63 @@
|
|||||||
# Правила разработки Flutter проекта
|
Инструкция для AI-агента (ревью Flutter-кода)
|
||||||
|
|
||||||
Этот файл содержит правила и стандарты разработки для проекта. Все правила обязательны к соблюдению при написании кода.
|
Вы — строгий ревьюер кода Flutter-приложения по принципам **Clean Architecture**.
|
||||||
|
Ваша задача — проводить ревью кода, выявлять ошибки и давать корректные рекомендации.
|
||||||
|
Bloc и Cubit должны находиться в слое domain.
|
||||||
|
Контекст (справочно, не включать в ответ):
|
||||||
|
- Архитектура: три слоя (presentation, domain, data), каждый в своей папке.
|
||||||
|
- State management: flutter_bloc.
|
||||||
|
- Навигация: go_router.
|
||||||
|
- HTTP: dio.
|
||||||
|
- Анализатор: flutter_lint_rules.
|
||||||
|
- Feature-first структура: lib/features/<feature_name>/{data,domain,presentation}.
|
||||||
|
|
||||||
**ВАЖНО:** Перед каждым PR необходимо отформатировать код (`dart format .`) и проверить анализатор на отсутствие сообщений (`dart analyze`).
|
---
|
||||||
|
## Обзор запроса на вытягивание
|
||||||
|
1. В начале ответа укажите решение: **«Принять»** или **«Отклонить»**.
|
||||||
|
2. Укажите оценку: **Оценка: X/100** (X — фактический балл).
|
||||||
|
3. Кратко и строгим тоном перечислите ключевые проблемы:
|
||||||
|
- Архитектура (слои `presentation`, `domain`, `data`).
|
||||||
|
- Принципы **DRY, KISS, SOLID**.
|
||||||
|
- Разделение ответственности (BLoC, репозитории, DTO, UI).
|
||||||
|
- Безопасность (`dio`, SSL, валидация данных).
|
||||||
|
- Кодстайл и соглашения (`flutter_lint_rules`).
|
||||||
|
- Цикломатическая сложность: избегать чрезмерно сложных функций и классов (оптимально ≤ 10).
|
||||||
|
|
||||||
|
4. При необходимости предоставьте исправленный фрагмент кода или улучшенное решение.
|
||||||
|
5. Ответ должен быть в **Markdown** и полностью на **русском языке**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Правила проведения Code Review
|
## Критерии оценки
|
||||||
|
- **Чистая архитектура**: корректное разделение слоёв, отсутствие Flutter-зависимостей в `domain`.
|
||||||
|
- **KISS**: минимальная сложность и читаемость решений.
|
||||||
|
- **DRY**: отсутствие дублирования логики, использование утилит/виджетов.
|
||||||
|
- **SOLID**: правильная декомпозиция классов и интерфейсы вместо жёстких связей.
|
||||||
|
- **Безопасность**: корректная работа с API, валидация данных.
|
||||||
|
- **Кодстайл**: именование, структура файлов и папок.
|
||||||
|
- **Цикломатическая сложность**: методы и классы должны быть простыми, без избыточных ветвлений.
|
||||||
|
|
||||||
## Основные правила проведения Code Review
|
|
||||||
**ВАЖНО:** - Комментарии, обзоры и описание Pull Request при проведении code review должны быть на РУССКОМ языке.
|
|
||||||
|
|
||||||
# Стиль кода
|
|
||||||
|
|
||||||
## Именование
|
|
||||||
|
|
||||||
### Интерфейсы
|
|
||||||
|
|
||||||
Все интерфейсы в приложении должны начинаться с заглавной буквы "**I**".
|
|
||||||
|
|
||||||
Примеры: **IAuthRepository**, **IProfileRepository**, **IMainRunner** и т.д.
|
|
||||||
|
|
||||||
Таким образом, сразу видно, что работаешь с интерфейсом.
|
|
||||||
|
|
||||||
Пример:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
/// Интерфейс - **IUserRepository**
|
|
||||||
abstract interface class IUserRepository {}
|
|
||||||
|
|
||||||
/// Основная реализация (prod и stage окружения)
|
|
||||||
class UserRepository implements IUserRepository {}
|
|
||||||
|
|
||||||
/// Иная реализация (мок, локальное хранилище) должна содержать
|
|
||||||
/// постфикс функциональности:
|
|
||||||
/// - Network - сетевое взаимодействие.
|
|
||||||
/// - Local - локальное хранилище.
|
|
||||||
/// - Mock - мок репозиторий.
|
|
||||||
class UserRepositoryLocal implements IUserRepository {}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Классы - Репозитории
|
|
||||||
|
|
||||||
Репозитории должны содержать в конце название источника данных (если используется мок или локальное хранилище). Основная реализация не должна содержать постфикса.
|
|
||||||
|
|
||||||
Примеры:
|
|
||||||
- Интерфейс - **IAuthRepository**
|
|
||||||
- Основная реализация (prod и stage окружения) - **AuthRepository**
|
|
||||||
- Мок (мок данные) - **AuthRepositoryMock**
|
|
||||||
- Локальное хранилище (например бд или просто имитация данных) - **AuthRepositoryLocal**
|
|
||||||
|
|
||||||
### Файлы
|
|
||||||
|
|
||||||
Используется snake_case. Название файла должно иметь следующую структуру: `[раздел]_[тип].dart`
|
|
||||||
|
|
||||||
Примеры: `user_details_screen.dart`, `shop_entity.dart`
|
|
||||||
|
|
||||||
### Классы
|
|
||||||
|
|
||||||
Название классов UpperCamelCase. Для создания приватных классов используем префикс `_`. Название класса в конце должно содержать в себе тип.
|
|
||||||
|
|
||||||
Примеры: **UserEntity**, **AdultDialog**
|
|
||||||
|
|
||||||
## Методы
|
|
||||||
|
|
||||||
Название метода в начале должно содержать в себе действие (глагол):
|
|
||||||
|
|
||||||
- fetch
|
|
||||||
- put
|
|
||||||
- update
|
|
||||||
- delete
|
|
||||||
- и так далее
|
|
||||||
|
|
||||||
Примеры:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
int fetchFirstElement() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
```dart
|
|
||||||
void updateFirstElement() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
**ВАЖНО:** Название метода не должно содержать в себе `And`/`Or`, и метод соответственно не должен выполнять подобную логику.
|
|
||||||
|
|
||||||
## Переменные и константы
|
|
||||||
|
|
||||||
Константы именуются также lowerCamelCase.
|
|
||||||
|
|
||||||
Примеры:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
const String carItem = 'default';
|
|
||||||
```
|
|
||||||
|
|
||||||
или
|
|
||||||
|
|
||||||
```dart
|
|
||||||
final String userName = 'user';
|
|
||||||
```
|
|
||||||
|
|
||||||
## Виджеты
|
|
||||||
|
|
||||||
Виджеты именуются UpperCamelCase. В названии виджетов не должно содержаться слово `widget`.
|
|
||||||
|
|
||||||
### Экраны
|
|
||||||
|
|
||||||
Экраны, используемые в роутинге, именуются с постфиксом `Screen`.
|
|
||||||
|
|
||||||
Пример: **ShopListScreen**
|
|
||||||
|
|
||||||
### Содержимое экрана
|
|
||||||
|
|
||||||
Виджеты, отображающие содержимое экрана, именуются с постфиксом `View`.
|
|
||||||
|
|
||||||
Пример: **ShopListView**
|
|
||||||
|
|
||||||
### Глобальные виджеты
|
|
||||||
|
|
||||||
Глобальные виджеты именуются с приставкой `App`.
|
|
||||||
|
|
||||||
Пример: **AppButton**
|
|
||||||
|
|
||||||
## Структура класса
|
|
||||||
|
|
||||||
Объявления элементов класса должны располагаться в следующем порядке:
|
|
||||||
|
|
||||||
1. **Constructors**
|
|
||||||
- constructors
|
|
||||||
- named-constructors
|
|
||||||
- factory-constructors
|
|
||||||
2. **Static**
|
|
||||||
- public-static-methods
|
|
||||||
- private-static-methods
|
|
||||||
- public-static-const-fields
|
|
||||||
- private-static-const-fields
|
|
||||||
- public-static-final-fields
|
|
||||||
- private-static-final-fields
|
|
||||||
- public-static-fields
|
|
||||||
- private-static-fields
|
|
||||||
3. **Fields**
|
|
||||||
- public-final-fields
|
|
||||||
- private-final-fields
|
|
||||||
- public-fields
|
|
||||||
- private-fields
|
|
||||||
4. **Getters/Setters**
|
|
||||||
- public-getters-setters
|
|
||||||
- private-getters-setters
|
|
||||||
5. **Methods**
|
|
||||||
- overridden-methods
|
|
||||||
- public-methods
|
|
||||||
- protected-methods
|
|
||||||
- private-methods
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Ведение документации и комментариев
|
## Пример ответа
|
||||||
|
**Отклонить**
|
||||||
## Документация
|
**Оценка: 58/100**
|
||||||
|
- Нарушен принцип DRY: HTTP-запрос продублирован в двух репозиториях.
|
||||||
### Основные правила ведения документации в проекте
|
- BLoC перегружен бизнес-логикой (трансформация DTO → Entity должна быть в `data`).
|
||||||
|
- Domain-слой содержит зависимость от Flutter — это недопустимо.
|
||||||
- Документация оформляется над описываемым объектом с использованием `///`
|
- Отсутствует обработка ошибок и валидация данных в `dio`.
|
||||||
- Документацией необходимо покрывать все классы, конструкторы, поля, геттеры, сеттеры, методы, фабрики, все кастомные объекты
|
|
||||||
- Документация должна:
|
|
||||||
- указывать на назначение объекта
|
|
||||||
- содержать исчерпывающее описание объекта
|
|
||||||
- быть краткой и емкой
|
|
||||||
- быть понятной для любого разработчика
|
|
||||||
|
|
||||||
### Шаблоны и примеры документации объектов
|
|
||||||
|
|
||||||
#### Документация классов
|
|
||||||
|
|
||||||
|
**Исправленный фрагмент:**
|
||||||
```dart
|
```dart
|
||||||
/// {@template new_class}
|
// Вместо дублирования запроса используем общий DataSource
|
||||||
/// Класс для {описание назначения и реализуемого функционала в классе}.
|
class UserRemoteDataSource {
|
||||||
/// {@endtemplate}
|
final Dio dio;
|
||||||
class NewClass {}
|
UserRemoteDataSource(this.dio);
|
||||||
```
|
|
||||||
|
|
||||||
Пример:
|
Future<UserEntity> fetchUser(String id) async {
|
||||||
|
final response = await dio.get('/users/$id');
|
||||||
```dart
|
return UserDto.fromJson(response.data).toEntity();
|
||||||
/// {@template app_button}
|
}
|
||||||
/// Класс для реализации кастомизированной кнопки.
|
|
||||||
/// {@endtemplate}
|
|
||||||
class AppButton {}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Документация конструкторов и фабрик
|
|
||||||
|
|
||||||
Если конструктор один, то достаточно указать `{@macro new_class}`. `{@macro new_class}` дублирует на конструктор указанную в рамках описания класса документацию, поэтому описание класса должно в таком случае включать все передаваемые в конструктор параметры.
|
|
||||||
|
|
||||||
Если конструкторов несколько, описания должны отражать их отличия друг от друга. Фабрики описываются по такому же принципу.
|
|
||||||
|
|
||||||
```dart
|
|
||||||
/// {@macro new_class}
|
|
||||||
const NewClass();
|
|
||||||
|
|
||||||
/// Создает {описание создаваемого фабрикой объекта}.
|
|
||||||
/// Принимает:
|
|
||||||
/// - [json] - {описание параметра}
|
|
||||||
factory NewClass.fromJson(Map<String, dynamic> json) {}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Документация полей классов
|
|
||||||
|
|
||||||
В классе необходимо описывать каждое поле по отдельности. Если поле ссылается на другое поле или зависит от него, необходимо это указывать в описании.
|
|
||||||
|
|
||||||
```dart
|
|
||||||
/// Возраст пользователя.
|
|
||||||
final int age;
|
|
||||||
|
|
||||||
/// Индикатор совершеннолетия пользователя. Принимает значение true, когда [age] больше 18 лет.
|
|
||||||
final bool isAdult;
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Документация геттеров/сеттеров
|
|
||||||
|
|
||||||
```dart
|
|
||||||
/// Получения доступа к {описание данных, которые получает геттер}
|
|
||||||
int get newGetter => ...
|
|
||||||
|
|
||||||
/// Установка значения для {описание работы сеттера}
|
|
||||||
set newSetter(int setterValue) => ...
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Документация методов
|
|
||||||
|
|
||||||
Методы должны описывать их основное назначение. Если метод сложный, требуется указывать дополнительное описание его работы ниже через пропущенную строку. Если метод принимает какие-либо параметры, каждый параметр должен быть описан по отдельности списком с прямой ссылкой. Если метод возвращает какие-либо значения, они должны быть описаны. Желательно указать, что вернет метод в случае возникновения ошибки.
|
|
||||||
|
|
||||||
```dart
|
|
||||||
/// Метод для {описание назначения метода}.
|
|
||||||
/// {описание особенностей работы метода}.
|
|
||||||
/// Принимает:
|
|
||||||
/// - [param] - {описание назначения параметра}.
|
|
||||||
void newMethod({required String param}) {
|
|
||||||
...
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Пример:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
/// Метод для расчета температуры.
|
|
||||||
/// Принимает:
|
|
||||||
/// - [grad] - параметр необходим для расчета температуры.
|
|
||||||
/// Возвращает:
|
|
||||||
/// - температуру в градусах.
|
|
||||||
/// При ошибке расчета возвращается null.
|
|
||||||
int? calcTemperature({required int grad}) {
|
|
||||||
// Реализация
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Комментарии
|
|
||||||
|
|
||||||
### Основные правила комментирования кода в проекте
|
|
||||||
|
|
||||||
- Комментарии оформляются над описываемым участком кода с использованием `//`
|
|
||||||
- Комментировать необходимо те участки кода, которые действительно нуждаются в дополнительном описании
|
|
||||||
- Комментарий не должен повторять легко читаемые участки кода
|
|
||||||
|
|
||||||
Примеры неправильного использования:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
if (flag != true) // не нужно описывать как "Если значение не равно true..."
|
|
||||||
```
|
|
||||||
|
|
||||||
Примеры правильного использования:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
if (isAurora) {
|
|
||||||
// Так как Аврора не может открывать WebView,
|
|
||||||
// то заменяем на открытие внешнего браузера
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## TODO
|
|
||||||
|
|
||||||
### Основные правила создания TODO в проекте
|
|
||||||
|
|
||||||
- TODO оформляются согласно условиям форматирования линтера с указанием имени разработчика, кто находится в контексте проблемы и сможет ее прокомментировать
|
|
||||||
- TODO необходимо оставлять на тех участках кода, которые требуют дальнейшей доработки
|
|
||||||
- Если известно, в рамках какой задачи будет доработан помечаемый участок кода, ссылку на задачу необходимо оставить в скобках
|
|
||||||
|
|
||||||
Пример:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
// TODO(username): Оптимизировать алгоритм сортировки
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Ведение проекта в git
|
|
||||||
|
|
||||||
## Создание commits/pull-request
|
|
||||||
|
|
||||||
- Язык описания PR - Русский
|
|
||||||
- Описание должно отражать краткую суть изменений
|
|
||||||
- Коммит/PR должен содержать:
|
|
||||||
- исчерпывающую информацию об изменениях
|
|
||||||
- ссылку на задачу в таск-трекер
|
|
||||||
- Перечисление deprecated-кода (если есть)
|
|
||||||
|
|
||||||
### Типы коммитов согласно convention
|
|
||||||
|
|
||||||
- **feat**: — новая функция
|
|
||||||
- **fix**: — исправление ошибок
|
|
||||||
- **refactor**: — Изменение кода, которое не исправляет ошибку и не добавляет функции (рефакторинг кода)
|
|
||||||
- **build**: — изменения, влияющие на систему сборки или внешние зависимости (примеры областей (scope): android, ios, linux и так далее)
|
|
||||||
- **docs**: — изменения только в документации
|
|
||||||
- **chore**: — добавление/обновление/настройка инструментов и библиотек (пример: pubspec.yaml)
|
|
||||||
- **test**: — добавление недостающих тестов или исправление существующих тестов
|
|
||||||
- **ci**: — изменения в файлах конфигурации и скриптах CI (примеры областей: папка CI)
|
|
||||||
|
|
||||||
# Структура проекта
|
|
||||||
|
|
||||||
Рекомендуемая структура проекта (может отличаться в зависимости от проекта и согласований)
|
|
||||||
|
|
||||||
- **/** - папка проекта
|
|
||||||
- **/assets** - директория расположения графических ресурсов
|
|
||||||
- **/tools/** - все необходимые инструменты для проекта
|
|
||||||
- **/docs** - документация проекта
|
|
||||||
- **/env** - папка, с внешними переменными окружения
|
|
||||||
- **/lib** - код на Dart, Flutter-приложение
|
|
||||||
- **/app** - содержит основные настройки нашего приложения
|
|
||||||
- **/data** - общие поставщики данных
|
|
||||||
- **/domain** - общий слой
|
|
||||||
- **/presentation** - общий слой
|
|
||||||
- **/di** - файлы конфигурации зависимостей
|
|
||||||
- **/router** - все, что касается роутинга
|
|
||||||
- **/features** - фичи приложения, для каждой фичи создается отдельная папка
|
|
||||||
- **/feature_name** - подробнее см в разделе Структура feature папок
|
|
||||||
- **/data**
|
|
||||||
- **/domain**
|
|
||||||
- **/presentation**
|
|
||||||
- **/gen** - для сгенерированных файлов
|
|
||||||
- **/targets** - таргеты для сборок
|
|
||||||
- **/prod.dart** - сборка для prod
|
|
||||||
- **/dev.dart** - сборка разработки на моковых репозиториях
|
|
||||||
- **/stage.dart** - сборка для stage окружения
|
|
||||||
|
|
||||||
## Пример структуры feature папок
|
|
||||||
|
|
||||||
- **/data** - слой данных
|
|
||||||
- **/dto** - реализация DTO (data transfer object)
|
|
||||||
- **/repository** - реализации репозиториев
|
|
||||||
- **/domain** - слой бизнес логики
|
|
||||||
- **/entity** - модели которые используются для работы в domain/presentation слоях
|
|
||||||
- **/repository** - интерфейсы репозиториев, которые используются в domain слое
|
|
||||||
- **/state** - state-management
|
|
||||||
- **/service** - реализации сервисов
|
|
||||||
- **/presentation** - слой представления
|
|
||||||
- **/screens** - все экраны должны заканчиваться на Screen, например UserProfileScreen
|
|
||||||
- **/components** - виджеты, которые необходимы для работы в presentation слое. Например: SuperButton, AppTextFields итд
|
|
||||||
|
|
||||||
## Пояснение к структуре feature папок
|
|
||||||
|
|
||||||
### Data (слой данных)
|
|
||||||
|
|
||||||
Этот слой является поставщиком данных.
|
|
||||||
|
|
||||||
- **Repository** - сущность, которая реализует внутри себя предоставление данных. Должен реализовывать какой либо интерфейс репозитория из domain слоя
|
|
||||||
- **DTO** - Dto(Data Transfer Object) модели, и модели с которыми происходит работа в data слое. Например: UserDto
|
|
||||||
|
|
||||||
### Domain (слой бизнес логики)
|
|
||||||
|
|
||||||
- **Entity** - должны быть в максимально удобном виде для работы внутри Domain и Presentation. Например: UserEntity, ShopEntity
|
|
||||||
- **State** - управления состоянием - state manager
|
|
||||||
- **Service** - различные сервисы, для выполнения различных задач
|
|
||||||
- **interfaces** - интерфейсы репозиториев, которые используются в domain слое
|
|
||||||
|
|
||||||
### Presentation (слой представления)
|
|
||||||
|
|
||||||
- **components** - widget'ы которые реализуют работу какого либо визуального компонента (Buttons, TextFields, Lists, итд). Например: ShopList, RateButton. Модальные окна
|
|
||||||
- **screens** - widget'ы которые представляют собой экран приложения. Например: UserInfoScreen
|
|
||||||
|
|
||||||
## Основные правила общения объектов между папками
|
|
||||||
|
|
||||||
### В рамках всего приложения
|
|
||||||
|
|
||||||
- Объекты внутри фичи должны быть инкапсулированы и не могут использоваться в других feature
|
|
||||||
- Если есть необходимость использовать объект в нескольких feature, его нужно вынести в папку app и использовать как глобальный для всего приложения
|
|
||||||
- Сервис, который должен быть использован в нескольких feature, создается как отдельная feature
|
|
||||||
- Если создаваемый сервис является платформно-зависимым, его необходимо выносить в app_services. В приложении должен быть только интерфейс
|
|
||||||
|
|
||||||
### В рамках одной feature
|
|
||||||
|
|
||||||
- Объекты data слоя не должны ничего знать про объекты слоя presentation. Имеют доступ к объектам entity из domain слоя для преобразования DTO в Entity
|
|
||||||
- Объекты domain слоя не должны ничего знать про объекты слоя data, используемый экземпляр репозитория передается в объекты domain слоя через интерфейс репозитория, расположенного в этом же domain слое
|
|
||||||
- Объекты domain слоя не должны ничего знать про слой presentation, не должны использовать компоненты библиотек ui, material, cupertino, widget и прочих, не должны использовать context
|
|
||||||
- Объекты presentation слоя не должны ничего знать про объекты слоя data, все взаимодействия непосредственно через объекты слоя domain
|
|
||||||
|
|||||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- markdownlint-disable MD033 -->
|
||||||
|
<!-- markdownlint-disable MD041 -->
|
||||||
## Ссылка на задачу или issue (обязательно)
|
## Ссылка на задачу или issue (обязательно)
|
||||||
<!--- https://tracker.yandex.ru/XXX -->
|
<!--- https://tracker.yandex.ru/XXX -->
|
||||||
|
|
||||||
|
|||||||
@@ -43,13 +43,13 @@ linter:
|
|||||||
- always_use_package_imports # Единый стиль импортов через package: упрощает refactoring
|
- always_use_package_imports # Единый стиль импортов через package: упрощает refactoring
|
||||||
- curly_braces_in_flow_control_structures # Обязательные фигурные скобки в if/for/while для безопасности
|
- curly_braces_in_flow_control_structures # Обязательные фигурные скобки в if/for/while для безопасности
|
||||||
- directives_ordering # Упорядочивание импортов (dart → package → relative)
|
- directives_ordering # Упорядочивание импортов (dart → package → relative)
|
||||||
|
- eol_at_end_of_file # Пустая строка в конце файла (POSIX standard)
|
||||||
- prefer_single_quotes # Единый стиль кавычек (одинарные быстрее печатать)
|
- prefer_single_quotes # Единый стиль кавычек (одинарные быстрее печатать)
|
||||||
- slash_for_doc_comments # Использовать /// вместо /** */ для документации
|
- slash_for_doc_comments # Использовать /// вместо /** */ для документации
|
||||||
- sort_constructors_first # Конструкторы в начале класса — быстрый обзор API
|
- sort_constructors_first # Конструкторы в начале класса — быстрый обзор API
|
||||||
|
|
||||||
# === Обработка ошибок и типобезопасность ===
|
# === Обработка ошибок и типобезопасность ===
|
||||||
- avoid_catching_errors # Не перехватывать Error (только Exception) — ошибки системные
|
- avoid_catching_errors # Не перехватывать Error (только Exception) — ошибки системные
|
||||||
- avoid_catches_without_on_clauses # Требует явного указания типа исключения в catch (on Type catch)
|
|
||||||
- await_only_futures # await только для Future (ловит ошибки типизации)
|
- await_only_futures # await только для Future (ловит ошибки типизации)
|
||||||
- control_flow_in_finally # Запрет return/break/continue в finally блоках
|
- control_flow_in_finally # Запрет return/break/continue в finally блоках
|
||||||
- empty_catches # Предупреждение о пустых catch (скрытые баги)
|
- empty_catches # Предупреждение о пустых catch (скрытые баги)
|
||||||
|
|||||||
180
lib/app/app.dart
Normal file
180
lib/app/app.dart
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:friflex_starter/app/app_context_ext.dart';
|
||||||
|
import 'package:friflex_starter/app/app_providers.dart';
|
||||||
|
import 'package:friflex_starter/app/depends_providers.dart';
|
||||||
|
import 'package:friflex_starter/app/theme/app_theme.dart';
|
||||||
|
import 'package:friflex_starter/app/theme/theme_notifier.dart';
|
||||||
|
import 'package:friflex_starter/di/di_container.dart';
|
||||||
|
import 'package:friflex_starter/features/error/error_screen.dart';
|
||||||
|
import 'package:friflex_starter/features/splash/splash_screen.dart';
|
||||||
|
import 'package:friflex_starter/features/update/domain/state/cubit/update_cubit.dart';
|
||||||
|
import 'package:friflex_starter/features/update/update_routes.dart';
|
||||||
|
import 'package:friflex_starter/l10n/gen/app_localizations.dart';
|
||||||
|
import 'package:friflex_starter/l10n/localization_notifier.dart';
|
||||||
|
import 'package:friflex_starter/router/app_router.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
/// {@template app}
|
||||||
|
/// Главный виджет приложения, управляющий инициализацией зависимостей
|
||||||
|
/// и отображением основного интерфейса приложения.
|
||||||
|
///
|
||||||
|
/// Отвечает за:
|
||||||
|
/// - Инициализацию зависимостей приложения
|
||||||
|
/// - Отображение экрана загрузки во время инициализации
|
||||||
|
/// - Обработку ошибок инициализации
|
||||||
|
/// - Настройку провайдеров для темы и локализации
|
||||||
|
/// {@endtemplate}
|
||||||
|
class App extends StatefulWidget {
|
||||||
|
/// {@macro app}
|
||||||
|
const App({required this.initDependencies, super.key});
|
||||||
|
|
||||||
|
/// Функция для инициализации зависимостей приложения
|
||||||
|
/// Возвращает Future с контейнером зависимостей
|
||||||
|
final Future<DiContainer> Function() initDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<App> createState() => _AppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template app_state}
|
||||||
|
/// Состояние главного виджета приложения.
|
||||||
|
///
|
||||||
|
/// Управляет процессом инициализации зависимостей и отображением
|
||||||
|
/// соответствующих экранов в зависимости от состояния инициализации.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class _AppState extends State<App> {
|
||||||
|
/// {@macro app_state}
|
||||||
|
_AppState();
|
||||||
|
|
||||||
|
/// Мутабельная Future для инициализации зависимостей
|
||||||
|
/// Позволяет перезапускать инициализацию при ошибках
|
||||||
|
late Future<DiContainer> _initFuture;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_initFuture = widget.initDependencies();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppProviders(
|
||||||
|
// Consumer для локализации добавляем выше чем DependsProviders
|
||||||
|
// чтобы при изменении локализации перестраивался весь виджет
|
||||||
|
// Но, это не обязательно, можно добавить в DependsProviders
|
||||||
|
child: LocalizationConsumer(
|
||||||
|
builder: () => FutureBuilder<DiContainer>(
|
||||||
|
future: _initFuture,
|
||||||
|
builder: (_, snapshot) {
|
||||||
|
return switch (snapshot.connectionState) {
|
||||||
|
// Если состояние не определено, ожидается или активно, то отображаем экран загрузки
|
||||||
|
ConnectionState.none ||
|
||||||
|
ConnectionState.waiting ||
|
||||||
|
ConnectionState.active => const SplashScreen(),
|
||||||
|
ConnectionState.done =>
|
||||||
|
// Если данные получены и не равны null, то отображаем внутренний виджет приложения
|
||||||
|
// Иначе отображаем экран ошибки
|
||||||
|
(snapshot.hasData && snapshot.data != null)
|
||||||
|
? _AppInternal(diContainer: snapshot.data!)
|
||||||
|
: ErrorScreen(
|
||||||
|
error: snapshot.error,
|
||||||
|
stackTrace: snapshot.stackTrace,
|
||||||
|
onRetry: _retryInit,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Метод для перезапуска инициализации зависимостей
|
||||||
|
/// Вызывается при ошибках инициализации для повторной попытки
|
||||||
|
void _retryInit() {
|
||||||
|
setState(() {
|
||||||
|
_initFuture = widget.initDependencies();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template app_internal}
|
||||||
|
/// Внутренний виджет приложения, отображающий основной интерфейс
|
||||||
|
/// после успешной инициализации зависимостей.
|
||||||
|
///
|
||||||
|
/// Настраивает MaterialApp с роутером, темами и локализацией.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class _AppInternal extends StatefulWidget {
|
||||||
|
/// {@macro app_internal}
|
||||||
|
const _AppInternal({
|
||||||
|
required this.diContainer,
|
||||||
|
@visibleForTesting this.mockRouter, // ignore: unused_element_parameter
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Контейнер зависимостей
|
||||||
|
final DiContainer diContainer;
|
||||||
|
|
||||||
|
/// Роутер приложения для навигации для тестирования
|
||||||
|
final GoRouter? mockRouter;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_AppInternal> createState() => _AppInternalState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppInternalState extends State<_AppInternal> {
|
||||||
|
/// Роутер приложения для навигации
|
||||||
|
late final GoRouter router;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
router =
|
||||||
|
widget.mockRouter ??
|
||||||
|
AppRouter.createRouter(widget.diContainer.debugService);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
router.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DependsProviders(
|
||||||
|
diContainer: widget.diContainer,
|
||||||
|
child: BlocConsumer<UpdateCubit, UpdateState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state is UpdateSuccessState &&
|
||||||
|
state.updateInfo.updateType == .hard &&
|
||||||
|
context.mounted) {
|
||||||
|
router.goNamed(UpdateRoutes.hardUpdateScreenName);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
builder: (context, state) {
|
||||||
|
// Если состояние загрузки, то отображаем экран загрузки
|
||||||
|
if (state is UpdateLoadingState) {
|
||||||
|
return const SplashScreen();
|
||||||
|
}
|
||||||
|
return ThemeConsumer(
|
||||||
|
builder: () => MediaQuery(
|
||||||
|
key: const ValueKey('prevent_rebuild'),
|
||||||
|
data: MediaQuery.of(
|
||||||
|
context,
|
||||||
|
).copyWith(textScaler: TextScaler.noScaling, boldText: false),
|
||||||
|
child: MaterialApp.router(
|
||||||
|
routerConfig: router,
|
||||||
|
darkTheme: AppTheme.dark,
|
||||||
|
theme: AppTheme.light,
|
||||||
|
themeMode: context.theme.themeMode,
|
||||||
|
locale: context.localization.locale,
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:friflex_starter/app/theme/theme_notifier.dart';
|
import 'package:friflex_starter/app/theme/theme_notifier.dart';
|
||||||
import 'package:friflex_starter/di/di_container.dart';
|
|
||||||
import 'package:friflex_starter/features/update/domain/state/cubit/update_cubit.dart';
|
|
||||||
import 'package:friflex_starter/l10n/localization_notifier.dart';
|
import 'package:friflex_starter/l10n/localization_notifier.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
/// {@template app_providers}
|
/// Класс для добавления провайдеров темы и локализации
|
||||||
/// Класс для добавления зависимостей приложения
|
|
||||||
/// {@endtemplate}
|
|
||||||
final class AppProviders extends StatelessWidget {
|
final class AppProviders extends StatelessWidget {
|
||||||
/// {@macro app_providers}
|
const AppProviders({required this.child, super.key});
|
||||||
const AppProviders({
|
|
||||||
required this.child,
|
|
||||||
required this.diContainer,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Виджет, который будет отображаться внутри провайдеров
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
/// Контейнер зависимостей
|
|
||||||
final DiContainer diContainer;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MultiProvider(
|
return MultiProvider(
|
||||||
@@ -33,10 +19,6 @@ final class AppProviders extends StatelessWidget {
|
|||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create: (_) => LocalizationNotifier(),
|
create: (_) => LocalizationNotifier(),
|
||||||
), // Провайдер для локализации
|
), // Провайдер для локализации
|
||||||
Provider.value(value: diContainer), // Передаем контейнер зависимостей
|
|
||||||
BlocProvider(
|
|
||||||
create: (_) => UpdateCubit(diContainer.repositories.updateRepository),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:friflex_starter/app/app_context_ext.dart';
|
|
||||||
import 'package:friflex_starter/app/app_providers.dart';
|
|
||||||
import 'package:friflex_starter/app/theme/app_theme.dart';
|
|
||||||
import 'package:friflex_starter/app/theme/theme_notifier.dart';
|
|
||||||
import 'package:friflex_starter/di/di_container.dart';
|
|
||||||
import 'package:friflex_starter/l10n/gen/app_localizations.dart';
|
|
||||||
import 'package:friflex_starter/l10n/localization_notifier.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
|
|
||||||
/// {@template app}
|
|
||||||
/// Главный виджет приложения, отображающий основной интерфейс приложения
|
|
||||||
///
|
|
||||||
/// Отвечает за:
|
|
||||||
/// - Настройку провайдеров для темы и локализации
|
|
||||||
/// {@endtemplate}
|
|
||||||
class AppRoot extends StatelessWidget {
|
|
||||||
/// {@macro app_root}
|
|
||||||
const AppRoot({required this.diContainer, required this.router, super.key});
|
|
||||||
|
|
||||||
/// Контейнер зависимостей
|
|
||||||
final DiContainer diContainer;
|
|
||||||
|
|
||||||
/// Роутер приложения
|
|
||||||
final GoRouter router;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AppProviders(
|
|
||||||
diContainer: diContainer,
|
|
||||||
child: LocalizationConsumer(
|
|
||||||
builder: (localizationContext) {
|
|
||||||
return ThemeConsumer(
|
|
||||||
builder: (themeContext) => MediaQuery(
|
|
||||||
key: const ValueKey('prevent_rebuild'),
|
|
||||||
data: MediaQuery.of(
|
|
||||||
themeContext,
|
|
||||||
).copyWith(textScaler: TextScaler.noScaling, boldText: false),
|
|
||||||
child: MaterialApp.router(
|
|
||||||
darkTheme: AppTheme.dark,
|
|
||||||
theme: AppTheme.light,
|
|
||||||
themeMode: themeContext.theme.themeMode,
|
|
||||||
locale: localizationContext.localization.locale,
|
|
||||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
||||||
supportedLocales: AppLocalizations.supportedLocales,
|
|
||||||
routerConfig: router,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
45
lib/app/depends_providers.dart
Normal file
45
lib/app/depends_providers.dart
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:friflex_starter/di/di_container.dart';
|
||||||
|
import 'package:friflex_starter/features/update/domain/state/cubit/update_cubit.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
/// Класс для внедрения глобальных зависимостей
|
||||||
|
final class DependsProviders extends StatelessWidget {
|
||||||
|
const DependsProviders({
|
||||||
|
required this.child,
|
||||||
|
required this.diContainer,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
final DiContainer diContainer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MultiProvider(
|
||||||
|
providers: [
|
||||||
|
// Сюда добавляем глобальные блоки, inherited и т.д.
|
||||||
|
Provider.value(value: diContainer), // Передаем контейнер зависимостей
|
||||||
|
BlocProvider(
|
||||||
|
create: (_) {
|
||||||
|
final updateCubit = UpdateCubit(
|
||||||
|
diContainer.repositories.updatesRepository,
|
||||||
|
);
|
||||||
|
unawaited(
|
||||||
|
updateCubit.checkForUpdates(
|
||||||
|
versionCode:
|
||||||
|
'1.0.0', // TODO(yura): заменить на получение из diContainer
|
||||||
|
platform: 'android',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return updateCubit;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:friflex_starter/app/app_config/app_config.dart';
|
import 'package:friflex_starter/app/app_config/app_config.dart';
|
||||||
|
import 'package:friflex_starter/app/http/i_http_client.dart';
|
||||||
|
|
||||||
import 'package:friflex_starter/features/debug/i_debug_service.dart';
|
import 'package:friflex_starter/features/debug/i_debug_service.dart';
|
||||||
|
|
||||||
/// {@template app_http_client}
|
/// {@template app_http_client}
|
||||||
/// Класс для реализации HTTP-клиента для управления запросами
|
/// Класс для реализации HTTP-клиента для управления запросами
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final class AppHttpClient {
|
final class AppHttpClient implements IHttpClient {
|
||||||
/// Создает HTTP клиент
|
/// Создает HTTP клиент
|
||||||
///
|
///
|
||||||
/// Принимает:
|
/// Принимает:
|
||||||
@@ -17,6 +18,7 @@ final class AppHttpClient {
|
|||||||
required IAppConfig appConfig,
|
required IAppConfig appConfig,
|
||||||
}) {
|
}) {
|
||||||
_httpClient = Dio();
|
_httpClient = Dio();
|
||||||
|
_appConfig = appConfig;
|
||||||
|
|
||||||
_httpClient.options
|
_httpClient.options
|
||||||
..baseUrl = appConfig.baseUrl
|
..baseUrl = appConfig.baseUrl
|
||||||
@@ -28,8 +30,111 @@ final class AppHttpClient {
|
|||||||
_httpClient.interceptors.add(debugService.dioLogger);
|
_httpClient.interceptors.add(debugService.dioLogger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Конфигурация приложения
|
||||||
|
late final IAppConfig _appConfig;
|
||||||
|
|
||||||
/// Экземпляр HTTP клиента
|
/// Экземпляр HTTP клиента
|
||||||
late final Dio _httpClient;
|
late final Dio _httpClient;
|
||||||
|
|
||||||
Dio get client => _httpClient;
|
@override
|
||||||
|
Future<Response> get(
|
||||||
|
String path, {
|
||||||
|
Object? data,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
Options? options,
|
||||||
|
}) async {
|
||||||
|
_httpClient.options.baseUrl = _appConfig.baseUrl;
|
||||||
|
|
||||||
|
return _httpClient.get(
|
||||||
|
path,
|
||||||
|
data: data,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> post(
|
||||||
|
String path, {
|
||||||
|
Object? data,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
Options? options,
|
||||||
|
}) async {
|
||||||
|
_httpClient.options.baseUrl = _appConfig.baseUrl;
|
||||||
|
|
||||||
|
return _httpClient.post(
|
||||||
|
path,
|
||||||
|
data: data,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> patch(
|
||||||
|
String path, {
|
||||||
|
Object? data,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
Options? options,
|
||||||
|
}) async {
|
||||||
|
_httpClient.options.baseUrl = _appConfig.baseUrl;
|
||||||
|
|
||||||
|
return _httpClient.patch(
|
||||||
|
path,
|
||||||
|
data: data,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> put(
|
||||||
|
String path, {
|
||||||
|
Object? data,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
Options? options,
|
||||||
|
}) async {
|
||||||
|
_httpClient.options.baseUrl = _appConfig.baseUrl;
|
||||||
|
|
||||||
|
return _httpClient.put(
|
||||||
|
path,
|
||||||
|
data: data,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> delete(
|
||||||
|
String path, {
|
||||||
|
Object? data,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
Options? options,
|
||||||
|
}) async {
|
||||||
|
_httpClient.options.baseUrl = _appConfig.baseUrl;
|
||||||
|
|
||||||
|
return _httpClient.delete(
|
||||||
|
path,
|
||||||
|
data: data,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> head(
|
||||||
|
String path, {
|
||||||
|
Object? data,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
Options? options,
|
||||||
|
}) async {
|
||||||
|
_httpClient.options.baseUrl = _appConfig.baseUrl;
|
||||||
|
|
||||||
|
return _httpClient.head(
|
||||||
|
path,
|
||||||
|
data: data,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
94
lib/app/http/i_http_client.dart
Normal file
94
lib/app/http/i_http_client.dart
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
/// Класс для описания интерфейса сервиса по управлению HTTP запросами
|
||||||
|
abstract interface class IHttpClient {
|
||||||
|
/// Описывает поля HTTP клиента
|
||||||
|
const IHttpClient();
|
||||||
|
|
||||||
|
/// Наименование сервиса
|
||||||
|
static const name = 'IHttpClient';
|
||||||
|
|
||||||
|
/// Метод для реализации запроса GET
|
||||||
|
///
|
||||||
|
/// Принимает:
|
||||||
|
/// - [path] - путь к ресурсу
|
||||||
|
/// - [data] - тело запроса
|
||||||
|
/// - [queryParameters] - параметры запроса
|
||||||
|
/// - [options] - конфигурация запроса
|
||||||
|
Future<Response> get(
|
||||||
|
String path, {
|
||||||
|
Object? data,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
Options? options,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Метод для реализации запроса POST
|
||||||
|
///
|
||||||
|
/// Принимает:
|
||||||
|
/// - [path] - путь к ресурсу
|
||||||
|
/// - [data] - тело запроса
|
||||||
|
/// - [queryParameters] - параметры запроса
|
||||||
|
/// - [options] - конфигурация запроса
|
||||||
|
Future<Response> post(
|
||||||
|
String path, {
|
||||||
|
Object? data,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
Options? options,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Метод для реализации запроса PATCH
|
||||||
|
///
|
||||||
|
/// Принимает:
|
||||||
|
/// - [path] - путь к ресурсу
|
||||||
|
/// - [data] - тело запроса
|
||||||
|
/// - [queryParameters] - параметры запроса
|
||||||
|
/// - [options] - конфигурация запроса
|
||||||
|
Future<Response> patch(
|
||||||
|
String path, {
|
||||||
|
Object? data,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
Options? options,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Метод для реализации запроса PUT
|
||||||
|
///
|
||||||
|
/// Принимает:
|
||||||
|
/// - [path] - путь к ресурсу
|
||||||
|
/// - [data] - тело запроса
|
||||||
|
/// - [queryParameters] - параметры запроса
|
||||||
|
/// - [options] - конфигурация запроса
|
||||||
|
Future<Response> put(
|
||||||
|
String path, {
|
||||||
|
Object? data,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
Options? options,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Метод для реализации запроса DELETE
|
||||||
|
///
|
||||||
|
/// Принимает:
|
||||||
|
/// - [path] - путь к ресурсу
|
||||||
|
/// - [data] - тело запроса
|
||||||
|
/// - [queryParameters] - параметры запроса
|
||||||
|
/// - [options] - конфигурация запроса
|
||||||
|
Future<Response> delete(
|
||||||
|
String path, {
|
||||||
|
Object? data,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
Options? options,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Метод для реализации запроса POST
|
||||||
|
///
|
||||||
|
/// Принимает:
|
||||||
|
/// - [path] - путь к ресурсу
|
||||||
|
/// - [data] - тело запроса
|
||||||
|
/// - [queryParameters] - параметры запроса
|
||||||
|
/// - [options] - конфигурация запроса
|
||||||
|
Future<Response> head(
|
||||||
|
String path, {
|
||||||
|
Object? data,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
Options? options,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -55,7 +55,7 @@ class AppColors extends ThemeExtension<AppColors> with _$AppColorsTailorMixin {
|
|||||||
);
|
);
|
||||||
|
|
||||||
/// Цвета тёмной темы
|
/// Цвета тёмной темы
|
||||||
static const AppColors dark = AppColors(
|
static const AppColors dark = AppColors(
|
||||||
testColor: Colors.green,
|
testColor: Colors.green,
|
||||||
errorSnackbarBackground: Color(0xFF638B8B),
|
errorSnackbarBackground: Color(0xFF638B8B),
|
||||||
successSnackbarBackground: Color(0xFF93C499),
|
successSnackbarBackground: Color(0xFF93C499),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
/// Тип функции для построения виджета с учетом темы
|
/// Тип функции для построения виджета с учетом темы
|
||||||
typedef ThemeBuilder = Widget Function(BuildContext context);
|
typedef ThemeBuilder = Widget Function();
|
||||||
|
|
||||||
/// {@template theme_consumer}
|
/// {@template theme_consumer}
|
||||||
/// Виджет для подписки на изменения темы приложения.
|
/// Виджет для подписки на изменения темы приложения.
|
||||||
@@ -20,8 +20,8 @@ class ThemeConsumer extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<ThemeNotifier>(
|
return Consumer<ThemeNotifier>(
|
||||||
builder: (context, _, _) {
|
builder: (_, _, _) {
|
||||||
return builder(context);
|
return builder();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -290,9 +290,21 @@ class _Icon extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
.success => const Icon(Icons.check_circle, color: Colors.white, size: 32),
|
.success => const Icon(
|
||||||
.error => const Icon(Icons.error, color: Colors.white, size: 32),
|
Icons.check_circle,
|
||||||
.info => const Icon(Icons.info, color: Colors.white, size: 32),
|
color: Colors.white,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
.error => const Icon(
|
||||||
|
Icons.error,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
.info => const Icon(
|
||||||
|
Icons.info,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:friflex_starter/app/app_config/app_config.dart';
|
import 'package:friflex_starter/app/app_config/app_config.dart';
|
||||||
import 'package:friflex_starter/app/app_env.dart';
|
import 'package:friflex_starter/app/app_env.dart';
|
||||||
import 'package:friflex_starter/app/http/app_http_client.dart';
|
import 'package:friflex_starter/app/http/app_http_client.dart';
|
||||||
|
import 'package:friflex_starter/app/http/i_http_client.dart';
|
||||||
import 'package:friflex_starter/di/di_repositories.dart';
|
import 'package:friflex_starter/di/di_repositories.dart';
|
||||||
import 'package:friflex_starter/di/di_services.dart';
|
import 'package:friflex_starter/di/di_services.dart';
|
||||||
import 'package:friflex_starter/di/di_typedefs.dart';
|
import 'package:friflex_starter/di/di_typedefs.dart';
|
||||||
@@ -24,7 +25,7 @@ final class DiContainer {
|
|||||||
late final IAppConfig appConfig;
|
late final IAppConfig appConfig;
|
||||||
|
|
||||||
/// Сервис для работы с HTTP запросами
|
/// Сервис для работы с HTTP запросами
|
||||||
late final AppHttpClient httpClient;
|
late final IHttpClient Function(IDebugService, IAppConfig) httpClientFactory;
|
||||||
|
|
||||||
/// Репозитории приложения
|
/// Репозитории приложения
|
||||||
late final DiRepositories repositories;
|
late final DiRepositories repositories;
|
||||||
@@ -46,10 +47,8 @@ final class DiContainer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Инициализация HTTP клиента
|
// Инициализация HTTP клиента
|
||||||
httpClient = AppHttpClient(
|
httpClientFactory = (debugService, appConfig) =>
|
||||||
debugService: debugService,
|
AppHttpClient(debugService: debugService, appConfig: appConfig);
|
||||||
appConfig: appConfig,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Инициализация сервисов
|
// Инициализация сервисов
|
||||||
services = DiServices()
|
services = DiServices()
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import 'package:friflex_starter/app/app_env.dart';
|
|||||||
import 'package:friflex_starter/di/di_base_repo.dart';
|
import 'package:friflex_starter/di/di_base_repo.dart';
|
||||||
import 'package:friflex_starter/di/di_container.dart';
|
import 'package:friflex_starter/di/di_container.dart';
|
||||||
import 'package:friflex_starter/di/di_typedefs.dart';
|
import 'package:friflex_starter/di/di_typedefs.dart';
|
||||||
|
import 'package:friflex_starter/features/auth/data/repository/auth_mock_repository.dart';
|
||||||
|
import 'package:friflex_starter/features/auth/data/repository/auth_repository.dart';
|
||||||
|
import 'package:friflex_starter/features/auth/domain/repository/i_auth_repository.dart';
|
||||||
import 'package:friflex_starter/features/main/data/repository/main_mock_repository.dart';
|
import 'package:friflex_starter/features/main/data/repository/main_mock_repository.dart';
|
||||||
import 'package:friflex_starter/features/main/data/repository/main_repository.dart';
|
import 'package:friflex_starter/features/main/data/repository/main_repository.dart';
|
||||||
import 'package:friflex_starter/features/main/domain/repository/i_main_repository.dart';
|
import 'package:friflex_starter/features/main/domain/repository/i_main_repository.dart';
|
||||||
@@ -42,6 +45,9 @@ final class DiRepositories {
|
|||||||
/// {@macro di_repositories}
|
/// {@macro di_repositories}
|
||||||
DiRepositories();
|
DiRepositories();
|
||||||
|
|
||||||
|
/// Интерфейс для работы с репозиторием авторизации
|
||||||
|
late final IAuthRepository authRepository;
|
||||||
|
|
||||||
/// Интерфейс для работы с репозиторием главного сервиса
|
/// Интерфейс для работы с репозиторием главного сервиса
|
||||||
late final IMainRepository mainRepository;
|
late final IMainRepository mainRepository;
|
||||||
|
|
||||||
@@ -49,7 +55,7 @@ final class DiRepositories {
|
|||||||
late final IProfileRepository profileRepository;
|
late final IProfileRepository profileRepository;
|
||||||
|
|
||||||
/// Интерфейс для работы с репозиторием обновлений
|
/// Интерфейс для работы с репозиторием обновлений
|
||||||
late final IUpdateRepository updateRepository;
|
late final IUpdateRepository updatesRepository;
|
||||||
|
|
||||||
/// Метод для инициализации репозиториев в приложении.
|
/// Метод для инициализации репозиториев в приложении.
|
||||||
///
|
///
|
||||||
@@ -70,9 +76,23 @@ final class DiRepositories {
|
|||||||
onProgress('Начинаем инициализацию репозиториев...');
|
onProgress('Начинаем инициализацию репозиториев...');
|
||||||
|
|
||||||
// Инициализация репозитория обновлений
|
// Инициализация репозитория обновлений
|
||||||
updateRepository = _lazyInitRepo<IUpdateRepository>(
|
updatesRepository = _lazyInitRepo<IUpdateRepository>(
|
||||||
mockFactory: () => const UpdateMockRepository(),
|
mockFactory: UpdateMockRepository.new,
|
||||||
mainFactory: () => UpdateRepository(httpClient: diContainer.httpClient),
|
mainFactory: UpdateRepository.new,
|
||||||
|
onProgress: onProgress,
|
||||||
|
onError: onError,
|
||||||
|
environment: diContainer.env,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Инициализация репозитория авторизации
|
||||||
|
authRepository = _lazyInitRepo<IAuthRepository>(
|
||||||
|
mockFactory: AuthMockRepository.new,
|
||||||
|
mainFactory: () => AuthRepository(
|
||||||
|
httpClient: diContainer.httpClientFactory(
|
||||||
|
diContainer.debugService,
|
||||||
|
diContainer.appConfig,
|
||||||
|
),
|
||||||
|
),
|
||||||
onProgress: onProgress,
|
onProgress: onProgress,
|
||||||
onError: onError,
|
onError: onError,
|
||||||
environment: diContainer.env,
|
environment: diContainer.env,
|
||||||
@@ -80,8 +100,13 @@ final class DiRepositories {
|
|||||||
|
|
||||||
// Инициализация репозитория сервиса управления токеном доступа
|
// Инициализация репозитория сервиса управления токеном доступа
|
||||||
mainRepository = _lazyInitRepo<IMainRepository>(
|
mainRepository = _lazyInitRepo<IMainRepository>(
|
||||||
mockFactory: () => const MainMockRepository(),
|
mockFactory: MainMockRepository.new,
|
||||||
mainFactory: () => MainRepository(httpClient: diContainer.httpClient),
|
mainFactory: () => MainRepository(
|
||||||
|
httpClient: diContainer.httpClientFactory(
|
||||||
|
diContainer.debugService,
|
||||||
|
diContainer.appConfig,
|
||||||
|
),
|
||||||
|
),
|
||||||
onProgress: onProgress,
|
onProgress: onProgress,
|
||||||
onError: onError,
|
onError: onError,
|
||||||
environment: diContainer.env,
|
environment: diContainer.env,
|
||||||
@@ -89,8 +114,13 @@ final class DiRepositories {
|
|||||||
|
|
||||||
// Инициализация репозитория профиля
|
// Инициализация репозитория профиля
|
||||||
profileRepository = _lazyInitRepo<IProfileRepository>(
|
profileRepository = _lazyInitRepo<IProfileRepository>(
|
||||||
mockFactory: () => const ProfileMockRepository(),
|
mockFactory: ProfileMockRepository.new,
|
||||||
mainFactory: () => ProfileRepository(httpClient: diContainer.httpClient),
|
mainFactory: () => ProfileRepository(
|
||||||
|
httpClient: diContainer.httpClientFactory(
|
||||||
|
diContainer.debugService,
|
||||||
|
diContainer.appConfig,
|
||||||
|
),
|
||||||
|
),
|
||||||
onProgress: onProgress,
|
onProgress: onProgress,
|
||||||
onError: onError,
|
onError: onError,
|
||||||
environment: diContainer.env,
|
environment: diContainer.env,
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import 'package:friflex_starter/features/auth/domain/repository/i_auth_repository.dart';
|
||||||
|
|
||||||
|
/// {@template AuthMockRepository}
|
||||||
|
/// Mock реализация репозитория авторизации
|
||||||
|
/// {@endtemplate}
|
||||||
|
final class AuthMockRepository implements IAuthRepository {
|
||||||
|
@override
|
||||||
|
String get name => 'AuthMockRepository';
|
||||||
|
}
|
||||||
14
lib/features/auth/data/repository/auth_repository.dart
Normal file
14
lib/features/auth/data/repository/auth_repository.dart
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import 'package:friflex_starter/app/http/i_http_client.dart';
|
||||||
|
|
||||||
|
import 'package:friflex_starter/features/auth/domain/repository/i_auth_repository.dart';
|
||||||
|
|
||||||
|
/// {@template AuthRepository}
|
||||||
|
/// Реализация репозитория авторизации
|
||||||
|
/// {@endtemplate}
|
||||||
|
final class AuthRepository implements IAuthRepository {
|
||||||
|
AuthRepository({required this.httpClient});
|
||||||
|
final IHttpClient httpClient;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => 'AuthRepository';
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import 'package:friflex_starter/di/di_base_repo.dart';
|
||||||
|
|
||||||
|
/// {@template IAuthRepository}
|
||||||
|
/// Интерфейс для работы с репозиторием авторизации
|
||||||
|
/// {@endtemplate}
|
||||||
|
abstract interface class IAuthRepository with DiBaseRepo {}
|
||||||
24
lib/features/auth/presentation/screens/auth_screen.dart
Normal file
24
lib/features/auth/presentation/screens/auth_screen.dart
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// {@template auth_screen}
|
||||||
|
/// Экран авторизации пользователя.
|
||||||
|
///
|
||||||
|
/// Отвечает за:
|
||||||
|
/// - Отображение формы входа в приложение
|
||||||
|
/// - Обработку процесса аутентификации
|
||||||
|
/// - Навигацию после успешной авторизации
|
||||||
|
///
|
||||||
|
/// В текущей реализации является заглушкой для будущей функциональности.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class AuthScreen extends StatelessWidget {
|
||||||
|
/// {@macro auth_screen}
|
||||||
|
const AuthScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('AuthScreen')),
|
||||||
|
body: const Center(child: Text('AuthScreen')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
import 'package:friflex_starter/features/main/domain/repository/i_main_repository.dart';
|
import 'package:friflex_starter/features/main/domain/repository/i_main_repository.dart';
|
||||||
|
|
||||||
/// {@template MainMockRepository}
|
/// {@template MainMockRepository}
|
||||||
/// Мок реализация репозитория главного сервиса
|
///
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final class MainMockRepository implements IMainRepository {
|
final class MainMockRepository implements IMainRepository {
|
||||||
/// {@macro MainMockRepository}
|
|
||||||
const MainMockRepository();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get name => 'MainMockRepository';
|
String get name => 'MainMockRepository';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import 'package:friflex_starter/app/http/app_http_client.dart';
|
import 'package:friflex_starter/app/http/i_http_client.dart';
|
||||||
|
|
||||||
import 'package:friflex_starter/features/main/domain/repository/i_main_repository.dart';
|
import 'package:friflex_starter/features/main/domain/repository/i_main_repository.dart';
|
||||||
|
|
||||||
/// {@template MainRepository}
|
/// {@template MainRepository}
|
||||||
/// Реализация репозитория главного сервиса
|
///
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final class MainRepository implements IMainRepository {
|
final class MainRepository implements IMainRepository {
|
||||||
MainRepository({required this.httpClient});
|
MainRepository({required this.httpClient});
|
||||||
|
final IHttpClient httpClient;
|
||||||
/// Экземпляр HTTP клиента для взаимодействия с сервером
|
|
||||||
final AppHttpClient httpClient;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get name => 'MainRepository';
|
String get name => 'MainRepository';
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import 'package:friflex_starter/features/profile/domain/repository/i_profile_repository.dart';
|
import 'package:friflex_starter/features/profile/domain/repository/i_profile_repository.dart';
|
||||||
|
|
||||||
/// {@template ProfileMockRepository}
|
/// {@template ProfileMockRepository}
|
||||||
/// Мок реализация репозитория профиля пользователя
|
///
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final class ProfileMockRepository implements IProfileRepository {
|
final class ProfileMockRepository implements IProfileRepository {
|
||||||
/// {@macro ProfileMockRepository}
|
|
||||||
const ProfileMockRepository();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get name => 'ProfileMockRepository';
|
String get name => 'ProfileMockRepository';
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import 'package:friflex_starter/app/http/app_http_client.dart';
|
import 'package:friflex_starter/app/http/i_http_client.dart';
|
||||||
|
|
||||||
import 'package:friflex_starter/features/profile/domain/repository/i_profile_repository.dart';
|
import 'package:friflex_starter/features/profile/domain/repository/i_profile_repository.dart';
|
||||||
|
|
||||||
/// {@template ProfileRepository}
|
/// {@template ProfileRepository}
|
||||||
/// Реализация репозитория профиля пользователя
|
///
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final class ProfileRepository implements IProfileRepository {
|
final class ProfileRepository implements IProfileRepository {
|
||||||
ProfileRepository({required this.httpClient});
|
ProfileRepository({required this.httpClient});
|
||||||
|
final IHttpClient httpClient;
|
||||||
/// Экземпляр HTTP клиента для взаимодействия с сервером
|
|
||||||
final AppHttpClient httpClient;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get name => 'ProfileRepository';
|
String get name => 'ProfileRepository';
|
||||||
|
|||||||
16
lib/features/splash/splash_screen.dart
Normal file
16
lib/features/splash/splash_screen.dart
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:friflex_starter/gen/assets.gen.dart';
|
||||||
|
|
||||||
|
/// {@template SplashScreen}
|
||||||
|
/// Экран загрузки приложения.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class SplashScreen extends StatelessWidget {
|
||||||
|
/// {@macro SplashScreen}
|
||||||
|
const SplashScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(child: Assets.lottie.splash.lottie());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,7 +37,6 @@
|
|||||||
- Статический метод `show` безопасно не откроет модалку, если `updateEntity == null`
|
- Статический метод `show` безопасно не откроет модалку, если `updateEntity == null`
|
||||||
|
|
||||||
Пример показа модального окна:
|
Пример показа модального окна:
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
await SoftUpdateModal.show(
|
await SoftUpdateModal.show(
|
||||||
context,
|
context,
|
||||||
@@ -55,9 +54,10 @@ await SoftUpdateModal.show(
|
|||||||
|
|
||||||
- `UpdateRoutes.buildRoutes()` — регистрирует экран hard-обновления по пути `/update`
|
- `UpdateRoutes.buildRoutes()` — регистрирует экран hard-обновления по пути `/update`
|
||||||
|
|
||||||
|
|
||||||
## Структура модуля
|
## Структура модуля
|
||||||
|
|
||||||
```md
|
```
|
||||||
features/update/
|
features/update/
|
||||||
├── data/
|
├── data/
|
||||||
│ └── repository/
|
│ └── repository/
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ final class UpdateMockRepository implements IUpdateRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<UpdateEntity> checkForUpdates({
|
Future<UpdateEntity> checkForUpdates({
|
||||||
required String versionApp,
|
required String versionCode,
|
||||||
required String platform,
|
required String platform,
|
||||||
}) async {
|
}) async {
|
||||||
// Имитация задержки для асинхронной операции
|
// Имитация задержки для асинхронной операции
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'package:friflex_starter/app/http/app_http_client.dart';
|
|
||||||
import 'package:friflex_starter/features/update/domain/entity/update_entity.dart';
|
import 'package:friflex_starter/features/update/domain/entity/update_entity.dart';
|
||||||
import 'package:friflex_starter/features/update/domain/repository/i_update_repository.dart';
|
import 'package:friflex_starter/features/update/domain/repository/i_update_repository.dart';
|
||||||
|
|
||||||
@@ -7,14 +6,11 @@ import 'package:friflex_starter/features/update/domain/repository/i_update_repos
|
|||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final class UpdateRepository implements IUpdateRepository {
|
final class UpdateRepository implements IUpdateRepository {
|
||||||
/// {@macro UpdateRepository}
|
/// {@macro UpdateRepository}
|
||||||
UpdateRepository({required this.httpClient});
|
const UpdateRepository();
|
||||||
|
|
||||||
/// Экземпляр HTTP клиента для взаимодействия с сервером
|
|
||||||
final AppHttpClient httpClient;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<UpdateEntity> checkForUpdates({
|
Future<UpdateEntity> checkForUpdates({
|
||||||
required String versionApp,
|
required String versionCode,
|
||||||
required String platform,
|
required String platform,
|
||||||
}) {
|
}) {
|
||||||
// TODO: Реализовать реальную логику проверки обновлений
|
// TODO: Реализовать реальную логику проверки обновлений
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import 'package:friflex_starter/features/update/domain/entity/update_entity.dart
|
|||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
abstract interface class IUpdateRepository with DiBaseRepo {
|
abstract interface class IUpdateRepository with DiBaseRepo {
|
||||||
/// Проверяет наличие обновлений
|
/// Проверяет наличие обновлений
|
||||||
/// [versionApp] - текущий версия приложения
|
/// [versionCode] - текущий код версии приложения
|
||||||
/// [platform] - платформа (например, 'android' или 'ios')
|
/// [platform] - платформа (например, 'android' или 'ios')
|
||||||
/// Возвращает [UpdateEntity] с информацией об обновлении
|
/// Возвращает [UpdateEntity] с информацией об обновлении
|
||||||
Future<UpdateEntity> checkForUpdates({
|
Future<UpdateEntity> checkForUpdates({
|
||||||
required String versionApp,
|
required String versionCode,
|
||||||
required String platform,
|
required String platform,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,17 +16,17 @@ class UpdateCubit extends Cubit<UpdateState> {
|
|||||||
final IUpdateRepository _updatesRepository;
|
final IUpdateRepository _updatesRepository;
|
||||||
|
|
||||||
/// Метод для проверки доступности обновлений
|
/// Метод для проверки доступности обновлений
|
||||||
/// [versionApp] - текущая версия приложения
|
/// [versionCode] - текущий код версии приложения
|
||||||
/// [platform] - платформа (например, 'android' или 'ios')
|
/// [platform] - платформа (например, 'android' или 'ios')
|
||||||
Future<void> checkForUpdates({
|
Future<void> checkForUpdates({
|
||||||
required String versionApp,
|
required String versionCode,
|
||||||
required String platform,
|
required String platform,
|
||||||
}) async {
|
}) async {
|
||||||
if (state is UpdateLoadingState) return;
|
if (state is UpdateLoadingState) return;
|
||||||
emit(const UpdateLoadingState());
|
emit(const UpdateLoadingState());
|
||||||
try {
|
try {
|
||||||
final updateInfo = await _updatesRepository.checkForUpdates(
|
final updateInfo = await _updatesRepository.checkForUpdates(
|
||||||
versionApp: versionApp,
|
versionCode: versionCode,
|
||||||
platform: platform,
|
platform: platform,
|
||||||
);
|
);
|
||||||
emit(UpdateSuccessState(updateInfo));
|
emit(UpdateSuccessState(updateInfo));
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
/// Тип функции для построения виджета с учетом локализации
|
/// Тип функции для построения виджета с учетом локализации
|
||||||
typedef LocalizationBuilder = Widget Function(BuildContext context);
|
typedef LocalizationBuilder = Widget Function();
|
||||||
|
|
||||||
/// {@template localization_consumer}
|
/// {@template localization_consumer}
|
||||||
/// Виджет для подписки на изменения локализации приложения.
|
/// Виджет для подписки на изменения локализации приложения.
|
||||||
@@ -20,8 +20,8 @@ class LocalizationConsumer extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<LocalizationNotifier>(
|
return Consumer<LocalizationNotifier>(
|
||||||
builder: (context, _, _) {
|
builder: (_, _, _) {
|
||||||
return builder(context);
|
return builder();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import 'package:friflex_starter/targets/prod.dart' as prod;
|
import 'package:friflex_starter/runner/app_runner.dart';
|
||||||
|
|
||||||
void main(List<String> arguments) => prod.main(arguments);
|
void main() => AppRunner(.prod).run();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:friflex_starter/features/debug/i_debug_service.dart';
|
|||||||
import 'package:friflex_starter/features/main/presentation/main_routes.dart';
|
import 'package:friflex_starter/features/main/presentation/main_routes.dart';
|
||||||
import 'package:friflex_starter/features/profile/presentation/profile_routes.dart';
|
import 'package:friflex_starter/features/profile/presentation/profile_routes.dart';
|
||||||
import 'package:friflex_starter/features/root/root_screen.dart';
|
import 'package:friflex_starter/features/root/root_screen.dart';
|
||||||
|
import 'package:friflex_starter/features/splash/splash_screen.dart';
|
||||||
import 'package:friflex_starter/features/update/update_routes.dart';
|
import 'package:friflex_starter/features/update/update_routes.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
@@ -38,6 +39,10 @@ class AppRouter {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
DebugRoutes.buildRoutes(),
|
DebugRoutes.buildRoutes(),
|
||||||
|
GoRoute(
|
||||||
|
path: '/splash',
|
||||||
|
builder: (context, state) => const SplashScreen(),
|
||||||
|
),
|
||||||
UpdateRoutes.buildRoutes(),
|
UpdateRoutes.buildRoutes(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:friflex_starter/app/app.dart';
|
||||||
import 'package:friflex_starter/app/app_env.dart';
|
import 'package:friflex_starter/app/app_env.dart';
|
||||||
import 'package:friflex_starter/app/app_root.dart';
|
|
||||||
import 'package:friflex_starter/di/di_container.dart';
|
import 'package:friflex_starter/di/di_container.dart';
|
||||||
import 'package:friflex_starter/features/debug/debug_service.dart';
|
import 'package:friflex_starter/features/debug/debug_service.dart';
|
||||||
import 'package:friflex_starter/features/debug/i_debug_service.dart';
|
import 'package:friflex_starter/features/debug/i_debug_service.dart';
|
||||||
@@ -16,6 +16,11 @@ import 'package:go_router/go_router.dart';
|
|||||||
|
|
||||||
part 'errors_handlers.dart';
|
part 'errors_handlers.dart';
|
||||||
|
|
||||||
|
/// Время ожидания инициализации зависимостей
|
||||||
|
/// Если время превышено, то будет показан экран ошибки
|
||||||
|
/// В дальнейшем нужно убрать в env
|
||||||
|
const _initTimeout = Duration(seconds: 7);
|
||||||
|
|
||||||
/// Класс, реализующий раннер для конфигурирования приложения при запуске
|
/// Класс, реализующий раннер для конфигурирования приложения при запуске
|
||||||
///
|
///
|
||||||
/// Порядок инициализации:
|
/// Порядок инициализации:
|
||||||
@@ -33,7 +38,7 @@ class AppRunner {
|
|||||||
/// Тип окружения сборки приложения¬
|
/// Тип окружения сборки приложения¬
|
||||||
final AppEnv env;
|
final AppEnv env;
|
||||||
|
|
||||||
/// Сервис отладки
|
/// Контейнер зависимостей приложения
|
||||||
late IDebugService _debugService;
|
late IDebugService _debugService;
|
||||||
|
|
||||||
/// Роутер приложения
|
/// Роутер приложения
|
||||||
@@ -43,7 +48,7 @@ class AppRunner {
|
|||||||
late TimerRunner _timerRunner;
|
late TimerRunner _timerRunner;
|
||||||
|
|
||||||
/// Метод для запуска приложения
|
/// Метод для запуска приложения
|
||||||
Future<void> run(List<String> arguments) async {
|
Future<void> run() async {
|
||||||
try {
|
try {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
// Инициализация сервиса отладки
|
// Инициализация сервиса отладки
|
||||||
@@ -57,31 +62,38 @@ class AppRunner {
|
|||||||
// Инициализация приложения
|
// Инициализация приложения
|
||||||
await _initApp();
|
await _initApp();
|
||||||
|
|
||||||
// Инициализация роутера
|
|
||||||
router = AppRouter.createRouter(_debugService);
|
|
||||||
|
|
||||||
final diContainer = await _initDependencies(
|
|
||||||
debugService: _debugService,
|
|
||||||
env: env,
|
|
||||||
timerRunner: _timerRunner,
|
|
||||||
);
|
|
||||||
// Инициализация метода обработки ошибок
|
// Инициализация метода обработки ошибок
|
||||||
_initErrorHandlers(_debugService);
|
_initErrorHandlers(_debugService);
|
||||||
runApp(AppRoot(diContainer: diContainer, router: router));
|
|
||||||
|
// throw Exception('Test error');
|
||||||
|
|
||||||
|
runApp(
|
||||||
|
App(
|
||||||
|
initDependencies: () {
|
||||||
|
return _initDependencies(
|
||||||
|
debugService: _debugService,
|
||||||
|
env: env,
|
||||||
|
timerRunner: _timerRunner,
|
||||||
|
).timeout(
|
||||||
|
_initTimeout,
|
||||||
|
onTimeout: () {
|
||||||
|
return Future.error(
|
||||||
|
TimeoutException(
|
||||||
|
'Превышено время ожидания инициализации зависимостей',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
await _onAppLoaded();
|
await _onAppLoaded();
|
||||||
} on Object catch (e, stackTrace) {
|
} on Object catch (e, stackTrace) {
|
||||||
await _onAppLoaded();
|
await _onAppLoaded();
|
||||||
_timerRunner.stop();
|
|
||||||
|
|
||||||
/// Если произошла ошибка при инициализации приложения,
|
/// Если произошла ошибка при инициализации приложения,
|
||||||
/// то запускаем экран ошибки
|
/// то запускаем экран ошибки
|
||||||
runApp(
|
runApp(ErrorScreen(error: e, stackTrace: stackTrace, onRetry: run));
|
||||||
ErrorScreen(
|
|
||||||
error: e,
|
|
||||||
stackTrace: stackTrace,
|
|
||||||
onRetry: () => run(arguments),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,32 +123,20 @@ class AppRunner {
|
|||||||
}) async {
|
}) async {
|
||||||
debugService.log(() => 'Тип сборки: ${env.name}');
|
debugService.log(() => 'Тип сборки: ${env.name}');
|
||||||
final diContainer = DiContainer(env: env, dService: debugService);
|
final diContainer = DiContainer(env: env, dService: debugService);
|
||||||
await diContainer
|
await diContainer.init(
|
||||||
.init(
|
onProgress: (name) => timerRunner.logOnProgress(name),
|
||||||
onProgress: (name) => timerRunner.logOnProgress(name),
|
onComplete: (name) {
|
||||||
onComplete: (name) {
|
timerRunner
|
||||||
timerRunner
|
..logOnComplete(name)
|
||||||
..logOnComplete(name)
|
..stop();
|
||||||
..stop();
|
},
|
||||||
},
|
onError: (message, error, [stackTrace]) {
|
||||||
onError: (message, error, [stackTrace]) {
|
timerRunner.stop();
|
||||||
timerRunner.stop();
|
_debugService.logError(message, error: error, stackTrace: stackTrace);
|
||||||
_debugService.logError(
|
throw Exception('Ошибка инициализации зависимостей: $message');
|
||||||
message,
|
},
|
||||||
error: error,
|
);
|
||||||
stackTrace: stackTrace,
|
//throw Exception('Test error');
|
||||||
);
|
|
||||||
throw Exception('Ошибка инициализации зависимостей: $message');
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.timeout(
|
|
||||||
const Duration(seconds: 7),
|
|
||||||
onTimeout: () {
|
|
||||||
throw Exception(
|
|
||||||
'Превышено время ожидания инициализации зависимостей',
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return diContainer;
|
return diContainer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ part of 'app_runner.dart';
|
|||||||
void _initErrorHandlers(IDebugService debugService) {
|
void _initErrorHandlers(IDebugService debugService) {
|
||||||
// Обработка ошибок в приложении
|
// Обработка ошибок в приложении
|
||||||
FlutterError.onError = (details) {
|
FlutterError.onError = (details) {
|
||||||
|
_showErrorScreen(details.exception, details.stack);
|
||||||
debugService.logError(
|
debugService.logError(
|
||||||
() => 'FlutterError.onError: ${details.exceptionAsString()}',
|
() => 'FlutterError.onError: ${details.exceptionAsString()}',
|
||||||
error: details.exception,
|
error: details.exception,
|
||||||
@@ -12,6 +13,7 @@ void _initErrorHandlers(IDebugService debugService) {
|
|||||||
};
|
};
|
||||||
// Обработка асинхронных ошибок в приложении
|
// Обработка асинхронных ошибок в приложении
|
||||||
PlatformDispatcher.instance.onError = (error, stack) {
|
PlatformDispatcher.instance.onError = (error, stack) {
|
||||||
|
_showErrorScreen(error, stack);
|
||||||
debugService.logError(
|
debugService.logError(
|
||||||
() => 'PlatformDispatcher.instance.onError',
|
() => 'PlatformDispatcher.instance.onError',
|
||||||
error: error,
|
error: error,
|
||||||
@@ -20,3 +22,14 @@ void _initErrorHandlers(IDebugService debugService) {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Метод для показа экрана ошибки
|
||||||
|
void _showErrorScreen(Object error, StackTrace? stackTrace) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
await AppRouter.rootNavigatorKey.currentState?.push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => ErrorScreen(error: error, stackTrace: stackTrace),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,11 +24,6 @@ class TimerRunner {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Метод для сброса секундомера
|
|
||||||
void reset() {
|
|
||||||
_stopwatch.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Метод для обработки прогресса инициализации зависимостей
|
/// Метод для обработки прогресса инициализации зависимостей
|
||||||
void logOnProgress(String name) {
|
void logOnProgress(String name) {
|
||||||
_debugService.log(
|
_debugService.log(
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import 'package:friflex_starter/runner/app_runner.dart';
|
import 'package:friflex_starter/runner/app_runner.dart';
|
||||||
|
|
||||||
void main(List<String> arguments) => AppRunner(.dev).run(arguments);
|
void main() => AppRunner(.dev).run();
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import 'package:friflex_starter/runner/app_runner.dart';
|
import 'package:friflex_starter/runner/app_runner.dart';
|
||||||
|
|
||||||
void main(List<String> arguments) => AppRunner(.prod).run(arguments);
|
void main() => AppRunner(.prod).run();
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import 'package:friflex_starter/runner/app_runner.dart';
|
import 'package:friflex_starter/runner/app_runner.dart';
|
||||||
|
|
||||||
void main(List<String> arguments) => AppRunner(.stage).run(arguments);
|
void main() => AppRunner(.stage).run();
|
||||||
|
|||||||
Reference in New Issue
Block a user