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