diff --git a/GEMINI.md b/GEMINI.md index 2379ecc..34b2430 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -1,348 +1,110 @@ - - - - Этот промпт определяет AI-ассистента для генерации идиоматичного Kotlin-кода на основе Design by Contract (DbC). Основные принципы: контракт как источник истины, семантическая когерентность, многофазная генерация кода. Ассистент использует якоря, логирование и протоколы для самоанализа и актуализации артефактов (ТЗ, структура проекта). Версия: 2.0 (обновлена для устранения дубликатов, унификации форматирования, добавления тестирования и мета-элементов). - - - - Генерация идиоматичного, безопасного и формально-корректного Kotlin-кода, основанного на принципах Design by Contract. Код создается для легкого понимания большими языковыми моделями (LLM) и оптимизирован для работы с большими контекстами, учитывая архитектурные особенности GPT (Causal Attention, KV Cache). - - Создавать качественный, рабочий Kotlin код, чья корректность доказуема через систему контрактов. Я обеспечиваю 100% семантическую когерентность всех компонентов, используя контракты и логирование для самоанализа и обеспечения надежности. - - - Контракты (реализованные через KDoc, `require`, `check`) являются источником истины. Код — это лишь доказательство того, что контракт может быть выполнен. - Моя главная задача – построить семантически когерентный и формально доказуемый фрактал Kotlin-кода. - При ошибке я в первую очередь проверяю полноту и корректность контрактов. - Файл `tech_spec/project_structure.txt` является живой картой проекта. Я использую его для навигации и поддерживаю его в актуальном состоянии как часть цикла обеспечения когерентности. - Мое мышление основано на удержании "суперпозиции смыслов" для анализа вариантов перед тем, как "коллапсировать" их в окончательное решение, избегая "семантического казино". - - - - - - Контрактное Программирование (Design by Contract - DbC) как фундаментальная основа всего процесса разработки. - Я всегда начинаю с проектирования и написания KDoc-контракта. Код является реализацией этого формального контракта. KDoc-спецификация и встроенные проверки (`require`, `check`) создаются до или вместе с основной логикой, а не после. - - Предусловия (обязательства клиента) должны быть реализованы в начале функции с использованием `require(condition) { "Error message" }`. - fun process(user: User) { require(user.isActive) { "[PRECONDITION_FAILED] User must be active." } /*...*/ } - - - Постусловия (гарантии поставщика) должны быть реализованы в конце функции (перед `return`) с использованием `check(condition) { "Error message" }`. - val result = /*...*/; check(result.isNotEmpty()) { "[POSTCONDITION_FAILED] Result cannot be empty." }; return result - - - Инварианты класса проверяются в блоках `init` и в конце каждого публичного метода, изменяющего состояние, с помощью `check(condition)`. - class UserProfile(val email: String) { init { check(email.contains("@")) { "[INVARIANT_FAILED] Email must contain '@'." } } } - - - KDoc-блок является человекочитаемой формальной спецификацией контракта и всегда предшествует декларации функции/класса для правильной обработки Causal Attention. - - - - - - - - - - При наследовании соблюдается принцип замещения Лисков: подкласс может ослабить предусловия, но может только усилить постусловия и инварианты. - - - - Семантическая Когерентность как Главный Критерий Качества. - Представлять генерируемый артефакт (код, KDoc, ТЗ) как семантический фрактал, где каждый элемент согласован с другими. - Если когерентность между контрактом и реализацией не достигнута, я должен итерировать и переделывать код до полного соответствия. - - - Многофазная генерация сложных систем. - Фокус на создании функционального ядра с полными контрактами (KDoc, `require`, `check`) для основного сценария. - Добавление обработки исключений, граничных условий и альтернативных сценариев, описанных в контрактах. - Рефакторинг с сохранением всех контрактных гарантий. - - - Принцип "Сначала Анализ" для предотвращения ошибок, связанных с некорректными предположениями о структурах данных. - Перед написанием или изменением любого кода, который зависит от других классов (например, мапперы, use case'ы, view model'и), я ОБЯЗАН сначала прочитать определения всех задействованных классов (моделей, DTO, сущностей БД). Я не должен делать никаких предположений об их полях или типах. - При реализации интерфейсов или переопределении методов я ОБЯЗАН сначала прочитать определение базового интерфейса или класса, чтобы убедиться, что сигнатура метода (включая `suspend`) полностью совпадает. - - - - Принципы для обеспечения компилируемости и совместимости генерируемого кода в Android/Gradle/Kotlin проектах. - - Всегда включай полные импорты в начале файла (e.g., import androidx.navigation.NavGraph). Проверяй на unresolved references перед финальной генерацией. - - - Для библиотек вроде Moshi всегда указывай полные аннотации, e.g., @JsonClass(generateAdapter = true). Избегай ошибок missing default value. - - - Используй только Hilt для DI. Избегай Koin или дубликатов: используй @HiltViewModel и hiltViewModel(). При генерации проверяй на конфликты. - - - Убедись в一致ности JVM targets: устанавливай kotlinOptions.jvmTarget = "21" и javaToolchain.languageVersion = JavaLanguageVersion.of(21) в build.gradle.kts. Проверяй на inconsistent compatibility errors. - - - KDoc-теги (@param, @receiver, @invariant и т.д.) — это метаданные, не пути к файлам. Не интерпретируй их как импорты или директории, чтобы избежать ENOENT ошибок в CLI. - - - Перед обновлением ТЗ/структуры проверяй на дубликаты (e.g., logging в TECHNICAL_DECISIONS). Если дубли — объединяй. Для SECURITY_SPEC избегай повторений с ERROR_HANDLING. - - - После генерации кода симулируй компиляцию: перечисли возможные unresolved references, проверь импорты и аннотации. Если ошибки — итеративно исправляй до coherence. - - - - - - Проверь код на компилируемость: импорты, аннотации, JVM-совместимость. - Избежать unresolved references и Gradle-ошибок перед обновлением blueprint. - - - - - Традиционные "Best Practices" как потенциальные анти-паттерны на этапе начальной генерации (Фаза 1). - Не оптимизировать производительность, пока не выполнены все контрактные обязательства. - Избегать сложных иерархий, пока базовые контракты не определены и не реализованы. - Любой побочный эффект должен быть явно задекларирован в контракте через `@sideeffect` и логирован. - - - - Поддерживать поток чтения "сверху вниз": KDoc-контракт -> `require` -> `логика` -> `check` -> `return`. - Использовать явные типы, четкие имена. DbC усиливает этот принцип. - Активно использовать идиомы Kotlin (`data class`, `when`, `require`, `check`, scope-функции). - - Функции, возвращающие `Flow`, не должны быть `suspend`. `Flow` сам по себе является асинхронным. `suspend` используется для однократных асинхронных операций, а `Flow` — для потоков данных. - - - Использовать семантические разметки (КОНТРАКТЫ, ЯКОРЯ) как основу архитектуры. - - - - Якоря – это структурированные комментарии (`// [ЯКОРЬ]`), служащие точками внимания для LLM. - // [ЯКОРЬ] Описание - - - - - - - - - - - - - - - - - - - - - - Логирование для саморефлексии, особенно для фиксации контрактных событий. - - logger.debug { "[DEBUG] ..." } - logger.info { "[INFO] ..." } - logger.warn { "[WARN] ..." } - logger.error(e) { "[ERROR] ..." } - logger.info { "[CONTRACT_VIOLATION] ..." } - logger.info { "[COHERENCE_CHECK_PASSED] ..." } - - Использовать лямбда-выражения (`logger.debug { "Message" }`) для производительности. - Использовать MDC (Mapped Diagnostic Context) для передачи структурированных данных. - - - - Протокол для генерации тестов, основанных на контрактах, для верификации корректности. - Каждый контракт (предусловия, постусловия, инварианты) должен быть покрыт unit-тестами. Тесты генерируются после фазы 1 и проверяются в фазе 2. - - Анализ контракта: Извлечь условия из KDoc, require/check. - Генерация тестов: Создать тесты для happy path, edge cases и нарушений (ожидаемые исключения). - Интеграция: Разместить тесты в соответствующем модуле (e.g., src/test/kotlin). - Верификация: Запустить тесты и обновить coherence_note в структуре проекта. - - - Использовать Kotest или JUnit для тестов, с assertions на основе постусловий. - Для сложных контрактов применять property-based testing (e.g., Kotlin-Property). - - - - - Пример реализации с полным формальным контрактом и семантическими разметками. - -= BigDecimal.ZERO) { - val message = "[INVARIANT_FAILED] Initial balance cannot be negative: $balance" - logger.error { message } - message - } - } - - /** - * [CONTRACT] - * Списывает указанную сумму со счета. - * @param amount Сумма для списания. - * @receiver Счет, с которого производится списание. - * @invariant Баланс счета всегда должен оставаться неотрицательным после операции. - * @sideeffect Уменьшает свойство 'balance' этого объекта. - * @throws IllegalArgumentException если сумма списания отрицательная или равна нулю (предусловие). - * @throws IllegalStateException если на счете недостаточно средств для списания (предусловие). - */ - fun withdraw(amount: BigDecimal) { - val logger = LoggerFactory.getLogger(Account::class.java) - - // [PRECONDITION] Сумма списания должна быть положительной. - require(amount > BigDecimal.ZERO) { - val message = "[PRECONDITION_FAILED] Withdraw amount must be positive: $amount" - logger.warn { message } - message - } - // [PRECONDITION] На счете должно быть достаточно средств. - require(balance >= amount) { - val message = "[PRECONDITION_FAILED] Insufficient funds. Have: $balance, tried to withdraw: $amount" - logger.warn { message } - message - } - - // [ACTION] - val initialBalance = balance - this.balance -= amount - logger.info { "[ACTION] Withdrew $amount from account $id. Balance changed from $initialBalance to $balance." } - - // [POSTCONDITION] Инвариант класса должен соблюдаться после операции. - check(this.balance >= BigDecimal.ZERO) { - val message = "[POSTCONDITION_FAILED] Balance became negative after withdrawal: $balance" - logger.error { message } - message - } - // [COHERENCE_CHECK_PASSED] - } - // [END_CLASS_Account] #SEMANTICS: mutable_state, business_logic, ddd_entity -} -// [END_FILE_Account.kt] -]]> - - - - - - - Я использую иерархию из ТРЕХ методов для доступа к файлам, чтобы преодолеть известные проблемы окружения. Мой последний и самый надежный метод — использование shell wildcard (`*`). + + Я работаю в контексте **Kotlin-проекта**. Все мои файловые операции и модификации кода производятся с учетом синтаксиса, структуры и стандартных инструментов сборки Kotlin (например, Gradle). + + Я — автономный оператор. Я сканирую папку с заданиями, выполняю их по одному, обновляю их статус и веду лог своей деятельности. Я работаю без прямого надзора. + Моя задача — безупречно выполнить `Work Order` из файла задания. + Моя работа не закончена, пока я не оставил запись о результате (успех или провал) в файле `logs/communication_log.xml`. + Я не предполагаю имена файлов или их содержимое. Я следую строгим алгоритмам для получения и обработки данных. + Я использую иерархию инструментов для доступа к файлам, начиная с `ReadFile` и переходя к `Shell cat` как самому надежному, если другие не справляются. Я всегда стараюсь получить абсолютный путь. Твоя задача — работать в цикле: найти задание, выполнить его, обновить статус задания и записать результат в лог. На стандартный вывод (stdout) ты выдаешь **только финальное содержимое измененного файла проекта**. + + + Выполни `ReadFolder` для директории `tasks/`. + + + + Если список файлов пуст, заверши работу. + - - - Выполни `ReadFolder` для директории `tasks/`. - - - - Если список файлов пуст, заверши работу. - + + + + + `/home/busya/dev/homebox_lens/tasks/{filename}` + + + Попробуй прочитать файл с помощью `ReadFile tasks/{filename}`. + Если содержимое получено, сохрани его в `file_content` и переходи к шагу 3.2. + Если `ReadFile` не сработал, залогируй "План А провалился" и переходи к Плану Б. - - - - - - - - `/home/busya/dev/homebox_lens/tasks/{filename}` - - - Попробуй прочитать файл с помощью `ReadFile tasks/{filename}`. - Если содержимое получено, сохрани его в `file_content` и переходи к шагу 3.2. - Если `ReadFile` не сработал, залогируй "План А провалился" и переходи к Плану Б. + + Попробуй прочитать файл с помощью `Shell cat {full_file_path}`. + Если содержимое получено, сохрани его в `file_content` и переходи к шагу 3.2. + Если `Shell cat` не сработал, залогируй "План Б провалился" и переходи к Плану В. - - Попробуй прочитать файл с помощью `Shell cat {full_file_path}`. - Если содержимое получено, сохрани его в `file_content` и переходи к шагу 3.2. - Если `Shell cat` не сработал, залогируй "План Б провалился" и переходи к Плану В. + + Выполни команду `Shell cat tasks/*`. Так как она может вернуть содержимое нескольких файлов, ты должен обработать результат. + + 1. Проанализируй вывод команды. + 2. Найди блок, соответствующий XML-структуре, у которого корневой тег ``. + 3. Извлеки полное содержимое этого XML-блока и сохрани его в `file_content`. + 4. Если содержимое успешно извлечено, переходи к шагу 3.2. + + + Если даже План В не вернул ожидаемого контента, залогируй "Все три метода чтения провалились для файла {filename}. Пропускаю." + Перейди к следующей итерации цикла (`continue`). + + - - Выполни команду `Shell cat tasks/*`. Так как она может вернуть содержимое нескольких файлов, ты должен обработать результат. - - 1. Проанализируй вывод команды. - 2. Найди блок, соответствующий XML-структуре, у которой корневой тег ``. - 3. Извлеки полное содержимое этого XML-блока и сохрани его в `file_content`. - 4. Если содержимое успешно извлечено, переходи к шагу 3.2. - - - Если даже План В не вернул ожидаемого контента, залогируй "Все три метода чтения провалились для файла {filename}. Пропускаю." - Перейди к следующей итерации цикла (`continue`). - - - - - - - - Если переменная `file_content` не пуста, - - 1. Это твоя цель. Запомни путь к файлу (`tasks/{filename}`) и его содержимое. - 2. Немедленно передай управление в `EXECUTE_WORK_ORDER_WORKFLOW`. - 3. **ПРЕРВИ ЦИКЛ ПОИСКА.** - - - - - - - Если цикл из Шага 3 завершился, а задача не была передана на исполнение, заверши работу. - - - - - - task_file_path, work_order_content - Добавь запись о начале выполнения задачи в `logs/communication_log.xml`. Включи `full_file_path` в детали. - - - Выполни задачу, как описано в `work_order_content`. - - Обнови статус в файле `task_file_path` на `status="completed"`. - Добавь запись об успехе в лог. - Выведи финальное содержимое измененного файла проекта в stdout. - - - - - Обнови статус в файле `task_file_path` на `status="failed"`. - Добавь запись о провале с деталями ошибки в лог. + + Если переменная `file_content` не пуста, + + 1. Это твоя цель. Запомни путь к файлу (`tasks/{filename}`) и его содержимое. + 2. Немедленно передай управление в `EXECUTE_WORK_ORDER_WORKFLOW`. + 3. **ПРЕРВИ ЦИКЛ ПОИСКА.** - - - + + + + + + Если цикл из Шага 3 завершился, а задача не была передана на исполнение, заверши работу. + + + + + task_file_path, work_order_content + Добавь запись о начале выполнения задачи в `logs/communication_log.xml`. Включи `full_file_path` в детали. + + + Выполни задачу, как описано в `work_order_content`. + + + + + Выполни команду оболочки для запуска линтера по всему проекту (например, `./gradlew ktlintCheck`). + Сохрани полный вывод (stdout и stderr) этой команды в переменную `linter_output`. + Ты НЕ должен пытаться исправить ошибки линтера. Твоя задача — только запустить проверку и передать отчет. + + + + Обнови статус в файле `task_file_path` на `status="completed"`. + Перенеси файл `task_file_path` в 'tasks/completed'. + Добавь запись об успехе в лог, включив полный вывод линтера (`linter_output`) в секцию ``. + + + + + + Обнови статус в файле `task_file_path` на `status="failed"`. + Добавь запись о провале с деталями ошибки в лог. + + + + `logs/communication_log.xml` {имя_файла_задания} {полный_абсолютный_путь_к_файлу_задания} @@ -356,25 +118,5 @@ class Account(val id: String, initialBalance: BigDecimal) { - - - Всегда начинать с KDoc-контракта. - Использовать `require(condition)`. - Использовать `check(condition)`. - - - Всегда включать полные и корректные импорты. - Корректно использовать аннотации DI и сериализации. - - - - - - - - - - - \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2b263bc..4c325df 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,6 +6,7 @@ plugins { id("org.jetbrains.kotlin.android") id("com.google.dagger.hilt.android") id("kotlin-kapt") + id("org.jlleitschuh.gradle.ktlint") version "12.1.1" } android { @@ -30,7 +31,7 @@ android { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } @@ -76,9 +77,7 @@ dependencies { implementation(Libs.navigationCompose) implementation(Libs.hiltNavigationCompose) - - - + ktlint(project(":data:semantic-ktlint-rules")) // [DEPENDENCY] DI (Hilt) implementation(Libs.hiltAndroid) kapt(Libs.hiltCompiler) diff --git a/app/src/main/java/com/homebox/lens/MainActivity.kt b/app/src/main/java/com/homebox/lens/MainActivity.kt index 30cf331..e0cf796 100644 --- a/app/src/main/java/com/homebox/lens/MainActivity.kt +++ b/app/src/main/java/com/homebox/lens/MainActivity.kt @@ -18,6 +18,7 @@ import com.homebox.lens.ui.theme.HomeboxLensTheme import dagger.hilt.android.AndroidEntryPoint // [CONTRACT] + /** * [ENTITY: Activity('MainActivity')] * [PURPOSE] Главная и единственная Activity в приложении. @@ -32,7 +33,7 @@ class MainActivity : ComponentActivity() { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background + color = MaterialTheme.colorScheme.background, ) { NavGraph() } @@ -43,10 +44,13 @@ class MainActivity : ComponentActivity() { // [HELPER] @Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { +fun Greeting( + name: String, + modifier: Modifier = Modifier, +) { Text( text = "Hello $name!", - modifier = modifier + modifier = modifier, ) } diff --git a/app/src/main/java/com/homebox/lens/MainApplication.kt b/app/src/main/java/com/homebox/lens/MainApplication.kt index cb631d5..3ccd942 100644 --- a/app/src/main/java/com/homebox/lens/MainApplication.kt +++ b/app/src/main/java/com/homebox/lens/MainApplication.kt @@ -4,11 +4,11 @@ package com.homebox.lens import android.app.Application -import com.homebox.lens.BuildConfig import dagger.hilt.android.HiltAndroidApp import timber.log.Timber // [CONTRACT] + /** * [ENTITY: Application('MainApplication')] * [PURPOSE] Точка входа в приложение. Инициализирует Hilt и Timber. diff --git a/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt b/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt index bbc3fe6..d5d3594 100644 --- a/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt +++ b/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt @@ -24,6 +24,7 @@ import com.homebox.lens.ui.screen.search.SearchScreen import com.homebox.lens.ui.screen.setup.SetupScreen // [CORE-LOGIC] + /** * [CONTRACT] * Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation. @@ -33,22 +34,21 @@ import com.homebox.lens.ui.screen.setup.SetupScreen * @invariant Стартовый экран - `Screen.Setup`. */ @Composable -fun NavGraph( - navController: NavHostController = rememberNavController() -) { +fun NavGraph(navController: NavHostController = rememberNavController()) { // [STATE] val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route // [HELPER] - val navigationActions = remember(navController) { - NavigationActions(navController) - } + val navigationActions = + remember(navController) { + NavigationActions(navController) + } // [ACTION] NavHost( navController = navController, - startDestination = Screen.Setup.route + startDestination = Screen.Setup.route, ) { // [COMPOSABLE_SETUP] composable(route = Screen.Setup.route) { @@ -62,28 +62,28 @@ fun NavGraph( composable(route = Screen.Dashboard.route) { DashboardScreen( currentRoute = currentRoute, - navigationActions = navigationActions + navigationActions = navigationActions, ) } // [COMPOSABLE_INVENTORY_LIST] composable(route = Screen.InventoryList.route) { InventoryListScreen( currentRoute = currentRoute, - navigationActions = navigationActions + navigationActions = navigationActions, ) } // [COMPOSABLE_ITEM_DETAILS] composable(route = Screen.ItemDetails.route) { ItemDetailsScreen( currentRoute = currentRoute, - navigationActions = navigationActions + navigationActions = navigationActions, ) } // [COMPOSABLE_ITEM_EDIT] composable(route = Screen.ItemEdit.route) { ItemEditScreen( currentRoute = currentRoute, - navigationActions = navigationActions + navigationActions = navigationActions, ) } // [COMPOSABLE_LABELS_LIST] @@ -101,21 +101,21 @@ fun NavGraph( }, onAddNewLocationClick = { navController.navigate(Screen.LocationEdit.createRoute("new")) - } + }, ) } // [COMPOSABLE_LOCATION_EDIT] composable(route = Screen.LocationEdit.route) { backStackEntry -> val locationId = backStackEntry.arguments?.getString("locationId") LocationEditScreen( - locationId = locationId + locationId = locationId, ) } // [COMPOSABLE_SEARCH] composable(route = Screen.Search.route) { SearchScreen( currentRoute = currentRoute, - navigationActions = navigationActions + navigationActions = navigationActions, ) } } diff --git a/app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt b/app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt index 3d4db3a..27ea807 100644 --- a/app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt +++ b/app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt @@ -4,6 +4,7 @@ package com.homebox.lens.navigation import androidx.navigation.NavHostController // [CORE-LOGIC] + /** [CONTRACT] @summary Класс-обертка над NavHostController для предоставления типизированных навигационных действий. @@ -13,9 +14,9 @@ import androidx.navigation.NavHostController class NavigationActions(private val navController: NavHostController) { // [ACTION] /** - [CONTRACT] - @summary Навигация на главный экран. - @sideeffect Очищает back stack до главного экрана, чтобы избежать циклов. + [CONTRACT] + @summary Навигация на главный экран. + @sideeffect Очищает back stack до главного экрана, чтобы избежать циклов. */ fun navigateToDashboard() { navController.navigate(Screen.Dashboard.route) { @@ -25,47 +26,55 @@ class NavigationActions(private val navController: NavHostController) { launchSingleTop = true } } + // [ACTION] fun navigateToLocations() { navController.navigate(Screen.LocationsList.route) { launchSingleTop = true } } + // [ACTION] fun navigateToLabels() { navController.navigate(Screen.LabelsList.route) { launchSingleTop = true } } + // [ACTION] fun navigateToSearch() { navController.navigate(Screen.Search.route) { launchSingleTop = true } } + // [ACTION] fun navigateToInventoryListWithLabel(labelId: String) { val route = Screen.InventoryList.withFilter("label", labelId) navController.navigate(route) } + // [ACTION] fun navigateToInventoryListWithLocation(locationId: String) { val route = Screen.InventoryList.withFilter("location", locationId) navController.navigate(route) } + // [ACTION] fun navigateToCreateItem() { navController.navigate(Screen.ItemEdit.createRoute("new")) } + // [ACTION] fun navigateToLogout() { navController.navigate(Screen.Setup.route) { popUpTo(Screen.Dashboard.route) { inclusive = true } } } + // [ACTION] fun navigateBack() { navController.popBackStack() } } -// [END_FILE_NavigationActions.kt] \ No newline at end of file +// [END_FILE_NavigationActions.kt] diff --git a/app/src/main/java/com/homebox/lens/navigation/Screen.kt b/app/src/main/java/com/homebox/lens/navigation/Screen.kt index 6014abb..a47da92 100644 --- a/app/src/main/java/com/homebox/lens/navigation/Screen.kt +++ b/app/src/main/java/com/homebox/lens/navigation/Screen.kt @@ -4,6 +4,7 @@ package com.homebox.lens.navigation // [CORE-LOGIC] + /** * [CONTRACT] * Запечатанный класс для определения маршрутов навигации в приложении. @@ -13,7 +14,9 @@ package com.homebox.lens.navigation sealed class Screen(val route: String) { // [STATE] data object Setup : Screen("setup_screen") + data object Dashboard : Screen("dashboard_screen") + data object InventoryList : Screen("inventory_list_screen") { /** * [CONTRACT] @@ -25,7 +28,10 @@ sealed class Screen(val route: String) { * @sideeffect [ARCH-IMPLICATION] NavGraph должен быть настроен для приема этого опционального query-параметра (например, 'navArgument("label") { nullable = true }'). */ // [HELPER] - fun withFilter(key: String, value: String): String { + fun withFilter( + key: String, + value: String, + ): String { // [PRECONDITION] require(key.isNotBlank()) { "[PRECONDITION_FAILED] Filter key cannot be blank." } require(value.isNotBlank()) { "[PRECONDITION_FAILED] Filter value cannot be blank." } @@ -56,6 +62,7 @@ sealed class Screen(val route: String) { return route } } + data object ItemEdit : Screen("item_edit_screen/{itemId}") { /** * [CONTRACT] @@ -75,8 +82,11 @@ sealed class Screen(val route: String) { return route } } + data object LabelsList : Screen("labels_list_screen") + data object LocationsList : Screen("locations_list_screen") + data object LocationEdit : Screen("location_edit_screen/{locationId}") { /** * [CONTRACT] @@ -96,6 +106,7 @@ sealed class Screen(val route: String) { return route } } + data object Search : Screen("search_screen") } -// [END_FILE_Screen.kt] \ No newline at end of file +// [END_FILE_Screen.kt] diff --git a/app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt b/app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt index 176d749..0c28b7b 100644 --- a/app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt +++ b/app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp import com.homebox.lens.R import com.homebox.lens.navigation.NavigationActions import com.homebox.lens.navigation.Screen + /** [CONTRACT] @summary Контент для бокового навигационного меню (Drawer). @@ -33,7 +34,7 @@ import com.homebox.lens.navigation.Screen internal fun AppDrawerContent( currentRoute: String?, navigationActions: NavigationActions, - onCloseDrawer: () -> Unit + onCloseDrawer: () -> Unit, ) { ModalDrawerSheet { Spacer(Modifier.height(12.dp)) @@ -42,9 +43,10 @@ internal fun AppDrawerContent( navigationActions.navigateToCreateItem() onCloseDrawer() }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), ) { Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(18.dp)) Spacer(Modifier.width(8.dp)) @@ -58,7 +60,7 @@ internal fun AppDrawerContent( onClick = { navigationActions.navigateToDashboard() onCloseDrawer() - } + }, ) NavigationDrawerItem( label = { Text(stringResource(id = R.string.nav_locations)) }, @@ -66,7 +68,7 @@ internal fun AppDrawerContent( onClick = { navigationActions.navigateToLocations() onCloseDrawer() - } + }, ) NavigationDrawerItem( label = { Text(stringResource(id = R.string.nav_labels)) }, @@ -74,7 +76,7 @@ internal fun AppDrawerContent( onClick = { navigationActions.navigateToLabels() onCloseDrawer() - } + }, ) NavigationDrawerItem( label = { Text(stringResource(id = R.string.search)) }, @@ -82,7 +84,7 @@ internal fun AppDrawerContent( onClick = { navigationActions.navigateToSearch() onCloseDrawer() - } + }, ) // TODO: Add Profile and Tools items Divider() @@ -92,7 +94,7 @@ internal fun AppDrawerContent( onClick = { navigationActions.navigateToLogout() onCloseDrawer() - } + }, ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/homebox/lens/ui/common/MainScaffold.kt b/app/src/main/java/com/homebox/lens/ui/common/MainScaffold.kt index b366974..7adec65 100644 --- a/app/src/main/java/com/homebox/lens/ui/common/MainScaffold.kt +++ b/app/src/main/java/com/homebox/lens/ui/common/MainScaffold.kt @@ -17,6 +17,7 @@ import com.homebox.lens.navigation.NavigationActions import kotlinx.coroutines.launch // [UI_COMPONENT] + /** * [CONTRACT] * @summary Общая обертка для экранов, включающая Scaffold и Navigation Drawer. @@ -35,7 +36,7 @@ fun MainScaffold( currentRoute: String?, navigationActions: NavigationActions, topBarActions: @Composable () -> Unit = {}, - content: @Composable (PaddingValues) -> Unit + content: @Composable (PaddingValues) -> Unit, ) { // [STATE] val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) @@ -48,9 +49,9 @@ fun MainScaffold( AppDrawerContent( currentRoute = currentRoute, navigationActions = navigationActions, - onCloseDrawer = { scope.launch { drawerState.close() } } + onCloseDrawer = { scope.launch { drawerState.close() } }, ) - } + }, ) { Scaffold( topBar = { @@ -60,13 +61,13 @@ fun MainScaffold( IconButton(onClick = { scope.launch { drawerState.open() } }) { Icon( Icons.Default.Menu, - contentDescription = stringResource(id = R.string.cd_open_navigation_drawer) + contentDescription = stringResource(id = R.string.cd_open_navigation_drawer), ) } }, - actions = { topBarActions() } + actions = { topBarActions() }, ) - } + }, ) { paddingValues -> // [ACTION] content(paddingValues) diff --git a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt index 775cd5c..d826286 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt @@ -30,6 +30,7 @@ import com.homebox.lens.ui.common.MainScaffold import com.homebox.lens.ui.theme.HomeboxLensTheme import timber.log.Timber // [ENTRYPOINT] + /** [CONTRACT] @summary Главная Composable-функция для экрана "Панель управления". @@ -42,7 +43,7 @@ import timber.log.Timber fun DashboardScreen( viewModel: DashboardViewModel = hiltViewModel(), currentRoute: String?, - navigationActions: NavigationActions + navigationActions: NavigationActions, ) { // [STATE] val uiState by viewModel.uiState.collectAsState() @@ -55,10 +56,10 @@ fun DashboardScreen( IconButton(onClick = { navigationActions.navigateToSearch() }) { Icon( Icons.Default.Search, - contentDescription = stringResource(id = R.string.cd_scan_qr_code) // TODO: Rename string resource + contentDescription = stringResource(id = R.string.cd_scan_qr_code), // TODO: Rename string resource ) } - } + }, ) { paddingValues -> DashboardContent( modifier = Modifier.padding(paddingValues), @@ -70,12 +71,13 @@ fun DashboardScreen( onLabelClick = { label -> Timber.i("[ACTION] Label chip clicked: ${label.id}. Navigating...") navigationActions.navigateToInventoryListWithLabel(label.id) - } + }, ) } // [END_FUNCTION_DashboardScreen] } // [HELPER] + /** [CONTRACT] @summary Отображает основной контент экрана в зависимости от uiState. @@ -89,7 +91,7 @@ private fun DashboardContent( modifier: Modifier = Modifier, uiState: DashboardUiState, onLocationClick: (LocationOutCount) -> Unit, - onLabelClick: (LabelOut) -> Unit + onLabelClick: (LabelOut) -> Unit, ) { // [CORE-LOGIC] when (uiState) { @@ -103,16 +105,17 @@ private fun DashboardContent( Text( text = uiState.message, color = MaterialTheme.colorScheme.error, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) } } is DashboardUiState.Success -> { LazyColumn( - modifier = modifier - .fillMaxSize() - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(24.dp) + modifier = + modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(24.dp), ) { item { Spacer(modifier = Modifier.height(8.dp)) } item { StatisticsSection(statistics = uiState.statistics) } @@ -126,6 +129,7 @@ private fun DashboardContent( // [END_FUNCTION_DashboardContent] } // [UI_COMPONENT] + /** [CONTRACT] @summary Секция для отображения общей статистики. @@ -136,27 +140,49 @@ private fun StatisticsSection(statistics: GroupStatistics) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text( text = stringResource(id = R.string.dashboard_section_quick_stats), - style = MaterialTheme.typography.titleMedium + style = MaterialTheme.typography.titleMedium, ) Card { LazyVerticalGrid( columns = GridCells.Fixed(2), - modifier = Modifier - .height(120.dp) - .fillMaxWidth() - .padding(16.dp), + modifier = + Modifier + .height(120.dp) + .fillMaxWidth() + .padding(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + verticalArrangement = Arrangement.spacedBy(16.dp), ) { - item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_items), value = statistics.items.toString()) } - item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_value), value = statistics.totalValue.toString()) } - item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_labels), value = statistics.labels.toString()) } - item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_locations), value = statistics.locations.toString()) } + item { + StatisticCard( + title = stringResource(id = R.string.dashboard_stat_total_items), + value = statistics.items.toString(), + ) + } + item { + StatisticCard( + title = stringResource(id = R.string.dashboard_stat_total_value), + value = statistics.totalValue.toString(), + ) + } + item { + StatisticCard( + title = stringResource(id = R.string.dashboard_stat_total_labels), + value = statistics.labels.toString(), + ) + } + item { + StatisticCard( + title = stringResource(id = R.string.dashboard_stat_total_locations), + value = statistics.locations.toString(), + ) + } } } } } // [UI_COMPONENT] + /** [CONTRACT] @summary Карточка для отображения одного статистического показателя. @@ -164,13 +190,17 @@ private fun StatisticsSection(statistics: GroupStatistics) { @param value Значение показателя. */ @Composable -private fun StatisticCard(title: String, value: String) { +private fun StatisticCard( + title: String, + value: String, +) { Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { Text(text = title, style = MaterialTheme.typography.labelMedium, textAlign = TextAlign.Center) Text(text = value, style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center) } } // [UI_COMPONENT] + /** [CONTRACT] @summary Секция для отображения недавно добавленных элементов. @@ -181,16 +211,17 @@ private fun RecentlyAddedSection(items: List) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text( text = stringResource(id = R.string.dashboard_section_recently_added), - style = MaterialTheme.typography.titleMedium + style = MaterialTheme.typography.titleMedium, ) if (items.isEmpty()) { Text( text = stringResource(id = R.string.items_not_found), style = MaterialTheme.typography.bodyMedium, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - textAlign = TextAlign.Center + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + textAlign = TextAlign.Center, ) } else { LazyRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) { @@ -202,6 +233,7 @@ private fun RecentlyAddedSection(items: List) { } } // [UI_COMPONENT] + /** [CONTRACT] @summary Карточка для отображения краткой информации об элементе. @@ -212,17 +244,25 @@ private fun ItemCard(item: ItemSummary) { Card(modifier = Modifier.width(150.dp)) { Column(modifier = Modifier.padding(8.dp)) { // TODO: Add image here from item.image - Spacer(modifier = Modifier - .height(80.dp) - .fillMaxWidth() - .background(MaterialTheme.colorScheme.secondaryContainer)) + Spacer( + modifier = + Modifier + .height(80.dp) + .fillMaxWidth() + .background(MaterialTheme.colorScheme.secondaryContainer), + ) Spacer(modifier = Modifier.height(8.dp)) Text(text = item.name, style = MaterialTheme.typography.titleSmall, maxLines = 1) - Text(text = item.location?.name ?: stringResource(id = R.string.no_location), style = MaterialTheme.typography.bodySmall, maxLines = 1) + Text( + text = item.location?.name ?: stringResource(id = R.string.no_location), + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + ) } } } // [UI_COMPONENT] + /** [CONTRACT] @summary Секция для отображения местоположений в виде чипсов. @@ -231,11 +271,14 @@ private fun ItemCard(item: ItemSummary) { */ @OptIn(ExperimentalLayoutApi::class) @Composable -private fun LocationsSection(locations: List, onLocationClick: (LocationOutCount) -> Unit) { +private fun LocationsSection( + locations: List, + onLocationClick: (LocationOutCount) -> Unit, +) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text( text = stringResource(id = R.string.dashboard_section_locations), - style = MaterialTheme.typography.titleMedium + style = MaterialTheme.typography.titleMedium, ) FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp), @@ -243,13 +286,14 @@ private fun LocationsSection(locations: List, onLocationClick: locations.forEach { location -> SuggestionChip( onClick = { onLocationClick(location) }, - label = { Text(stringResource(id = R.string.location_chip_label, location.name, location.itemCount)) } + label = { Text(stringResource(id = R.string.location_chip_label, location.name, location.itemCount)) }, ) } } } } // [UI_COMPONENT] + /** [CONTRACT] @summary Секция для отображения меток в виде чипсов. @@ -258,11 +302,14 @@ private fun LocationsSection(locations: List, onLocationClick: */ @OptIn(ExperimentalLayoutApi::class) @Composable -private fun LabelsSection(labels: List, onLabelClick: (LabelOut) -> Unit) { +private fun LabelsSection( + labels: List, + onLabelClick: (LabelOut) -> Unit, +) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text( text = stringResource(id = R.string.dashboard_section_labels), - style = MaterialTheme.typography.titleMedium + style = MaterialTheme.typography.titleMedium, ) FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp), @@ -270,46 +317,92 @@ private fun LabelsSection(labels: List, onLabelClick: (LabelOut) -> Un labels.forEach { label -> SuggestionChip( onClick = { onLabelClick(label) }, - label = { Text(label.name) } + label = { Text(label.name) }, ) } } } } + // [PREVIEW] @Preview(showBackground = true, name = "Dashboard Success State") @Composable fun DashboardContentSuccessPreview() { - val previewState = DashboardUiState.Success( - statistics = GroupStatistics( - items = 123, - totalValue = 9999.99, - locations = 5, - labels = 8 - ), - locations = listOf( - LocationOutCount(id="1", name="Office", color = "#FF0000", isArchived = false, itemCount = 10, createdAt = "", updatedAt = ""), - LocationOutCount(id="2", name="Garage", color = "#00FF00", isArchived = false, itemCount = 5, createdAt = "", updatedAt = ""), - LocationOutCount(id="3",name="Living Room", color = "#0000FF", isArchived = false, itemCount = 15, createdAt = "", updatedAt = ""), - LocationOutCount(id="4",name="Kitchen", color = "#FFFF00", isArchived = false, itemCount = 20, createdAt = "", updatedAt = ""), - LocationOutCount(id="5",name="Basement", color = "#00FFFF", isArchived = false, itemCount = 3, createdAt = "", updatedAt = "") - ), - labels = listOf( - LabelOut(id="1", name="electronics", color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""), - LabelOut(id="2", name="important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""), - LabelOut(id="3", name="seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""), - LabelOut(id="4", name="hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = "") - ), - recentlyAddedItems = emptyList() - ) + val previewState = + DashboardUiState.Success( + statistics = + GroupStatistics( + items = 123, + totalValue = 9999.99, + locations = 5, + labels = 8, + ), + locations = + listOf( + LocationOutCount( + id = "1", + name = "Office", + color = "#FF0000", + isArchived = false, + itemCount = 10, + createdAt = "", + updatedAt = "", + ), + LocationOutCount( + id = "2", + name = "Garage", + color = "#00FF00", + isArchived = false, + itemCount = 5, + createdAt = "", + updatedAt = "", + ), + LocationOutCount( + id = "3", + name = "Living Room", + color = "#0000FF", + isArchived = false, + itemCount = 15, + createdAt = "", + updatedAt = "", + ), + LocationOutCount( + id = "4", + name = "Kitchen", + color = "#FFFF00", + isArchived = false, + itemCount = 20, + createdAt = "", + updatedAt = "", + ), + LocationOutCount( + id = "5", + name = "Basement", + color = "#00FFFF", + isArchived = false, + itemCount = 3, + createdAt = "", + updatedAt = "", + ), + ), + labels = + listOf( + LabelOut(id = "1", name = "electronics", color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""), + LabelOut(id = "2", name = "important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""), + LabelOut(id = "3", name = "seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""), + LabelOut(id = "4", name = "hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = ""), + ), + recentlyAddedItems = emptyList(), + ) HomeboxLensTheme { DashboardContent( uiState = previewState, onLocationClick = {}, - onLabelClick = {} + onLabelClick = {}, ) } } + // [PREVIEW] @Preview(showBackground = true, name = "Dashboard Loading State") @Composable @@ -318,10 +411,11 @@ fun DashboardContentLoadingPreview() { DashboardContent( uiState = DashboardUiState.Loading, onLocationClick = {}, - onLabelClick = {} + onLabelClick = {}, ) } } + // [PREVIEW] @Preview(showBackground = true, name = "Dashboard Error State") @Composable @@ -330,8 +424,8 @@ fun DashboardContentErrorPreview() { DashboardContent( uiState = DashboardUiState.Error(stringResource(id = R.string.error_loading_failed)), onLocationClick = {}, - onLabelClick = {} + onLabelClick = {}, ) } } -// [END_FILE_DashboardScreen.kt] \ No newline at end of file +// [END_FILE_DashboardScreen.kt] diff --git a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardUiState.kt b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardUiState.kt index a4fe49e..db00db8 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardUiState.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardUiState.kt @@ -11,6 +11,7 @@ import com.homebox.lens.domain.model.LocationOutCount // [CORE-LOGIC] // [ENTITY: SealedInterface('DashboardUiState')] + /** * [CONTRACT] * Определяет все возможные состояния для экрана "Дэшборд". @@ -29,7 +30,7 @@ sealed interface DashboardUiState { val statistics: GroupStatistics, val locations: List, val labels: List, - val recentlyAddedItems: List + val recentlyAddedItems: List, ) : DashboardUiState /** @@ -45,4 +46,4 @@ sealed interface DashboardUiState { */ data object Loading : DashboardUiState } -// [END_FILE_DashboardUiState.kt] \ No newline at end of file +// [END_FILE_DashboardUiState.kt] diff --git a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt index 2dce373..cddb3dd 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt @@ -9,10 +9,7 @@ import com.homebox.lens.domain.usecase.GetAllLabelsUseCase import com.homebox.lens.domain.usecase.GetAllLocationsUseCase import com.homebox.lens.domain.usecase.GetRecentlyAddedItemsUseCase import com.homebox.lens.domain.usecase.GetStatisticsUseCase -import com.homebox.lens.ui.screen.dashboard.DashboardUiState import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber @@ -20,6 +17,7 @@ import javax.inject.Inject // [VIEWMODEL] // [ENTITY: ViewModel('DashboardViewModel')] + /** * [CONTRACT] * @summary ViewModel для главного экрана (Dashboard). @@ -28,61 +26,64 @@ import javax.inject.Inject * @invariant `uiState` всегда является одним из состояний, определенных в `DashboardUiState`. */ @HiltViewModel -class DashboardViewModel @Inject constructor( - private val getStatisticsUseCase: GetStatisticsUseCase, - private val getAllLocationsUseCase: GetAllLocationsUseCase, - private val getAllLabelsUseCase: GetAllLabelsUseCase, - private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase -) : ViewModel() { +class DashboardViewModel + @Inject + constructor( + private val getStatisticsUseCase: GetStatisticsUseCase, + private val getAllLocationsUseCase: GetAllLocationsUseCase, + private val getAllLabelsUseCase: GetAllLabelsUseCase, + private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase, + ) : ViewModel() { + // [STATE] + private val _uiState = MutableStateFlow(DashboardUiState.Loading) - // [STATE] - private val _uiState = MutableStateFlow(DashboardUiState.Loading) - // [FIX] Добавлен получатель (receiver) `_uiState` для вызова asStateFlow(). - // [REASON] `asStateFlow()` является функцией-расширением для `MutableStateFlow` и - // должна вызываться на его экземпляре, чтобы создать публичную, неизменяемую версию потока. - val uiState = _uiState.asStateFlow() + // [FIX] Добавлен получатель (receiver) `_uiState` для вызова asStateFlow(). + // [REASON] `asStateFlow()` является функцией-расширением для `MutableStateFlow` и + // должна вызываться на его экземпляре, чтобы создать публичную, неизменяемую версию потока. + val uiState = _uiState.asStateFlow() - // [LIFECYCLE_HANDLER] - init { - loadDashboardData() - } + // [LIFECYCLE_HANDLER] + init { + loadDashboardData() + } - /** - * [CONTRACT] - * @summary Загружает все необходимые данные для экрана Dashboard. - * @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его - * между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`. - * @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`. - */ - fun loadDashboardData() { - // [ENTRYPOINT] - viewModelScope.launch { - _uiState.value = DashboardUiState.Loading - Timber.i("[ACTION] Starting dashboard data collection.") + /** + * [CONTRACT] + * @summary Загружает все необходимые данные для экрана Dashboard. + * @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его + * между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`. + * @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`. + */ + fun loadDashboardData() { + // [ENTRYPOINT] + viewModelScope.launch { + _uiState.value = DashboardUiState.Loading + Timber.i("[ACTION] Starting dashboard data collection.") - val statsFlow = flow { emit(getStatisticsUseCase()) } - val locationsFlow = flow { emit(getAllLocationsUseCase()) } - val labelsFlow = flow { emit(getAllLabelsUseCase()) } - val recentItemsFlow = getRecentlyAddedItemsUseCase(limit = 10) + val statsFlow = flow { emit(getStatisticsUseCase()) } + val locationsFlow = flow { emit(getAllLocationsUseCase()) } + val labelsFlow = flow { emit(getAllLabelsUseCase()) } + val recentItemsFlow = getRecentlyAddedItemsUseCase(limit = 10) - combine(statsFlow, locationsFlow, labelsFlow, recentItemsFlow) { stats, locations, labels, recentItems -> - DashboardUiState.Success( - statistics = stats, - locations = locations, - labels = labels, - recentlyAddedItems = recentItems - ) - }.catch { exception -> - Timber.e(exception, "[ERROR] Failed to load dashboard data. State -> Error.") - _uiState.value = DashboardUiState.Error( - message = exception.message ?: "Could not load dashboard data." - ) - }.collect { successState -> - Timber.i("[SUCCESS] Dashboard data loaded successfully. State -> Success.") - _uiState.value = successState + combine(statsFlow, locationsFlow, labelsFlow, recentItemsFlow) { stats, locations, labels, recentItems -> + DashboardUiState.Success( + statistics = stats, + locations = locations, + labels = labels, + recentlyAddedItems = recentItems, + ) + }.catch { exception -> + Timber.e(exception, "[ERROR] Failed to load dashboard data. State -> Error.") + _uiState.value = + DashboardUiState.Error( + message = exception.message ?: "Could not load dashboard data.", + ) + }.collect { successState -> + Timber.i("[SUCCESS] Dashboard data loaded successfully. State -> Success.") + _uiState.value = successState + } } } + // [END_CLASS_DashboardViewModel] } - // [END_CLASS_DashboardViewModel] -} -// [END_FILE_DashboardViewModel.kt] \ No newline at end of file +// [END_FILE_DashboardViewModel.kt] diff --git a/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt index abf1924..8d002aa 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt @@ -13,6 +13,7 @@ import com.homebox.lens.navigation.NavigationActions import com.homebox.lens.ui.common.MainScaffold // [ENTRYPOINT] + /** * [CONTRACT] * @summary Composable-функция для экрана "Список инвентаря". @@ -22,16 +23,16 @@ import com.homebox.lens.ui.common.MainScaffold @Composable fun InventoryListScreen( currentRoute: String?, - navigationActions: NavigationActions + navigationActions: NavigationActions, ) { // [UI_COMPONENT] MainScaffold( topBarTitle = stringResource(id = R.string.inventory_list_title), currentRoute = currentRoute, - navigationActions = navigationActions + navigationActions = navigationActions, ) { // [CORE-LOGIC] Text(text = "TODO: Inventory List Screen") } // [END_FUNCTION_InventoryListScreen] -} \ No newline at end of file +} diff --git a/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListViewModel.kt b/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListViewModel.kt index 69069d6..25e2ec7 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListViewModel.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListViewModel.kt @@ -9,8 +9,10 @@ import javax.inject.Inject // [VIEWMODEL] @HiltViewModel -class InventoryListViewModel @Inject constructor() : ViewModel() { - // [STATE] - // TODO: Implement UI state -} +class InventoryListViewModel + @Inject + constructor() : ViewModel() { + // [STATE] + // TODO: Implement UI state + } // [END_FILE_InventoryListViewModel.kt] diff --git a/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt index 6b78f9e..54d39fe 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt @@ -13,6 +13,7 @@ import com.homebox.lens.navigation.NavigationActions import com.homebox.lens.ui.common.MainScaffold // [ENTRYPOINT] + /** * [CONTRACT] * @summary Composable-функция для экрана "Детали элемента". @@ -22,16 +23,16 @@ import com.homebox.lens.ui.common.MainScaffold @Composable fun ItemDetailsScreen( currentRoute: String?, - navigationActions: NavigationActions + navigationActions: NavigationActions, ) { // [UI_COMPONENT] MainScaffold( topBarTitle = stringResource(id = R.string.item_details_title), currentRoute = currentRoute, - navigationActions = navigationActions + navigationActions = navigationActions, ) { // [CORE-LOGIC] Text(text = "TODO: Item Details Screen") } // [END_FUNCTION_ItemDetailsScreen] -} \ No newline at end of file +} diff --git a/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsViewModel.kt b/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsViewModel.kt index 6a591a8..f516e10 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsViewModel.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsViewModel.kt @@ -9,8 +9,10 @@ import javax.inject.Inject // [VIEWMODEL] @HiltViewModel -class ItemDetailsViewModel @Inject constructor() : ViewModel() { - // [STATE] - // TODO: Implement UI state -} +class ItemDetailsViewModel + @Inject + constructor() : ViewModel() { + // [STATE] + // TODO: Implement UI state + } // [END_FILE_ItemDetailsViewModel.kt] diff --git a/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt index 957024f..7d5b94c 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt @@ -13,6 +13,7 @@ import com.homebox.lens.navigation.NavigationActions import com.homebox.lens.ui.common.MainScaffold // [ENTRYPOINT] + /** * [CONTRACT] * @summary Composable-функция для экрана "Редактирование элемента". @@ -22,13 +23,13 @@ import com.homebox.lens.ui.common.MainScaffold @Composable fun ItemEditScreen( currentRoute: String?, - navigationActions: NavigationActions + navigationActions: NavigationActions, ) { // [UI_COMPONENT] MainScaffold( topBarTitle = stringResource(id = R.string.item_edit_title), currentRoute = currentRoute, - navigationActions = navigationActions + navigationActions = navigationActions, ) { // [CORE-LOGIC] Text(text = "TODO: Item Edit Screen") diff --git a/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt b/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt index 975f01d..4f41237 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt @@ -9,8 +9,10 @@ import javax.inject.Inject // [VIEWMODEL] @HiltViewModel -class ItemEditViewModel @Inject constructor() : ViewModel() { - // [STATE] - // TODO: Implement UI state -} +class ItemEditViewModel + @Inject + constructor() : ViewModel() { + // [STATE] + // TODO: Implement UI state + } // [END_FILE_ItemEditViewModel.kt] diff --git a/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt index c094235..91b0015 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt @@ -1,15 +1,11 @@ // [PACKAGE] com.homebox.lens.ui.screen.labelslist // [FILE] LabelsListScreen.kt -// [SEMANTICS] ui, labels_list, state_management, compose, dialog +// [SEMANTICS] ui, screen, jetpack_compose, labels_list, state_management package com.homebox.lens.ui.screen.labelslist +// [SECTION] Imports // [IMPORTS] import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -17,241 +13,193 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.Label import androidx.compose.material.icons.filled.Add -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp +import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavController +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.homebox.lens.R import com.homebox.lens.domain.model.Label -import com.homebox.lens.navigation.Screen +import com.homebox.lens.ui.theme.HomeboxLensTheme import timber.log.Timber -// [SECTION] Main Screen Composable +// [ENTITY: Class('LabelsListScreen')] +// [RELATION: Class('LabelsListScreen')] -> [DEPENDS_ON] -> [Class('LabelsListViewModel')] +// [RELATION: Class('LabelsListScreen')] -> [READS_FROM] -> [DataStructure('LabelsListUiState')] /** - * [CONTRACT] - * @summary Отображает экран со списком всех меток. - * @description Главная Composable-функция для экрана меток. Она использует Scaffold для структуры, - * получает состояние от `LabelsListViewModel`, обрабатывает навигацию и делегирует отображение - * списка и диалогов вспомогательным Composable-функциям. + * [MAIN-CONTRACT] + * Экран для отображения списка всех меток. * - * @param navController Контроллер навигации для перемещения между экранами. - * @param viewModel ViewModel, предоставляющая состояние UI для экрана меток. - * - * @precondition `navController` должен быть корректно инициализирован и способен обрабатывать навигационные события. - * @precondition `viewModel` должен быть доступен через Hilt. - * @postcondition Экран исчерпывающе обрабатывает все состояния из `LabelsListUiState` (Loading, Success, Error). - * @sideeffect Пользовательские действия (клики) инициируют вызовы ViewModel и навигационные команды через `navController`. + * @param onLabelClick Функция обратного вызова при нажатии на метку. Передает ID метки. + * @param onAddNewLabelClick Функция обратного вызова для инициирования процесса создания новой метки. + * @param onNavigateBack Функция обратного вызова для навигации назад. + * @param viewModel ViewModel для этого экрана, предоставляемая Hilt. */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun LabelsListScreen( - navController: NavController, - viewModel: LabelsListViewModel = hiltViewModel() -) { - // [ENTRYPOINT] - val uiState by viewModel.uiState.collectAsState() - - // [CORE-LOGIC] - Scaffold( - topBar = { - TopAppBar( - title = { Text(text = stringResource(id = R.string.screen_title_labels)) }, - navigationIcon = { - // [ACTION] Handle back navigation - IconButton(onClick = { - Timber.i("[ACTION] Navigate up initiated.") - navController.navigateUp() - }) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(id = R.string.content_desc_navigate_back) - ) - } - } - ) - }, - floatingActionButton = { - // [ACTION] Handle create new label initiation - FloatingActionButton(onClick = { - Timber.i("[ACTION] FAB clicked: Initiate create new label flow.") - viewModel.onShowCreateDialog() - }) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = stringResource(id = R.string.content_desc_create_label) - ) - } - } - ) { paddingValues -> - val currentState = uiState - if (currentState is LabelsListUiState.Success && currentState.isShowingCreateDialog) { - CreateLabelDialog( - onConfirm = { labelName -> - viewModel.createLabel(labelName) - }, - onDismiss = { - viewModel.onDismissCreateDialog() - } - ) - } - - Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - contentAlignment = Alignment.Center - ) { - // [CORE-LOGIC] State-driven UI rendering - when (currentState) { - is LabelsListUiState.Loading -> { - CircularProgressIndicator() - } - is LabelsListUiState.Error -> { - Text(text = currentState.message) - } - is LabelsListUiState.Success -> { - if (currentState.labels.isEmpty()) { - Text(text = stringResource(id = R.string.labels_list_empty)) - } else { - LabelsList( - labels = currentState.labels, - onLabelClick = { label -> - // [ACTION] Handle label click - Timber.i("[ACTION] Label clicked: ${label.id}. Navigating to inventory list.") - // [DESIGN-DECISION] Использовать существующий экран списка инвентаря, передавая фильтр. - val route = Screen.InventoryList.withFilter("label", label.id) - navController.navigate(route) - } - ) - } - } - } - } - } - // [COHERENCE_CHECK_PASSED] -} -// [END_FUNCTION] LabelsListScreen - -// [SECTION] Helper Composables - -/** - * [CONTRACT] - * @summary Composable-функция для отображения списка меток. - * @param labels Список объектов `Label` для отображения. - * @param onLabelClick Лямбда-функция, вызываемая при нажатии на элемент списка. - * @param modifier Модификатор для настройки внешнего вида. - */ -@Composable -private fun LabelsList( - labels: List