|
| 1 | +# Архитектура UI-слоя (Presentation Layer) |
| 2 | + |
| 3 | +Презентационный слой приложения разработан на базе **Jetpack Compose** и реализует реактивный подход к отображению данных. Управление состоянием разделено на два подхода в зависимости от специфики экрана: использование классического `StateFlow` (для авторизации и онбординга) и прямое использование `mutableStateOf` во ViewModel для минимизации оверхеда при частых обновлениях (списки заметок, редактор). |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## 1. Компоненты UI-слоя |
| 8 | + |
| 9 | +Вся бизнес-логика презентации инкапсулирована во ViewModels, которые внедряются в UI-компоненты с помощью DI-фреймворка **Koin**. |
| 10 | + |
| 11 | +| ViewModel | Тип состояния | Стратегия навигации | Архитектурная роль | |
| 12 | +| :--- | :--- | :--- | :--- | |
| 13 | +| `AuthViewModel` | `StateFlow<AuthUiState>` | Шаги внутри экрана (`AuthScreenStep`) | Управление сессией пользователя (Firebase Auth / Google Sign-In), валидация и обработка ошибок входа. | |
| 14 | +| `OnboardingViewModel` | `MutableStateFlow` / `StateFlow` | Пошаговый интерактивный тур | Подсчет координат целевых элементов (`Rect`), управление подсказками и сохранение флага завершения в `Preferences`. | |
| 15 | +| `NotesViewModel` | `mutableStateOf(NotesUiState)` | Стейт-ориентированная (`NotesUiScreen`) | Главный координатор списков: папки, заметки, поиск, избранное. Управляет навигацией через изменение поля `screen`. | |
| 16 | +| `EditorViewModel` | Раздельные `mutableStateOf` поля | Назад (BackHandler) | Облегченная модель для изоляции состояния конкретной редактируемой заметки (динамический ввод текста, вложения). | |
| 17 | + |
| 18 | +--- |
| 19 | + |
| 20 | +## 2. Схемы управления состоянием и навигации |
| 21 | + |
| 22 | +### Стейт-ориентированная навигация в списках (`NotesViewModel`) |
| 23 | + |
| 24 | +В отличие от классического Jetpack Compose Navigation, переключение между экраном папок (`Directories`) и экраном заметок конкретной папки (`Notes`) происходит декларативно через изменение свойства `uiState.screen` типа `NotesUiScreen`. Это позволяет сохранять контекст данных без сложной передачи аргументов по строковым путям. |
| 25 | + |
| 26 | +```mermaid |
| 27 | +graph LR |
| 28 | + A[DirectoriesScreen] -- Выбор папки / Клик --> B(NotesViewModel.onDirectoryClick) |
| 29 | + B --> C[Обновление uiState.screen = NotesUiScreen.Notes] |
| 30 | + C --> D[Рекомпозиция: NotesScreen] |
| 31 | + D -- Системный BackHandler --> E(NotesViewModel.onBack) |
| 32 | + E --> F[Обновление uiState.screen = NotesUiScreen.Directories] |
| 33 | +``` |
| 34 | + |
| 35 | +###Архитектура потока данных в редакторе (EditorViewModel) |
| 36 | +Для предотвращения лишних рекомпозиций всего экрана при каждом нажатии клавиши (вводе символа в BasicTextField), EditorViewModel не использует единый объект Data Class для всего стейта. Вместо этого ключевые свойства разнесены по атомарным наблюдаемым полям: |
| 37 | + |
| 38 | +```kotlin |
| 39 | +graph TD |
| 40 | + UI[EditorScreen: BasicTextField] -- onValueChange --> VM_Change(EditorViewModel.onContentChange) |
| 41 | + VM_Change --> VM_State[var content: String by mutableStateOf] |
| 42 | + VM_State --> UI_Recompose[Быстрая рекомпозиция только текстового поля] |
| 43 | + |
| 44 | + UI_Save[Кнопка Назад / Автосохранение] --> VM_Build(EditorViewModel.buildUpdatedNote) |
| 45 | + VM_Build --> Domain[NotesUseCases.updateNoteUseCase] |
| 46 | +``` |
| 47 | + |
| 48 | +##3. Спецификация экранов и UI-компонентов |
| 49 | +####Экраны списков (DirectoriesScreen & NotesScreen) |
| 50 | +Особенности реализации: Активно используют LazyColumn со строго типизированными элементами интерфейса (DirectoryItemUi, NoteItemUi). |
| 51 | + |
| 52 | +####Поиск: |
| 53 | +Поисковый запрос обрабатывается реактивно через SearchNotesUseCase. Поле ввода интегрировано с модулем онбординга через кастомный модификатор .onboardingTarget(). |
| 54 | + |
| 55 | +####Контекстные действия: |
| 56 | + Длинный тап на папку или заметку активирует режим выбора/удаления, минуя открытие карточки. |
| 57 | + |
| 58 | +####Экран редактора (EditorScreen) |
| 59 | +#####Компонент ввода: |
| 60 | + Построен на базе BasicTextField с кастомной декорацией (editorPlainTextFieldDecoration), что обеспечивает плавный скролл и адаптацию под экранную клавиатуру. |
| 61 | + |
| 62 | +#####Медиавложения: |
| 63 | + Поддерживает динамическое добавление элементов ContentItem (изображения, файлы, ссылки). Список вложений фильтруется с помощью расширения .withoutTextItems(), отделяя бинарный контент от тела заметки. |
| 64 | + |
| 65 | +#####Перехват системных кнопок: |
| 66 | +На экранах авторизации и редактирования явно объявлен BackHandler, предотвращающий случайное закрытие приложения или потерю несохраненного текста. |
| 67 | + |
| 68 | +##4. Внедрение зависимостей (DI) в AppModule.kt |
| 69 | +Регистрация ViewModels и связывание их с доменным слоем (Use Cases) вынесены в модуль Koin. ViewModels списков и онбординга регистрируются через viewModelOf для автоматического разрешения конструкторов, а AuthViewModel настраивается вручную для явной передачи контекста приложения: |
| 70 | +```kotlin |
| 71 | +val appModule = module { |
| 72 | + // Фабрики Use Cases |
| 73 | + factory { NotesUseCases(...) } |
| 74 | + |
| 75 | + // Презентационный слой |
| 76 | + viewModelOf(::NotesViewModel) |
| 77 | + viewModelOf(::OnboardingViewModel) |
| 78 | + viewModel { |
| 79 | + AuthViewModel( |
| 80 | + firebaseAuth = get(), |
| 81 | + app = androidApplication(), |
| 82 | + appSessionPreferences = get(), |
| 83 | + clearLocalDataOnSignOut = get() |
| 84 | + ) |
| 85 | + } |
| 86 | +} |
| 87 | +``` |
0 commit comments