Labels + Location list
This commit is contained in:
@@ -57,10 +57,28 @@ fun NavGraph() {
|
||||
ItemEditScreen()
|
||||
}
|
||||
composable(route = Screen.LabelsList.route) {
|
||||
LabelsListScreen()
|
||||
LabelsListScreen(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onLabelClick = { labelId ->
|
||||
// TODO: Navigate to a pre-filtered inventory list screen
|
||||
navController.navigate(Screen.InventoryList.route)
|
||||
},
|
||||
onAddNewLabelClick = {
|
||||
// TODO: Navigate to a screen for creating a new label
|
||||
}
|
||||
)
|
||||
}
|
||||
composable(route = Screen.LocationsList.route) {
|
||||
LocationsListScreen()
|
||||
LocationsListScreen(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onLocationClick = { locationId ->
|
||||
// TODO: Navigate to a pre-filtered inventory list screen
|
||||
navController.navigate(Screen.InventoryList.route)
|
||||
},
|
||||
onAddNewLocationClick = {
|
||||
// TODO: Navigate to a screen for creating a new location
|
||||
}
|
||||
)
|
||||
}
|
||||
composable(route = Screen.Search.route) {
|
||||
SearchScreen()
|
||||
|
||||
@@ -1,22 +1,172 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
|
||||
// [FILE] LabelsListScreen.kt
|
||||
|
||||
// [SEMANTICS] ui, screen, labels_list, compose
|
||||
package com.homebox.lens.ui.screen.labelslist
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.*
|
||||
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.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.homebox.lens.R
|
||||
import com.homebox.lens.domain.model.LabelOut
|
||||
import com.homebox.lens.ui.theme.HomeboxLensTheme
|
||||
|
||||
// [ENTRYPOINT]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Главная Composable-функция для экрана списка меток.
|
||||
* @param onNavigateBack Функция для навигации на предыдущий экран.
|
||||
* @param onLabelClick Функция, вызываемая при нажатии на метку.
|
||||
* @param onAddNewLabelClick Функция, вызываемая при нажатии на FAB для добавления новой метки.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LabelsListScreen() {
|
||||
// [ACTION]
|
||||
Text(text = "Labels List Screen")
|
||||
fun LabelsListScreen(
|
||||
viewModel: LabelsListViewModel = hiltViewModel(),
|
||||
onNavigateBack: () -> Unit,
|
||||
onLabelClick: (String) -> Unit,
|
||||
onAddNewLabelClick: () -> Unit
|
||||
) {
|
||||
// [STATE]
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
// [UI]
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(id = R.string.nav_labels)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = stringResource(id = R.string.cd_navigate_back)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = onAddNewLabelClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = stringResource(id = R.string.cd_add_new_label)
|
||||
)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
// [ANCHOR] Основной контент в зависимости от состояния
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
when (val state = uiState) {
|
||||
is LabelsListUiState.Loading -> {
|
||||
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||
}
|
||||
is LabelsListUiState.Error -> {
|
||||
Text(
|
||||
text = state.message,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.align(Alignment.Center).padding(16.dp)
|
||||
)
|
||||
}
|
||||
is LabelsListUiState.Success -> {
|
||||
LabelsList(
|
||||
labels = state.labels,
|
||||
onLabelClick = onLabelClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
// [HELPER]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Отображает список меток.
|
||||
*/
|
||||
@Composable
|
||||
fun LabelsListScreenPreview() {
|
||||
LabelsListScreen()
|
||||
private fun LabelsList(
|
||||
labels: List<LabelOut>,
|
||||
onLabelClick: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
items(labels) { label ->
|
||||
LabelListItem(
|
||||
label = label,
|
||||
onClick = { onLabelClick(label.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_FILE_LabelsListScreen.kt]
|
||||
|
||||
// [HELPER]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Отображает один элемент списка меток.
|
||||
*/
|
||||
@Composable
|
||||
private fun LabelListItem(
|
||||
label: LabelOut,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(label.name) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PlayArrow,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable(onClick = onClick)
|
||||
)
|
||||
}
|
||||
|
||||
// [PREVIEW]
|
||||
@Preview(showBackground = true, name = "Labels List Success")
|
||||
@Composable
|
||||
private fun LabelsListScreenSuccessPreview() {
|
||||
val labels = listOf(
|
||||
LabelOut("1", "Electronics", "#FF0000", false, "", ""),
|
||||
LabelOut("2", "Books", "#00FF00", false, "", ""),
|
||||
LabelOut("3", "Winter Clothes", "#0000FF", false, "", "")
|
||||
)
|
||||
HomeboxLensTheme {
|
||||
LabelsList(labels = labels, onLabelClick = {})
|
||||
}
|
||||
}
|
||||
|
||||
// [PREVIEW]
|
||||
@Preview(showBackground = true, name = "Labels List Empty")
|
||||
@Composable
|
||||
private fun LabelsListScreenEmptyPreview() {
|
||||
HomeboxLensTheme {
|
||||
LabelsList(labels = emptyList(), onLabelClick = {})
|
||||
}
|
||||
}
|
||||
// [END_FILE_LabelsListScreen.kt]
|
||||
@@ -0,0 +1,36 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
|
||||
// [FILE] LabelsListUiState.kt
|
||||
// [SEMANTICS] ui, state, labels_list
|
||||
package com.homebox.lens.ui.screen.labelslist
|
||||
|
||||
import com.homebox.lens.domain.model.LabelOut
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: SealedInterface('LabelsListUiState')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Определяет все возможные состояния для экрана "Список меток".
|
||||
* @invariant В любой момент времени экран может находиться только в одном из этих состояний.
|
||||
*/
|
||||
sealed interface LabelsListUiState {
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Состояние успешной загрузки данных.
|
||||
* @property labels Список меток.
|
||||
*/
|
||||
data class Success(val labels: List<LabelOut>) : LabelsListUiState
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Состояние ошибки во время загрузки данных.
|
||||
* @property message Человекочитаемое сообщение об ошибке.
|
||||
*/
|
||||
data class Error(val message: String) : LabelsListUiState
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Состояние, когда данные для экрана загружаются.
|
||||
*/
|
||||
data object Loading : LabelsListUiState
|
||||
}
|
||||
// [END_FILE_LabelsListUiState.kt]
|
||||
@@ -1,16 +1,73 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
|
||||
// [FILE] LabelsListViewModel.kt
|
||||
|
||||
// [SEMANTICS] ui_logic, labels_list, state_management
|
||||
package com.homebox.lens.ui.screen.labelslist
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
// [VIEWMODEL]
|
||||
// [ENTITY: ViewModel('LabelsListViewModel')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary ViewModel для экрана со списком меток.
|
||||
* @description Управляет состоянием экрана, загружает список меток и обрабатывает ошибки.
|
||||
* @invariant `uiState` всегда является одним из состояний, определенных в `LabelsListUiState`.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class LabelsListViewModel @Inject constructor() : ViewModel() {
|
||||
class LabelsListViewModel @Inject constructor(
|
||||
private val getAllLabelsUseCase: GetAllLabelsUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
// [STATE]
|
||||
// TODO: Implement UI state
|
||||
private val _uiState = MutableStateFlow<LabelsListUiState>(LabelsListUiState.Loading)
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
// [LIFECYCLE_HANDLER]
|
||||
init {
|
||||
loadLabels()
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Загружает список меток.
|
||||
* @description Выполняет `GetAllLabelsUseCase` и обновляет UI, переключая его
|
||||
* между состояниями `Loading`, `Success` и `Error`.
|
||||
* @sideeffect Асинхронно обновляет `_uiState`.
|
||||
*/
|
||||
fun loadLabels() {
|
||||
// [ENTRYPOINT]
|
||||
viewModelScope.launch {
|
||||
_uiState.value = LabelsListUiState.Loading
|
||||
Timber.i("[ACTION] Starting labels list load. State -> Loading.")
|
||||
|
||||
// [CORE-LOGIC]
|
||||
val result = runCatching {
|
||||
getAllLabelsUseCase()
|
||||
}
|
||||
|
||||
// [RESULT_HANDLER]
|
||||
result.fold(
|
||||
onSuccess = { labels ->
|
||||
Timber.i("[SUCCESS] Labels loaded successfully. Count: ${labels.size}. State -> Success.")
|
||||
_uiState.value = LabelsListUiState.Success(labels)
|
||||
},
|
||||
onFailure = { exception ->
|
||||
Timber.e(exception, "[ERROR] Failed to load labels. State -> Error.")
|
||||
_uiState.value = LabelsListUiState.Error(
|
||||
message = exception.message ?: "Could not load labels."
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_CLASS_LabelsListViewModel]
|
||||
}
|
||||
// [END_FILE_LabelsListViewModel.kt]
|
||||
// [END_FILE_LabelsListViewModel.kt]
|
||||
@@ -1,22 +1,175 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
|
||||
// [FILE] LocationsListScreen.kt
|
||||
|
||||
// [SEMANTICS] ui, screen, locations_list, compose
|
||||
package com.homebox.lens.ui.screen.locationslist
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Place
|
||||
import androidx.compose.material3.*
|
||||
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.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.homebox.lens.R
|
||||
import com.homebox.lens.domain.model.LocationOutCount
|
||||
import com.homebox.lens.ui.theme.HomeboxLensTheme
|
||||
|
||||
// [ENTRYPOINT]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Главная Composable-функция для экрана списка локаций.
|
||||
* @param onNavigateBack Функция для навигации на предыдущий экран.
|
||||
* @param onLocationClick Функция, вызываемая при нажатии на локацию.
|
||||
* @param onAddNewLocationClick Функция, вызываемая при нажатии на FAB для добавления новой локации.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LocationsListScreen() {
|
||||
// [ACTION]
|
||||
Text(text = "Locations List Screen")
|
||||
fun LocationsListScreen(
|
||||
viewModel: LocationsListViewModel = hiltViewModel(),
|
||||
onNavigateBack: () -> Unit,
|
||||
onLocationClick: (String) -> Unit,
|
||||
onAddNewLocationClick: () -> Unit
|
||||
) {
|
||||
// [STATE]
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
// [UI]
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(id = R.string.nav_locations)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = stringResource(id = R.string.cd_navigate_back)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = onAddNewLocationClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = stringResource(id = R.string.cd_add_new_location)
|
||||
)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
// [ANCHOR] Основной контент в зависимости от состояния
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
when (val state = uiState) {
|
||||
is LocationsListUiState.Loading -> {
|
||||
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||
}
|
||||
is LocationsListUiState.Error -> {
|
||||
Text(
|
||||
text = state.message,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.align(Alignment.Center).padding(16.dp)
|
||||
)
|
||||
}
|
||||
is LocationsListUiState.Success -> {
|
||||
LocationsList(
|
||||
locations = state.locations,
|
||||
onLocationClick = onLocationClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
// [HELPER]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Отображает список локаций.
|
||||
*/
|
||||
@Composable
|
||||
fun LocationsListScreenPreview() {
|
||||
LocationsListScreen()
|
||||
private fun LocationsList(
|
||||
locations: List<LocationOutCount>,
|
||||
onLocationClick: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
items(locations) { location ->
|
||||
LocationListItem(
|
||||
location = location,
|
||||
onClick = { onLocationClick(location.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_FILE_LocationsListScreen.kt]
|
||||
|
||||
// [HELPER]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Отображает один элемент списка локаций.
|
||||
*/
|
||||
@Composable
|
||||
private fun LocationListItem(
|
||||
location: LocationOutCount,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(location.name) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Place,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Text(text = location.itemCount.toString())
|
||||
},
|
||||
modifier = Modifier.clickable(onClick = onClick)
|
||||
)
|
||||
}
|
||||
|
||||
// [PREVIEW]
|
||||
@Preview(showBackground = true, name = "Locations List Success")
|
||||
@Composable
|
||||
private fun LocationsListScreenSuccessPreview() {
|
||||
val locations = listOf(
|
||||
LocationOutCount("1", "Garage", "#FF0000", false, 12, "", ""),
|
||||
LocationOutCount("2", "Kitchen", "#00FF00", false, 3, "", ""),
|
||||
LocationOutCount("3", "Office", "#0000FF", false, 25, "", "")
|
||||
)
|
||||
HomeboxLensTheme {
|
||||
LocationsList(locations = locations, onLocationClick = {})
|
||||
}
|
||||
}
|
||||
|
||||
// [PREVIEW]
|
||||
@Preview(showBackground = true, name = "Locations List Empty")
|
||||
@Composable
|
||||
private fun LocationsListScreenEmptyPreview() {
|
||||
HomeboxLensTheme {
|
||||
LocationsList(locations = emptyList(), onLocationClick = {})
|
||||
}
|
||||
}
|
||||
// [END_FILE_LocationsListScreen.kt]
|
||||
@@ -0,0 +1,36 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
|
||||
// [FILE] LocationsListUiState.kt
|
||||
// [SEMANTICS] ui, state, locations_list
|
||||
package com.homebox.lens.ui.screen.locationslist
|
||||
|
||||
import com.homebox.lens.domain.model.LocationOutCount
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: SealedInterface('LocationsListUiState')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Определяет все возможные состояния для экрана "Список локаций".
|
||||
* @invariant В любой момент времени экран может находиться только в одном из этих состояний.
|
||||
*/
|
||||
sealed interface LocationsListUiState {
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Состояние успешной загрузки данных.
|
||||
* @property locations Список локаций со счетчиками.
|
||||
*/
|
||||
data class Success(val locations: List<LocationOutCount>) : LocationsListUiState
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Состояние ошибки во время загрузки данных.
|
||||
* @property message Человекочитаемое сообщение об ошибке.
|
||||
*/
|
||||
data class Error(val message: String) : LocationsListUiState
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Состояние, когда данные для экрана загружаются.
|
||||
*/
|
||||
data object Loading : LocationsListUiState
|
||||
}
|
||||
// [END_FILE_LocationsListUiState.kt]
|
||||
@@ -1,16 +1,73 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
|
||||
// [FILE] LocationsListViewModel.kt
|
||||
|
||||
// [SEMANTICS] ui_logic, locations_list, state_management
|
||||
package com.homebox.lens.ui.screen.locationslist
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
// [VIEWMODEL]
|
||||
// [ENTITY: ViewModel('LocationsListViewModel')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary ViewModel для экрана со списком локаций.
|
||||
* @description Управляет состоянием экрана, загружает список локаций и обрабатывает ошибки.
|
||||
* @invariant `uiState` всегда является одним из состояний, определенных в `LocationsListUiState`.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class LocationsListViewModel @Inject constructor() : ViewModel() {
|
||||
class LocationsListViewModel @Inject constructor(
|
||||
private val getAllLocationsUseCase: GetAllLocationsUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
// [STATE]
|
||||
// TODO: Implement UI state
|
||||
private val _uiState = MutableStateFlow<LocationsListUiState>(LocationsListUiState.Loading)
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
// [LIFECYCLE_HANDLER]
|
||||
init {
|
||||
loadLocations()
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Загружает список локаций.
|
||||
* @description Выполняет `GetAllLocationsUseCase` и обновляет UI, переключая его
|
||||
* между состояниями `Loading`, `Success` и `Error`.
|
||||
* @sideeffect Асинхронно обновляет `_uiState`.
|
||||
*/
|
||||
fun loadLocations() {
|
||||
// [ENTRYPOINT]
|
||||
viewModelScope.launch {
|
||||
_uiState.value = LocationsListUiState.Loading
|
||||
Timber.i("[ACTION] Starting locations list load. State -> Loading.")
|
||||
|
||||
// [CORE-LOGIC]
|
||||
val result = runCatching {
|
||||
getAllLocationsUseCase()
|
||||
}
|
||||
|
||||
// [RESULT_HANDLER]
|
||||
result.fold(
|
||||
onSuccess = { locations ->
|
||||
Timber.i("[SUCCESS] Locations loaded successfully. Count: ${locations.size}. State -> Success.")
|
||||
_uiState.value = LocationsListUiState.Success(locations)
|
||||
},
|
||||
onFailure = { exception ->
|
||||
Timber.e(exception, "[ERROR] Failed to load locations. State -> Error.")
|
||||
_uiState.value = LocationsListUiState.Error(
|
||||
message = exception.message ?: "Could not load locations."
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_CLASS_LocationsListViewModel]
|
||||
}
|
||||
// [END_FILE_LocationsListViewModel.kt]
|
||||
// [END_FILE_LocationsListViewModel.kt]
|
||||
@@ -12,6 +12,9 @@
|
||||
<!-- Content Descriptions -->
|
||||
<string name="cd_open_navigation_drawer">Open navigation drawer</string>
|
||||
<string name="cd_scan_qr_code">Scan QR code</string>
|
||||
<string name="cd_navigate_back">Navigate back</string>
|
||||
<string name="cd_add_new_location">Add new location</string>
|
||||
<string name="cd_add_new_label">Add new label</string>
|
||||
|
||||
<!-- Dashboard Screen -->
|
||||
<string name="dashboard_title">Dashboard</string>
|
||||
@@ -28,5 +31,6 @@
|
||||
|
||||
<!-- Navigation -->
|
||||
<string name="nav_locations">Locations</string>
|
||||
<string name="nav_labels">Labels</string>
|
||||
|
||||
</resources>
|
||||
@@ -12,6 +12,9 @@
|
||||
<!-- Content Descriptions -->
|
||||
<string name="cd_open_navigation_drawer">Открыть боковое меню</string>
|
||||
<string name="cd_scan_qr_code">Сканировать QR-код</string>
|
||||
<string name="cd_navigate_back">Вернуться назад</string>
|
||||
<string name="cd_add_new_location">Добавить новую локацию</string>
|
||||
<string name="cd_add_new_label">Добавить новую метку</string>
|
||||
|
||||
<!-- Dashboard Screen -->
|
||||
<string name="dashboard_title">Главная</string>
|
||||
@@ -28,5 +31,6 @@
|
||||
|
||||
<!-- Navigation -->
|
||||
<string name="nav_locations">Локации</string>
|
||||
<string name="nav_labels">Метки</string>
|
||||
|
||||
</resources>
|
||||
@@ -231,6 +231,44 @@
|
||||
</USER_INTERACTIONS>
|
||||
</SCREEN>
|
||||
<!-- [ЯКОРЬ] Конец спецификации UI для экрана Локаций. -->
|
||||
|
||||
<!-- [ЯКОРЬ] Начало спецификации UI для экрана Меток. -->
|
||||
<SCREEN id="screen_labels_list" status="defined">
|
||||
<summary>Экран "Метки"</summary>
|
||||
<description>
|
||||
Отображает вертикальный список всех доступных меток. Экран должен быть интегрирован в общую структуру навигации приложения.
|
||||
</description>
|
||||
<LAYOUT>
|
||||
<COMPONENT type="TopAppBar">
|
||||
<description>Общая верхняя панель приложения с заголовком "Метки" и кнопкой "назад".</description>
|
||||
</COMPONENT>
|
||||
<COMPONENT type="MainContent" orientation="vertical">
|
||||
<description>Основная область контента, занимающая все доступное пространство под TopAppBar.</description>
|
||||
<SUB_COMPONENT type="List" name="LabelsList">
|
||||
<description>Вертикальный, прокручиваемый список (LazyColumn) всех меток.</description>
|
||||
<ELEMENT type="ListItem">
|
||||
<description>Элемент списка, представляющий одну метку. Состоит из иконки (например, 'label') и названия метки. Весь элемент является кликабельным и ведет на экран со списком предметов с данной меткой.</description>
|
||||
</ELEMENT>
|
||||
</SUB_COMPONENT>
|
||||
</COMPONENT>
|
||||
<COMPONENT type="FloatingActionButton" icon="add">
|
||||
<description>
|
||||
Плавающая кнопка действия, расположенная в правом нижнем углу. Позволяет пользователю добавить новую метку.
|
||||
</description>
|
||||
</COMPONENT>
|
||||
</LAYOUT>
|
||||
<USER_INTERACTIONS>
|
||||
<INTERACTION>
|
||||
<action>Нажатие на элемент списка меток</action>
|
||||
<reaction>Осуществляется навигация на экран списка инвентаря, отфильтрованного по выбранной метке.</reaction>
|
||||
</INTERACTION>
|
||||
<INTERACTION>
|
||||
<action>Нажатие на FloatingActionButton</action>
|
||||
<reaction>Открывается диалоговое окно или новый экран для создания новой метки.</reaction>
|
||||
</INTERACTION>
|
||||
</USER_INTERACTIONS>
|
||||
</SCREEN>
|
||||
<!-- [ЯКОРЬ] Конец спецификации UI для экрана Меток. -->
|
||||
|
||||
</UI_SPECIFICATIONS>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user