237 lines
11 KiB
XML
237 lines
11 KiB
XML
<!-- tasks/20250813_080300_implement_labels_screen.xml -->
|
||
<TASK status="completed">
|
||
<WORK_ORDER id="task-20250813080300-001">
|
||
<ACTION>MODIFY_CODE</ACTION>
|
||
|
||
<TARGET_FILE>app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt</TARGET_FILE>
|
||
|
||
<GOAL>
|
||
Реализовать UI для экрана "Метки" (`LabelsListScreen`) в соответствии со спецификацией `screen_labels_list`.
|
||
Это включает в себя создание Composable-функции, которая:
|
||
1. Использует `Scaffold` с `TopAppBar` и `FloatingActionButton`.
|
||
2. Получает состояние (список меток, статус загрузки, ошибки) от `LabelsListViewModel`.
|
||
3. Отображает список меток с помощью `LazyColumn`.
|
||
4. Обрабатывает клики по элементам списка для навигации на экран инвентаря с фильтром по метке.
|
||
5. Обрабатывает нажатие на FAB для создания новой метки (пока что через лог).
|
||
6. Отображает индикатор загрузки и сообщения об ошибках или пустом списке.
|
||
7. Строго следует принципам Design by Contract, использует иконки из гайда и строки из ресурсов.
|
||
</GOAL>
|
||
|
||
<CONTEXT_FILES>
|
||
<FILE>PROJECT_SPECIFICATION.xml</FILE>
|
||
<FILE>PROJECT_STRUCTURE.xml</FILE>
|
||
<FILE>app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListViewModel.kt</FILE>
|
||
<FILE>domain/src/main/java/com/homebox/lens/domain/model/Label.kt</FILE>
|
||
<FILE>app/src/main/java/com/homebox/lens/navigation/Screen.kt</FILE>
|
||
</CONTEXT_FILES>
|
||
|
||
<CONTRACT>
|
||
<![CDATA[
|
||
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
|
||
// [FILE] app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt
|
||
|
||
// [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.padding
|
||
import androidx.compose.foundation.lazy.LazyColumn
|
||
import androidx.compose.foundation.lazy.items
|
||
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.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.Scaffold
|
||
import androidx.compose.material3.Text
|
||
import androidx.compose.material3.TopAppBar
|
||
import androidx.compose.runtime.Composable
|
||
import androidx.compose.runtime.collectAsState
|
||
import androidx.compose.runtime.getValue
|
||
import androidx.compose.ui.Alignment
|
||
import androidx.compose.ui.Modifier
|
||
import androidx.compose.ui.res.stringResource
|
||
import androidx.compose.ui.unit.dp
|
||
import androidx.hilt.navigation.compose.hiltViewModel
|
||
import androidx.navigation.NavController
|
||
import com.homebox.lens.R
|
||
import com.homebox.lens.domain.model.Label
|
||
import timber.log.Timber
|
||
|
||
// [CONTRACT]
|
||
/**
|
||
* [CONTRACT]
|
||
* Отображает экран со списком всех меток.
|
||
*
|
||
* @param navController Контроллер навигации для перемещения между экранами.
|
||
* @param viewModel ViewModel, предоставляющая состояние UI для экрана меток.
|
||
*
|
||
* @precondition `navController` должен быть корректно инициализирован и способен обрабатывать навигационные события.
|
||
* @precondition `viewModel` должен быть доступен через Hilt.
|
||
* @postcondition Экран отображает список меток или соответствующее состояние (загрузка, ошибка, пустой список).
|
||
* @sideeffect Пользовательские действия (клики) инициируют навигационные команды через `navController` или логируются.
|
||
*/
|
||
@OptIn(ExperimentalMaterial3Api::class)
|
||
@Composable
|
||
fun LabelsListScreen(
|
||
navController: NavController,
|
||
viewModel: LabelsListViewModel = hiltViewModel()
|
||
) {
|
||
// [ACTION]
|
||
val uiState by viewModel.uiState.collectAsState()
|
||
val logger = Timber.tag("LabelsListScreen")
|
||
|
||
Scaffold(
|
||
topBar = {
|
||
TopAppBar(
|
||
title = { Text(text = stringResource(id = R.string.screen_title_labels)) },
|
||
navigationIcon = {
|
||
IconButton(onClick = {
|
||
logger.info { "[ACTION] Navigate up initiated." }
|
||
navController.navigateUp()
|
||
}) {
|
||
Icon(
|
||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||
contentDescription = stringResource(id = R.string.content_desc_navigate_back)
|
||
)
|
||
}
|
||
}
|
||
)
|
||
},
|
||
floatingActionButton = {
|
||
FloatingActionButton(onClick = {
|
||
// [ACTION]
|
||
// TODO: Открыть диалог или экран создания метки
|
||
logger.info { "[ACTION] FAB clicked: Initiate create new label flow." }
|
||
}) {
|
||
Icon(
|
||
imageVector = Icons.Filled.Add,
|
||
contentDescription = stringResource(id = R.string.content_desc_create_label)
|
||
)
|
||
}
|
||
}
|
||
) { paddingValues ->
|
||
// [CORE-LOGIC]
|
||
Box(
|
||
modifier = Modifier
|
||
.fillMaxSize()
|
||
.padding(paddingValues),
|
||
contentAlignment = Alignment.Center
|
||
) {
|
||
when {
|
||
uiState.isLoading -> {
|
||
// [STATE_BRANCH] Loading
|
||
CircularProgressIndicator()
|
||
}
|
||
uiState.error != null -> {
|
||
// [STATE_BRANCH] Error
|
||
Text(text = uiState.error ?: stringResource(id = R.string.error_unknown))
|
||
}
|
||
uiState.labels.isEmpty() -> {
|
||
// [STATE_BRANCH] Empty
|
||
Text(text = stringResource(id = R.string.labels_list_empty))
|
||
}
|
||
else -> {
|
||
// [STATE_BRANCH] Success
|
||
LabelsList(
|
||
labels = uiState.labels,
|
||
onLabelClick = { label ->
|
||
// [ACTION]
|
||
// TODO: Реализовать навигацию на экран инвентаря с фильтром
|
||
logger.info { "[ACTION] Label clicked: ${label.id}. Navigating to inventory list." }
|
||
// navController.navigate(Screen.InventoryList.withFilter("label", label.id))
|
||
}
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// [COHERENCE_CHECK_PASSED]
|
||
}
|
||
// [END_FUNCTION] LabelsListScreen
|
||
|
||
// [HELPER]
|
||
/**
|
||
* [CONTRACT]
|
||
* Composable-функция для отображения списка меток.
|
||
*
|
||
* @param labels Список объектов `Label` для отображения.
|
||
* @param onLabelClick Лямбда-функция, вызываемая при нажатии на элемент списка.
|
||
*
|
||
* @precondition `labels` не должен быть null.
|
||
* @postcondition Отображается вертикальный прокручиваемый список.
|
||
*/
|
||
@Composable
|
||
private fun LabelsList(
|
||
labels: List<Label>,
|
||
onLabelClick: (Label) -> Unit,
|
||
modifier: Modifier = Modifier
|
||
) {
|
||
// [CORE-LOGIC]
|
||
LazyColumn(
|
||
modifier = modifier.fillMaxSize(),
|
||
contentPadding = PaddingValues(16.dp),
|
||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||
) {
|
||
items(labels) { label ->
|
||
LabelListItem(
|
||
label = label,
|
||
onClick = { onLabelClick(label) }
|
||
)
|
||
}
|
||
}
|
||
}
|
||
// [END_FUNCTION] LabelsList
|
||
|
||
// [HELPER]
|
||
/**
|
||
* [CONTRACT]
|
||
* Composable-функция для отображения одного элемента в списке меток.
|
||
*
|
||
* @param label Объект `Label`, который нужно отобразить.
|
||
* @param onClick Лямбда-функция, вызываемая при нажатии на элемент.
|
||
*
|
||
* @precondition `label` не должен быть null.
|
||
* @postcondition Отображается кликабельный элемент списка с иконкой и названием метки.
|
||
*/
|
||
@Composable
|
||
private fun LabelListItem(
|
||
label: Label,
|
||
onClick: () -> Unit
|
||
) {
|
||
// [CORE-LOGIC]
|
||
ListItem(
|
||
headlineContent = { Text(text = label.name) },
|
||
leadingContent = {
|
||
Icon(
|
||
imageVector = Icons.AutoMirrored.Filled.Label,
|
||
contentDescription = stringResource(id = R.string.content_desc_label_icon)
|
||
)
|
||
},
|
||
modifier = Modifier.clickable(onClick = onClick)
|
||
)
|
||
}
|
||
// [END_FUNCTION] LabelListItem
|
||
|
||
// [END_FILE] LabelsListScreen.kt
|
||
]]>
|
||
</CONTRACT>
|
||
|
||
<IMPLEMENTATION_HINTS>
|
||
<HINT>Используйте `@HiltViewModel` для получения экземпляра `LabelsListViewModel`.</HINT>
|
||
<HINT>Собирайте `uiState` из ViewModel с помощью `collectAsState()` для автоматического обновления UI при изменении состояния.</HINT>
|
||
<HINT>Используйте `Scaffold` для базовой структуры экрана (TopAppBar, FAB, основное содержимое).</HINT>
|
||
<HINT>Для навигации назад используйте `navController.navigateUp()`.</HINT>
|
||
<HINT>Используйте `LazyColumn` для эффективного отображения потенциально длинных списков меток.</HINT>
|
||
<HINT>Обязательно добавьте новые строковые ресурсы (`screen_title_labels`, `content_desc_navigate_back`, `content_desc_create_label`, `labels_list_empty`, `content_desc_label_icon`) в `strings.xml`.</HINT>
|
||
<HINT>Иконки должны браться из `androidx.compose.material.icons` в соответствии с `ICONOGRAPHY_GUIDE`.</HINT>
|
||
</IMPLEMENTATION_HINTS>
|
||
</WORK_ORDER>
|
||
</TASK> |