Homebox Lens
Android-клиент для системы управления инвентарем Homebox. Позволяет пользователям управлять своим инвентарем, взаимодействуя с экземпляром сервера Homebox.
UI Framework
Пользовательский интерфейс приложения построен с использованием Jetpack Compose, современного декларативного UI-фреймворка от Google. Это обеспечивает быстрое создание, гибкость и поддержку динамических данных.
Асинхронные операции
Все асинхронные операции, такие как сетевые запросы или доступ к базе данных, выполняются с использованием Kotlin Coroutines. Это обеспечивает эффективное управление фоновыми задачами без блокировки основного потока.
Сетевое взаимодействие
Для взаимодействия с API сервера Homebox используется стек технологий: Retrofit для создания типобезопасных HTTP-клиентов, OkHttp в качестве HTTP-клиента и Moshi для парсинга JSON.
Локальное хранилище
Для кэширования данных на устройстве используется библиотека Room. Она предоставляет абстракцию над SQLite и обеспечивает надежное локальное хранение данных.
Библиотека логирования
В проекте используется Timber (timber.log.Timber) для всех целей логирования. Он предоставляет простой и расширяемый API для логирования.
Интернационализация (Мультиязычность)
Приложение должно поддерживать несколько языков для обеспечения доступности для глобальной аудитории.
- Все строки, видимые пользователю, вынесены в `app/src/main/res/values/strings.xml`.
- Язык по умолчанию - русский (ru).
- В коде для доступа к строкам используются ссылки на ресурсы (например, `R.string.app_name`).
Внедрение зависимостей (Dependency Injection)
Для управления зависимостями в проекте используется Hilt. Он интегрирован с компонентами Jetpack и упрощает внедрение зависимостей в Android-приложениях.
Модуль Hilt для зависимостей уровня приложения
AppModule.kt предоставляет зависимости на уровне приложения, такие как контекст приложения и другие синглтоны.
Навигация
Навигация между экранами (Composable-функциями) реализована с помощью библиотеки Navigation Compose, которая является частью Jetpack Navigation.
Навигационный граф
NavGraph.kt определяет структуру навигации приложения, связывая экраны и их маршруты.
Определение маршрутов экранов
Screen.kt определяет все возможные маршруты (экраны) в приложении в виде запечатанного класса для типобезопасной навигации.
Спецификация безопасности проекта.
Все сетевые взаимодействия должны быть защищены HTTPS. Аутентификация пользователя хранится в EncryptedSharedPreferences. Обработка ошибок аутентификации должна включать logout и редирект на экран логина.
Использовать JWT или API-ключ для авторизации запросов. При истечении токена автоматически обновлять.
Локальные данные (credentials) шифровать с помощью Android KeyStore.
Спецификация обработки ошибок.
Все потенциальные ошибки (сеть, БД, валидация) должны быть обработаны с использованием sealed classes для ошибок (e.g., NetworkError, ValidationError) и отображаться пользователю через Snackbar или Dialog.
При сетевых ошибках показывать сообщение "No internet connection" и предлагать retry.
Для HTTP 4xx/5xx отображать user-friendly сообщение на основе response body.
Использовать require/check для контрактов, логировать и показывать toast.
Руководство по использованию иконок
Этот раздел определяет стандартный набор иконок 'androidx.compose.material.icons.Icons.Filled' для использования в приложении. Для некоторых иконок указаны их AutoMirrored версии для корректного отображения в RTL-языках.
build, dependencies
build, dependencies
Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation.
navigation, compose, nav_host
Запечатанный класс для определения маршрутов навигации в приложении.
Обеспечивает типобезопасность при навигации. @param route Строковый идентификатор маршрута. / sealed class Screen(val route: String) { // [ENTITY: Object('Setup')] data object Setup : Screen("setup_screen") // [END_ENTITY: Object('Setup')] // [ENTITY: Object('Dashboard')] data object Dashboard : Screen("dashboard_screen") // [END_ENTITY: Object('Dashboard')] // [ENTITY: Object('InventoryList')] data object InventoryList : Screen("inventory_list_screen") { // [ENTITY: Function('withFilter')] /** @summary Создает маршрут для экрана списка инвентаря с параметром фильтра. @param key Ключ фильтра (например, "label" или "location"). @param value Значение фильтра (например, ID метки или местоположения). @return Строку полного маршрута с query-параметром. @throws IllegalArgumentException если ключ или значение пустые. / fun withFilter(key: String, value: String): String { require(key.isNotBlank()) { "Filter key cannot be blank." } require(value.isNotBlank()) { "Filter value cannot be blank." } val constructedRoute = "inventory_list_screen?$key=$value" check(constructedRoute.contains("?$key=$value")) { "Route must contain the filter query." } return constructedRoute } // [END_ENTITY: Function('withFilter')] } // [END_ENTITY: Object('InventoryList')] // [ENTITY: Object('ItemDetails')] data object ItemDetails : Screen("item_details_screen/{itemId}") { // [ENTITY: Function('createRoute')] /** @summary Создает маршрут для экрана деталей элемента с указанным ID. @param itemId ID элемента для отображения. @return Строку полного маршрута. @throws IllegalArgumentException если itemId пустой. / fun createRoute(itemId: String): String { require(itemId.isNotBlank()) { "itemId не может быть пустым." } val route = "item_details_screen/$itemId" check(route.endsWith(itemId)) { "Маршрут должен заканчиваться на itemId." } return route } // [END_ENTITY: Function('createRoute')] } // [END_ENTITY: Object('ItemDetails')] // [ENTITY: Object('ItemEdit')] data object ItemEdit : Screen("item_edit_screen?itemId={itemId}") { // [ENTITY: Function('createRoute')] /** @summary Создает маршрут для экрана редактирования элемента с указанным ID. @param itemId ID элемента для редактирования. Null, если создается новый элемент. @return Строку полного маршрута. / fun createRoute(itemId: String? = null): String { return itemId?.let { "item_edit_screen?itemId=$it" } ?: "item_edit_screen" } // [END_ENTITY: Function('createRoute')] } // [END_ENTITY: Object('ItemEdit')] // [ENTITY: Object('LabelsList')] data object LabelsList : Screen("labels_list_screen") // [END_ENTITY: Object('LabelsList')] // [ENTITY: Object('LabelEdit')] data object LabelEdit : Screen("label_edit_screen?labelId={labelId}") { // [ENTITY: Function('createRoute')] /** @summary Создает маршрут для экрана редактирования метки с указанным ID. @param labelId ID метки для редактирования. Null, если создается новая метка. @return Строку полного маршрута. / fun createRoute(labelId: String? = null): String { return labelId?.let { "label_edit_screen?labelId=$it" } ?: "label_edit_screen" } // [END_ENTITY: Function('createRoute')] } // [END_ENTITY: Object('LabelEdit')] // [ENTITY: Object('LocationsList')] data object LocationsList : Screen("locations_list_screen") // [END_ENTITY: Object('LocationsList')] // [ENTITY: Object('LocationEdit')] data object LocationEdit : Screen("location_edit_screen/{locationId}") { // [ENTITY: Function('createRoute')] /** @summary Создает маршрут для экрана редактирования местоположения с указанным ID. @param locationId ID местоположения для редактирования. @return Строку полного маршрута. @throws IllegalArgumentException если locationId пустой. / fun createRoute(locationId: String): String { require(locationId.isNotBlank()) { "locationId не может быть пустым." } val route = "location_edit_screen/$locationId" check(route.endsWith(locationId)) { "Маршрут должен заканчиваться на locationId." } return route } // [END_ENTITY: Function('createRoute')] } // [END_ENTITY: Object('LocationEdit')] // [ENTITY: Object('Search')] data object Search : Screen("search_screen") // [END_ENTITY: Object('Search')] }
navigation, routes, sealed_class
Класс-обертка над NavHostController для предоставления типизированных навигационных действий.
navigation, controller, actions
Точка входа в приложение. Инициализирует Hilt и Timber.
application, hilt, timber
Контент для бокового навигационного меню (Drawer).
ui, common, navigation_drawer
Общая обертка для экранов, включающая Scaffold и Navigation Drawer.
ui, common, scaffold, navigation_drawer
The main theme for the Homebox Lens application.
ui, theme
Defines the typography for the application.
ui, theme, typography
Определяет все возможные состояния для экрана "Дэшборд".
ui, state, dashboard
Главная Composable-функция для экрана "Панель управления".
ui, screen, dashboard, compose, navigation
Отображает основной контент экрана в зависимости от uiState.
ui, screen, dashboard, compose, navigation
Секция для отображения общей статистики.
ui, screen, dashboard, compose, navigation
Карточка для отображения одного статистического показателя.
ui, screen, dashboard, compose, navigation
Секция для отображения недавно добавленных элементов.
ui, screen, dashboard, compose, navigation
Карточка для отображения краткой информации об элементе.
ui, screen, dashboard, compose, navigation
Секция для отображения местоположений в виде чипсов.
ui, screen, dashboard, compose, navigation
Секция для отображения меток в виде чипсов.
ui, screen, dashboard, compose, navigation
ui, screen, dashboard, compose, navigation
ui, screen, dashboard, compose, navigation
ui, screen, dashboard, compose, navigation
ViewModel для главного экрана (Dashboard).
Оркестрирует загрузку данных для Dashboard, используя строгую модель состояний (`DashboardUiState`), и обрабатывает параллельные запросы без состояний гонки. @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() { private val _uiState = MutableStateFlow<DashboardUiState>(DashboardUiState.Loading) val uiState = _uiState.asStateFlow() init { loadDashboardData() } // [ENTITY: Function('loadDashboardData')] /** @summary Загружает все необходимые данные для экрана Dashboard. @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`. @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`. / fun loadDashboardData() { viewModelScope.launch { _uiState.value = DashboardUiState.Loading Timber.i("[INFO][ENTRYPOINT][loading_dashboard_data] Starting dashboard data collection.") 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][EXCEPTION][loading_failed] Failed to load dashboard data. State -> Error.") _uiState.value = DashboardUiState.Error( message = exception.message ?: "Could not load dashboard data." ) }.collect { successState -> Timber.i("[INFO][SUCCESS][dashboard_data_loaded] Dashboard data loaded successfully. State -> Success.") _uiState.value = successState } } } // [END_ENTITY: Function('loadDashboardData')] }
ui_logic, dashboard, state_management, sealed_state, parallel_data_loading, timber_logging
Composable-функция для экрана "Редактирование метки".
ui, screen, label, edit
ViewModel для экрана редактирования/создания метки.
Управляет состоянием и логикой экрана, включая загрузку, создание и обновление метки.
ui, viewmodel, label_management, hilt
Состояние UI для экрана редактирования метки.
ui, viewmodel, label_management
Composable-функция для экрана "Список инвентаря".
ui, screen, inventory, list
ViewModel for the inventory list screen.
ui, viewmodel, inventory_list
Неизменяемая модель данных, представляющая полное состояние экрана настройки (Setup Screen).
Использование `data class` предоставляет метод `copy()` для легкого создания новых состояний. @param serverUrl URL-адрес сервера Homebox. @param username Имя пользователя для входа. @param password Пароль пользователя. @param isLoading Флаг, указывающий, выполняется ли в данный момент сетевой запрос. @param error Сообщение об ошибке для отображения пользователю, или `null` при отсутствии ошибки. @param isSetupComplete Флаг, указывающий на успешное завершение настройки и входа. / data class SetupUiState( val serverUrl: String = "", val username: String = "", val password: String = "", val isLoading: Boolean = false, val error: String? = null, val isSetupComplete: Boolean = false )
ui_state, data_model, immutable
Главная Composable-функция для экрана настройки соединения с сервером.
ui, screen, setup, compose
Отображает контент экрана настройки: поля ввода и кнопку.
ui, screen, setup, compose
ui, screen, setup, compose
ViewModel для экрана первоначальной настройки (Setup).
ui_logic, viewmodel, state_management, user_setup, authentication_flow
Отображает экран со списком всех меток.
ui, labels_list, state_management, compose, dialog
Composable-функция для отображения списка меток.
ui, labels_list, state_management, compose, dialog
Composable-функция для отображения одного элемента в списке меток.
ui, labels_list, state_management, compose, dialog
ViewModel для экрана со списком меток.
Управляет состоянием экрана, загружает список меток, обрабатывает ошибки и управляет диалогом создания новой метки. @invariant `uiState` всегда является одним из состояний, определенных в `LabelsListUiState`. / @HiltViewModel class LabelsListViewModel @Inject constructor( private val getAllLabelsUseCase: GetAllLabelsUseCase ) : ViewModel() { private val _uiState = MutableStateFlow<LabelsListUiState>(LabelsListUiState.Loading) val uiState = _uiState.asStateFlow() init { loadLabels() } // [ENTITY: Function('loadLabels')] /** @summary Загружает список меток. @description Выполняет `GetAllLabelsUseCase` и обновляет UI, переключая его между состояниями `Loading`, `Success` и `Error`. @sideeffect Асинхронно обновляет `_uiState`. / fun loadLabels() { viewModelScope.launch { _uiState.value = LabelsListUiState.Loading Timber.i("[INFO][ENTRYPOINT][loading_labels] Starting labels list load. State -> Loading.") val result = runCatching { getAllLabelsUseCase() } result.fold( onSuccess = { labelOuts -> Timber.i("[INFO][SUCCESS][labels_loaded] Labels loaded successfully. Count: ${labelOuts.size}. State -> Success.") val labels = labelOuts.map { labelOut -> Label( id = labelOut.id, name = labelOut.name ) } _uiState.value = LabelsListUiState.Success(labels, isShowingCreateDialog = false) }, onFailure = { exception -> Timber.e(exception, "[ERROR][EXCEPTION][loading_failed] Failed to load labels. State -> Error.") _uiState.value = LabelsListUiState.Error( message = exception.message ?: "Could not load labels." ) } ) } } // [END_ENTITY: Function('loadLabels')] // [ENTITY: Function('onShowCreateDialog')] /** @summary Инициирует отображение диалога для создания метки. @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `true`. @sideeffect Обновляет `_uiState`. / fun onShowCreateDialog() { Timber.i("[INFO][ACTION][show_create_dialog] Show create label dialog requested.") if (_uiState.value is LabelsListUiState.Success) { _uiState.update { (it as LabelsListUiState.Success).copy(isShowingCreateDialog = true) } } } // [END_ENTITY: Function('onShowCreateDialog')] // [ENTITY: Function('onDismissCreateDialog')] /** @summary Скрывает диалог создания метки. @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `false`. @sideeffect Обновляет `_uiState`. / fun onDismissCreateDialog() { Timber.i("[INFO][ACTION][dismiss_create_dialog] Dismiss create label dialog requested.") if (_uiState.value is LabelsListUiState.Success) { _uiState.update { (it as LabelsListUiState.Success).copy(isShowingCreateDialog = false) } } } // [END_ENTITY: Function('onDismissCreateDialog')] // [ENTITY: Function('createLabel')] /** @summary Создает новую метку. [MVP_SCOPE] ЗАГЛУШКА. @description В текущей реализации (План Б, Этап 1), эта функция только логирует действие и скрывает диалог. Реальная логика сохранения будет добавлена на следующем этапе. @param name Название новой метки. @precondition `name` не должен быть пустым. @sideeffect Логирует действие, обновляет `_uiState`, чтобы скрыть диалог. / fun createLabel(name: String) { require(name.isNotBlank()) { "[CONTRACT_VIOLATION] Label name cannot be blank." } Timber.i("[INFO][ACTION][create_label] Create label called with name: '$name'. [STUBBED]") // [AI_NOTE]: Здесь будет вызов CreateLabelUseCase. onDismissCreateDialog() } // [END_ENTITY: Function('createLabel')] }
ui_logic, labels_list, state_management, dialog_management
Определяет все возможные состояния для UI экрана со списком меток.
Использование sealed-интерфейса позволяет исчерпывающе обрабатывать все состояния в Composable-функциях. / sealed interface LabelsListUiState { // [ENTITY: DataClass('Success')] // [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('Label')] /** @summary Состояние успеха, содержит список меток и состояние диалога. @param labels Список меток для отображения. @param isShowingCreateDialog Флаг, показывающий, должен ли быть отображен диалог создания метки. @invariant labels не может быть null. / data class Success( val labels: List<Label>, val isShowingCreateDialog: Boolean = false ) : LabelsListUiState // [END_ENTITY: DataClass('Success')] // [ENTITY: DataClass('Error')] /** @summary Состояние ошибки. @param message Текст ошибки для отображения пользователю. @invariant message не может быть пустой. / data class Error(val message: String) : LabelsListUiState // [END_ENTITY: DataClass('Error')] // [ENTITY: Object('Loading')] /** @summary Состояние загрузки данных. @description Указывает, что идет процесс загрузки меток. / data object Loading : LabelsListUiState // [END_ENTITY: Object('Loading')] }
ui_state, sealed_interface, contract
Экран для сканирования QR-кодов и штрих-кодов.
ui, screen, scan, camera, qrcode, barcode
ViewModel для экрана сканирования.
ui_logic, viewmodel, scan, camera
Определяет все возможные состояния для UI экрана сканирования.
ui_state, sealed_interface, contract
Анализатор изображений для обнаружения штрих-кодов.
data, service, barcode_scanning
Composable-функция для экрана "Редактирование элемента".
ui, screen, item, edit
UI state for the item edit screen.
ui, viewmodel, item_edit
ViewModel for the item edit screen.
ui, viewmodel, item_edit
Composable-функция для экрана "Поиск".
ui, screen, search
ViewModel for the search screen.
ui, viewmodel, search
Composable-функция для экрана "Редактирование местоположения".
ui, screen, location, edit
ViewModel for the item details screen.
ui, viewmodel, item_details
Composable-функция для экрана "Детали элемента".
ui, screen, item, details
Composable-функция для экрана "Список местоположений".
ui, screen, locations, list
Отображает основной контент экрана в зависимости от `uiState`.
ui, screen, locations, list
Карточка для отображения одного местоположения.
ui, screen, locations, list
ui, screen, locations, list
ui, screen, locations, list
ui, screen, locations, list
ui, screen, locations, list
ViewModel для экрана списка местоположений.
ui, viewmodel, locations, hilt
Определяет возможные состояния UI для экрана списка местоположений.
ui, state, locations
Компонент для выбора цвета.
ui, component, color_selection
Полноэкранный оверлей с индикатором загрузки.
ui, component, loading
Главная и единственная Activity в приложении.
ui, activity, entrypoint
ui, activity, entrypoint
ui, activity, entrypoint
Unit tests for [UpdateItemUseCase].
testing, usecase, unit_test
Сценарий использования для обновления метки.
business_logic, use_case, label, update
Сценарий использования для обновления местоположения.
business_logic, use_case, location, update
Use case для удаления вещи.
business_logic, use_case, item_deletion
Получает список всех локаций.
domain, usecase
Сценарий использования для получения списка недавно добавленных товаров.
domain, usecase
Use case для получения детальной информации о вещи.
business_logic, use_case, item_retrieval
Получает статистику инвентаря.
domain, usecase
Use case для выполнения входа пользователя.
domain, usecase, authentication
Use case для создания новой вещи.
business_logic, use_case, item_creation
Use case для синхронизации (получения) списка вещей.
business_logic, use_case, data_sync
Use case для поиска вещей по текстовому запросу.
business_logic, use_case, search
Сценарий использования для создания нового местоположения.
business_logic, use_case, location, create
Сценарий использования для удаления местоположения.
business_logic, use_case, location, delete
Сценарий использования для удаления метки.
business_logic, use_case, label, delete
Получает детальную информацию о метке по ее ID.
business_logic, use_case, label_retrieval
Use case для обновления существующей вещи.
business_logic, use_case, item_management
Сценарий использования для создания новой метки.
business_logic, use_case, label, create
Получает список всех меток.
domain, usecase
Модель с данными, необходимыми для создания новой метки.
data_structure, contract, label, create
Модель данных для представления агрегированной статистики.
data_structure, statistics
Модель данных для создания новой "Вещи".
data_structure, entity, input, create
Представляет собой метку (тег), которую можно присвоить вещи.
domain, model
Модель с данными, необходимыми для обновления местоположения.
data_structure, contract, location, update
Модель данных, представляющая ответ от сервера с токеном аутентификации.
data_transfer_object, authentication, model
Представляет собой результат операции, который может быть либо успешным, либо неуспешным.
domain, model, result
Модель данных для представления кастомного поля.
data_structure, entity, custom_field
Представляет краткую информацию о метке, обычно возвращаемую после создания.
data_structure, entity, label, summary
Модель данных для представления местоположения (без счетчика).
data_structure, entity, location
Модель с данными, необходимыми для обновления метки.
data_structure, contract, label, update
Модель данных для представления изображения, привязанного к вещи.
data_structure, entity, image
Модель с данными, необходимыми для создания нового местоположения.
data_structure, contract, location, create
Data class to hold server credentials.
domain, model, credentials
Полная модель данных для представления "Вещи" со всеми полями.
data_structure, entity, detailed
Модель данных для обновления существующей "Вещи".
data_structure, entity, input, update
Модель данных для представления метки (тега).
data_structure, entity, label
Модель данных для представления вложения (файла), привязанного к вещи.
data_structure, entity, attachment
Сокращенная модель данных для представления "Вещи" в списках.
data_structure, entity, summary
Представляет собой статистику по инвентарю.
domain, model
Модель данных для представления местоположения со счетчиком вещей.
data_structure, entity, location
Представляет собой местоположение, где может находиться вещь.
domain, model
Модель данных для записи о техническом обслуживании.
data_structure, entity, maintenance
Представляет собой вещь в инвентаре.
domain, model
Репозиторий для управления аутентификацией.
authentication, data_access, repository
Абстракция репозитория для работы с "Вещами".
Определяет контракт, которому должен следовать слой данных. / interface ItemRepository { // [ENTITY: Function('createItem')] // [RELATION: Function('createItem')] -> [RETURNS] -> [DataClass('ItemSummary')] /** @summary Создает новый элемент. @param newItemData Данные для создания нового элемента. @return Сводка по созданному элементу. / suspend fun createItem(newItemData: ItemCreate): ItemSummary // [END_ENTITY: Function('createItem')] // [ENTITY: Function('getItemDetails')] // [RELATION: Function('getItemDetails')] -> [RETURNS] -> [DataClass('ItemOut')] /** @summary Получает детальную информацию об элементе. @param itemId ID элемента. @return Детальная информация об элементе. / suspend fun getItemDetails(itemId: String): ItemOut // [END_ENTITY: Function('getItemDetails')] // [ENTITY: Function('updateItem')] // [RELATION: Function('updateItem')] -> [RETURNS] -> [DataClass('ItemOut')] /** @summary Обновляет элемент. @param itemId ID элемента для обновления. @param item Данные для обновления элемента. @return Обновленная детальная информация об элементе. / suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut // [END_ENTITY: Function('updateItem')] // [ENTITY: Function('deleteItem')] /** @summary Удаляет элемент. @param itemId ID элемента для удаления. / suspend fun deleteItem(itemId: String) // [END_ENTITY: Function('deleteItem')] // [ENTITY: Function('syncInventory')] // [RELATION: Function('syncInventory')] -> [RETURNS] -> [DataClass('PaginationResult<ItemSummary>')] /** @summary Синхронизирует инвентарь. @param page Номер страницы. @param pageSize Размер страницы. @return Результат пагинации со сводкой по элементам. / suspend fun syncInventory(page: Int, pageSize: Int): PaginationResult<ItemSummary> // [END_ENTITY: Function('syncInventory')] // [ENTITY: Function('getStatistics')] // [RELATION: Function('getStatistics')] -> [RETURNS] -> [DataClass('GroupStatistics')] /** @summary Получает статистику. @return Статистика по группам. / suspend fun getStatistics(): GroupStatistics // [END_ENTITY: Function('getStatistics')] // [ENTITY: Function('getAllLocations')] // [RELATION: Function('getAllLocations')] -> [RETURNS] -> [DataStructure('List<LocationOutCount>')] /** @summary Получает все местоположения. @return Список всех местоположений со счетчиками. / suspend fun getAllLocations(): List<LocationOutCount> // [END_ENTITY: Function('getAllLocations')] // [ENTITY: Function('getAllLabels')] // [RELATION: Function('getAllLabels')] -> [RETURNS] -> [DataStructure('List<LabelOut>')] /** @summary Получает все метки. @return Список всех меток. / suspend fun getAllLabels(): List<LabelOut> // [END_ENTITY: Function('getAllLabels')] // [ENTITY: Function('getLabelDetails')] // [RELATION: Function('getLabelDetails')] -> [RETURNS] -> [DataClass('LabelOut')] /** @summary Получает детальную информацию о метке. @param labelId ID метки. @return Детальная информация о метке. / suspend fun getLabelDetails(labelId: String): LabelOut // [END_ENTITY: Function('getLabelDetails')] // [ENTITY: Function('createLabel')] // [RELATION: Function('createLabel')] -> [RETURNS] -> [DataClass('LabelSummary')] /** @summary Создает новую метку. @param newLabelData Данные для создания новой метки. @return Сводка по созданной метке. / suspend fun createLabel(newLabelData: LabelCreate): LabelSummary // [END_ENTITY: Function('createLabel')] // [ENTITY: Function('updateLabel')] // [RELATION: Function('updateLabel')] -> [RETURNS] -> [DataClass('LabelOut')] /** @summary Обновляет метку. @param labelId ID метки для обновления. @param labelData Данные для обновления метки. @return Обновленная информация о метке. / suspend fun updateLabel(labelId: String, labelData: LabelUpdate): LabelOut // [END_ENTITY: Function('updateLabel')] // [ENTITY: Function('deleteLabel')] /** @summary Удаляет метку. @param labelId ID метки для удаления. / suspend fun deleteLabel(labelId: String) // [END_ENTITY: Function('deleteLabel')] // [ENTITY: Function('createLocation')] // [RELATION: Function('createLocation')] -> [RETURNS] -> [DataClass('LocationOut')] /** @summary Создает новое местоположение. @param newLocationData Данные для создания нового местоположения. @return Информация о созданном местоположении. / suspend fun createLocation(newLocationData: LocationCreate): LocationOut // [END_ENTITY: Function('createLocation')] // [ENTITY: Function('updateLocation')] // [RELATION: Function('updateLocation')] -> [RETURNS] -> [DataClass('LocationOut')] /** @summary Обновляет местоположение. @param locationId ID местоположения для обновления. @param locationData Данные для обновления местоположения. @return Обновленная информация о местоположении. / suspend fun updateLocation(locationId: String, locationData: LocationUpdate): LocationOut // [END_ENTITY: Function('updateLocation')] // [ENTITY: Function('deleteLocation')] /** @summary Удаляет местоположение. @param locationId ID местоположения для удаления. / suspend fun deleteLocation(locationId: String) // [END_ENTITY: Function('deleteLocation')] // [ENTITY: Function('searchItems')] // [RELATION: Function('searchItems')] -> [RETURNS] -> [DataClass('PaginationResult<ItemSummary>')] /** @summary Ищет элементы. @param query Поисковый запрос. @return Результат пагинации со сводкой по найденным элементам. / suspend fun searchItems(query: String): PaginationResult<ItemSummary> // [END_ENTITY: Function('searchItems')] // [ENTITY: Function('getRecentlyAddedItems')] // [RELATION: Function('getRecentlyAddedItems')] -> [RETURNS] -> [DataStructure('Flow<List<ItemSummary>>')] /** @summary Получает недавно добавленные элементы. @param limit Максимальное количество возвращаемых элементов. @return Поток со списком недавно добавленных элементов. / fun getRecentlyAddedItems(limit: Int): Flow<List<ItemSummary>> // [END_ENTITY: Function('getRecentlyAddedItems')] }
data_access, abstraction, repository
Repository for managing user credentials and session tokens.
domain, repository, credentials
Hilt-модуль для предоставления реализаций репозиториев.
Использует `@Binds` для эффективного связывания интерфейсов с их реализациями. / @Module @InstallIn(SingletonComponent::class) abstract class RepositoryModule { // [ENTITY: Function('bindItemRepository')] // [RELATION: Function('bindItemRepository')] -> [PROVIDES] -> [Interface('ItemRepository')] /** @summary Связывает интерфейс ItemRepository с его реализацией. / @Binds @Singleton abstract fun bindItemRepository( itemRepositoryImpl: ItemRepositoryImpl ): ItemRepository // [END_ENTITY: Function('bindItemRepository')] // [ENTITY: Function('bindCredentialsRepository')] // [RELATION: Function('bindCredentialsRepository')] -> [PROVIDES] -> [Interface('CredentialsRepository')] /** @summary Связывает интерфейс CredentialsRepository с его реализацией. / @Binds @Singleton abstract fun bindCredentialsRepository( credentialsRepositoryImpl: CredentialsRepositoryImpl ): CredentialsRepository // [END_ENTITY: Function('bindCredentialsRepository')] // [ENTITY: Function('bindAuthRepository')] // [RELATION: Function('bindAuthRepository')] -> [PROVIDES] -> [Interface('AuthRepository')] /** @summary Связывает интерфейс AuthRepository с его реализацией. / @Binds @Singleton abstract fun bindAuthRepository( authRepositoryImpl: AuthRepositoryImpl ): AuthRepository // [END_ENTITY: Function('bindAuthRepository')] }
dependency_injection, hilt, module, binding
Hilt-модуль, отвечающий за создание и предоставление всех зависимостей,
di, networking
di, hilt, storage
Предоставляет зависимости для работы с базой данных Room.
di, hilt, database
Определяет эндпоинты для взаимодействия с Homebox API, используя DTO.
data, api, retrofit
DTO для полной модели вещи.
data_transfer_object, item_detailed
Маппер из ItemOutDto в доменную модель ItemOut.
data_transfer_object, item_detailed
DTO для местоположения со счетчиком.
data_transfer_object, location, count
Маппер из LocationOutCountDto в доменную модель LocationOutCount.
data_transfer_object, location, count
DTO для создания вещи.
data_transfer_object, item_creation
Маппер из доменной модели ItemCreate в ItemCreateDto.
data_transfer_object, item_creation
data, dto, api, token
DTO для полной информации о вещи (GET /v1/items/{id}).
data, dto, api
DTO для краткой информации о вещи в списках (GET /v1/items).
data, dto, api
DTO для создания новой вещи (POST /v1/items).
data, dto, api
DTO для обновления вещи (PUT /v1/items/{id}).
data, dto, api
data_transfer_object, location, output
data_transfer_object, location, output
Маппер из PaginationResultDto в доменную модель PaginationResult.
data_transfer_object, pagination
DTO для обновления вещи.
data_transfer_object, item_update
Маппер из доменной модели ItemUpdate в ItemUpdateDto.
data_transfer_object, item_update
data_transfer_object, location, update
data_transfer_object, location, update
DTO для изображения.
data_transfer_object, image
Маппер из ImageDto в доменную модель Image.
data_transfer_object, image
DTO для кастомного поля.
data_transfer_object, custom_field
Маппер из CustomFieldDto в доменную модель CustomField.
data_transfer_object, custom_field
DTO для статистики.
data_transfer_object, statistics
Маппер из GroupStatisticsDto в доменную модель GroupStatistics.
data_transfer_object, statistics
DTO для ответа от API при создании метки.
data_transfer_object, label, summary, api, mapper
Маппер из DTO в доменную модель.
data_transfer_object, label, summary, api, mapper
DTO для записи об обслуживании.
data_transfer_object, maintenance
Маппер из MaintenanceEntryDto в доменную модель MaintenanceEntry.
data_transfer_object, maintenance
data_transfer_object, location, create
DTO для метки.
data_transfer_object, label
Маппер из LabelOutDto в доменную модель LabelOut.
data_transfer_object, label
DTO для статистической информации.
data, dto, api, statistics
DTO для информации о местоположении.
data, dto, api, location
DTO для информации о местоположении со счетчиком вещей.
data, dto, api, location
DTO для вложения.
data_transfer_object, attachment
Маппер из ItemAttachmentDto в доменную модель ItemAttachment.
data_transfer_object, attachment
data, dto, api, login
DTO для тела запроса на создание метки (POST /v1/labels).
data_transfer_object, label, create, api
DTO для сокращенной модели вещи.
data_transfer_object, item_summary
Маппер из ItemSummaryDto в доменную модель ItemSummary.
data_transfer_object, item_summary
data_transfer_object, label, update
data_transfer_object, label, update
Преобразует DTO-объект токена в доменную модель.
mapper, data_conversion, clean_architecture
Основной класс для работы с локальной базой данных Room.
data, database, room
Представляет собой строку в таблице 'labels' в локальной БД.
data, database, entity, label
Таблица для связи "многие-ко-многим" между ItemEntity и LabelEntity.
data, database, entity, relation
Представляет собой строку в таблице 'items' в локальной БД.
data, database, entity, item
Представляет собой строку в таблице 'locations' в локальной БД.
data, database, entity, location
Преобразует [ItemWithLabels] (сущность БД) в [ItemSummary] (доменную модель).
data, database, mapper
Преобразует [LabelEntity] (сущность БД) в [LabelOut] (доменную модель).
data, database, mapper
POJO для получения ItemEntity вместе со связанными LabelEntity.
data, database, entity, relation
Предоставляет TypeConverters для Room для типов, не поддерживаемых по умолчанию.
data, database, room, converter
Предоставляет методы для работы с 'labels' в локальной БД.
data, database, dao, label
Предоставляет методы для работы с 'items' в локальной БД.
data, database, dao, item
Предоставляет методы для работы с 'locations' в локальной БД.
data, database, dao, location
A manager for handling encryption and decryption using the Android Keystore system.
This class ensures that cryptographic keys are stored securely. It is designed to be a Singleton provided by Hilt. @invariant The underlying SecretKey must be valid within the AndroidKeyStore. / @RequiresApi(Build.VERSION_CODES.M) @Singleton class CryptoManager @Inject constructor() { private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } private val encryptCipher get() = Cipher.getInstance(TRANSFORMATION).apply { init(Cipher.ENCRYPT_MODE, getKey()) } private fun getDecryptCipherForIv(iv: ByteArray): Cipher { return Cipher.getInstance(TRANSFORMATION).apply { init(Cipher.DECRYPT_MODE, getKey(), IvParameterSpec(iv)) } } private fun getKey(): SecretKey { val existingKey = keyStore.getEntry(ALIAS, null) as? KeyStore.SecretKeyEntry return existingKey?.secretKey ?: createKey() } private fun createKey(): SecretKey { return KeyGenerator.getInstance(ALGORITHM).apply { init( KeyGenParameterSpec.Builder( ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT ) .setBlockModes(BLOCK_MODE) .setEncryptionPaddings(PADDING) .setUserAuthenticationRequired(false) .setRandomizedEncryptionRequired(true) .build() ) }.generateKey() } // [ENTITY: Function('encrypt')] /** @summary Encrypts a byte array and writes it to an output stream. @param bytes The byte array to encrypt. @param outputStream The stream to write the encrypted data to. @return The encrypted byte array. / fun encrypt(bytes: ByteArray, outputStream: OutputStream): ByteArray { Timber.d("[DEBUG][ACTION][encrypting_data] Encrypting data.") val cipher = encryptCipher val encryptedBytes = cipher.doFinal(bytes) outputStream.use { it.write(cipher.iv.size) it.write(cipher.iv) it.write(encryptedBytes.size) it.write(encryptedBytes) } return encryptedBytes } // [END_ENTITY: Function('encrypt')] // [ENTITY: Function('decrypt')] /** @summary Decrypts a byte array from an input stream. @param inputStream The stream to read the encrypted data from. @return The decrypted byte array. / fun decrypt(inputStream: InputStream): ByteArray { Timber.d("[DEBUG][ACTION][decrypting_data] Decrypting data.") return inputStream.use { val ivSize = it.read() val iv = ByteArray(ivSize) it.read(iv) val encryptedBytesSize = it.read() val encryptedBytes = ByteArray(encryptedBytesSize) it.read(encryptedBytes) getDecryptCipherForIv(iv).doFinal(encryptedBytes) } } // [END_ENTITY: Function('decrypt')] companion object { private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC private const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7 private const val TRANSFORMATION = "$ALGORITHM/$BLOCK_MODE/$PADDING" private const val ALIAS = "homebox_lens_secret_key" } }
data, security, cryptography
Реализует репозиторий для управления учетными данными пользователя.
Взаимодействует с зашифрованными SharedPreferences для сохранения и извлечения данных. @param encryptedPrefs Зашифрованное хранилище ключ-значение, предоставляемое Hilt. @invariant Состояние этого репозитория полностью зависит от содержимого `encryptedPrefs`. / class CredentialsRepositoryImpl @Inject constructor( private val encryptedPrefs: SharedPreferences ) : CredentialsRepository { companion object { private const val KEY_SERVER_URL = "key_server_url" private const val KEY_USERNAME = "key_username" private const val KEY_PASSWORD = "key_password" private const val KEY_AUTH_TOKEN = "key_auth_token" } // [ENTITY: Function('saveCredentials')] /** @summary Сохраняет основные учетные данные пользователя. @param credentials Объект с учетными данными для сохранения. @sideeffect Перезаписывает существующие учетные данные в SharedPreferences. / override suspend fun saveCredentials(credentials: Credentials) { withContext(Dispatchers.IO) { Timber.d("[DEBUG][ACTION][saving_credentials] Saving user credentials.") encryptedPrefs.edit() .putString(KEY_SERVER_URL, credentials.serverUrl) .putString(KEY_USERNAME, credentials.username) .putString(KEY_PASSWORD, credentials.password) .apply() } } // [END_ENTITY: Function('saveCredentials')] // [ENTITY: Function('getCredentials')] /** @summary Извлекает сохраненные учетные данные пользователя в виде потока. @return Flow, который эммитит объект [Credentials] или null, если данные отсутствуют. / override fun getCredentials(): Flow<Credentials?> = flow { Timber.d("[DEBUG][ACTION][getting_credentials] Getting user credentials.") val serverUrl = encryptedPrefs.getString(KEY_SERVER_URL, null) val username = encryptedPrefs.getString(KEY_USERNAME, null) val password = encryptedPrefs.getString(KEY_PASSWORD, null) if (serverUrl != null && username != null && password != null) { Timber.d("[DEBUG][SUCCESS][credentials_found] Found and emitting credentials.") emit(Credentials(serverUrl, username, password)) } else { Timber.d("[DEBUG][FALLBACK][no_credentials] No credentials found, emitting null.") emit(null) } }.flowOn(Dispatchers.IO) // [END_ENTITY: Function('getCredentials')] // [ENTITY: Function('saveToken')] /** @summary Сохраняет токен авторизации. @param token Токен для сохранения. @sideeffect Перезаписывает существующий токен в SharedPreferences. / override suspend fun saveToken(token: String) { withContext(Dispatchers.IO) { Timber.d("[DEBUG][ACTION][saving_token] Saving auth token.") encryptedPrefs.edit() .putString(KEY_AUTH_TOKEN, token) .apply() } } // [END_ENTITY: Function('saveToken')] // [ENTITY: Function('getToken')] /** @summary Извлекает сохраненный токен авторизации. @return Строка с токеном или null, если он не найден. / override suspend fun getToken(): String? { return withContext(Dispatchers.IO) { Timber.d("[DEBUG][ACTION][getting_token] Getting auth token.") encryptedPrefs.getString(KEY_AUTH_TOKEN, null) } } // [END_ENTITY: Function('getToken')] }
data, repository, credentials, security
data_repository, implementation, items, labels
data_repository, implementation, items, labels
data_repository, implementation, items, labels
data_repository, implementation, items, labels
Реализация репозитория для управления аутентификацией.
data_implementation, authentication, repository
Provides a simplified and secure interface for storing and retrieving sensitive string data.
It uses a CryptoManager to encrypt/decrypt data before writing/reading from a standard SharedPreferences instance. @param sharedPreferences The underlying standard SharedPreferences instance to store encrypted data. @param cryptoManager The manager responsible for all cryptographic operations. / class EncryptedPreferencesWrapper @Inject constructor( private val sharedPreferences: SharedPreferences, private val cryptoManager: CryptoManager ) { // [ENTITY: Function('getString')] /** @summary Retrieves a decrypted string value for a given key. @param key The key for the preference. @param defaultValue The value to return if the key is not found or decryption fails. @return The decrypted string, or the defaultValue. @sideeffect Reads from SharedPreferences. / fun getString(key: String, defaultValue: String?): String? { Timber.d("[DEBUG][ENTRYPOINT][getting_string] Attempting to get string for key: %s", key) val encryptedValue = sharedPreferences.getString(key, null) ?: return defaultValue.also { Timber.d("[DEBUG][FALLBACK][no_value_found] No value for key %s, returning default.", key) } return try { Timber.d("[DEBUG][ACTION][decoding_value] Decoding Base64 value.") val bytes = android.util.Base64.decode(encryptedValue, android.util.Base64.DEFAULT) Timber.d("[DEBUG][ACTION][decrypting_value] Decrypting value with CryptoManager.") val decryptedBytes = cryptoManager.decrypt(ByteArrayInputStream(bytes)) String(decryptedBytes, Charset.defaultCharset()).also { Timber.d("[DEBUG][SUCCESS][decryption_complete] Successfully decrypted value for key: %s", key) } } catch (e: Exception) { Timber.e(e, "[ERROR][EXCEPTION][decryption_failed] Failed to decrypt value for key: %s", key) defaultValue } } // [END_ENTITY: Function('getString')] // [ENTITY: Function('putString')] /** @summary Encrypts and saves a string value for a given key. @param key The key for the preference. @param value The string value to encrypt and save. @sideeffect Modifies the underlying SharedPreferences file. / fun putString(key: String, value: String) { Timber.d("[DEBUG][ENTRYPOINT][putting_string] Attempting to put string for key: %s", key) try { Timber.d("[DEBUG][ACTION][encrypting_value] Encrypting value with CryptoManager.") val outputStream = ByteArrayOutputStream() cryptoManager.encrypt(value.toByteArray(Charset.defaultCharset()), outputStream) val encryptedBytes = outputStream.toByteArray() Timber.d("[DEBUG][ACTION][encoding_value] Encoding encrypted value to Base64.") val encryptedValue = android.util.Base64.encodeToString(encryptedBytes, android.util.Base64.DEFAULT) Timber.d("[DEBUG][ACTION][writing_to_prefs] Writing encrypted value to SharedPreferences.") sharedPreferences.edit().putString(key, encryptedValue).apply() Timber.d("[DEBUG][SUCCESS][encryption_complete] Successfully encrypted and saved value for key: %s", key) } catch (e: Exception) { Timber.e(e, "[ERROR][EXCEPTION][encryption_failed] Failed to encrypt and save value for key: %s", key) } } // [END_ENTITY: Function('putString')] }
data, security, preferences
Маршрут для экрана настроек.
data object Settings : Screen("settings_screen")
navigation, route, settings
Composable-функция для экрана "Настройки".
Отображает UI для управления настройками приложения.
ui, screen, settings, compose
ViewModel для экрана "Настройки".
Управляет состоянием и логикой экрана настроек.
ui_logic, viewmodel, settings, state_management
Состояние UI для экрана настроек.
Содержит поля для URL сервера и других настроек.
ui_state, data_model, immutable, settings
Маршрут для экрана сканирования QR/штрих-кодов.
data object Scan : Screen("scan_screen")
navigation, route, scan
Экран для сканирования QR-кодов и штрих-кодов.
ui, screen, scan, camera, qrcode, barcode
ViewModel для экрана сканирования.
ui_logic, viewmodel, scan, camera
Определяет все возможные состояния для UI экрана сканирования.
ui_state, sealed_interface, contract