add location screen
This commit is contained in:
@@ -18,6 +18,7 @@ import com.homebox.lens.ui.screen.inventorylist.InventoryListScreen
|
|||||||
import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen
|
import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen
|
||||||
import com.homebox.lens.ui.screen.itemedit.ItemEditScreen
|
import com.homebox.lens.ui.screen.itemedit.ItemEditScreen
|
||||||
import com.homebox.lens.ui.screen.labelslist.LabelsListScreen
|
import com.homebox.lens.ui.screen.labelslist.LabelsListScreen
|
||||||
|
import com.homebox.lens.ui.screen.locationedit.LocationEditScreen
|
||||||
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
|
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
|
||||||
import com.homebox.lens.ui.screen.search.SearchScreen
|
import com.homebox.lens.ui.screen.search.SearchScreen
|
||||||
import com.homebox.lens.ui.screen.setup.SetupScreen
|
import com.homebox.lens.ui.screen.setup.SetupScreen
|
||||||
@@ -109,10 +110,17 @@ fun NavGraph(
|
|||||||
navController.navigate(Screen.InventoryList.route)
|
navController.navigate(Screen.InventoryList.route)
|
||||||
},
|
},
|
||||||
onAddNewLocationClick = {
|
onAddNewLocationClick = {
|
||||||
// TODO: Navigate to a screen for creating a new location
|
navController.navigate(Screen.LocationEdit.createRoute("new"))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// [COMPOSABLE_LOCATION_EDIT]
|
||||||
|
composable(route = Screen.LocationEdit.route) { backStackEntry ->
|
||||||
|
val locationId = backStackEntry.arguments?.getString("locationId")
|
||||||
|
LocationEditScreen(
|
||||||
|
locationId = locationId
|
||||||
|
)
|
||||||
|
}
|
||||||
// [COMPOSABLE_SEARCH]
|
// [COMPOSABLE_SEARCH]
|
||||||
composable(route = Screen.Search.route) {
|
composable(route = Screen.Search.route) {
|
||||||
SearchScreen(
|
SearchScreen(
|
||||||
|
|||||||
@@ -56,6 +56,25 @@ sealed class Screen(val route: String) {
|
|||||||
}
|
}
|
||||||
data object LabelsList : Screen("labels_list_screen")
|
data object LabelsList : Screen("labels_list_screen")
|
||||||
data object LocationsList : Screen("locations_list_screen")
|
data object LocationsList : Screen("locations_list_screen")
|
||||||
|
data object LocationEdit : Screen("location_edit_screen/{locationId}") {
|
||||||
|
/**
|
||||||
|
* [CONTRACT]
|
||||||
|
* Создает маршрут для экрана редактирования местоположения с указанным ID.
|
||||||
|
* @param locationId ID местоположения для редактирования.
|
||||||
|
* @return Строку полного маршрута.
|
||||||
|
* @throws IllegalArgumentException если locationId пустой.
|
||||||
|
*/
|
||||||
|
// [HELPER]
|
||||||
|
fun createRoute(locationId: String): String {
|
||||||
|
// [PRECONDITION]
|
||||||
|
require(locationId.isNotBlank()) { "[PRECONDITION_FAILED] locationId не может быть пустым." }
|
||||||
|
// [ACTION]
|
||||||
|
val route = "location_edit_screen/$locationId"
|
||||||
|
// [POSTCONDITION]
|
||||||
|
check(route.endsWith(locationId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на locationId." }
|
||||||
|
return route
|
||||||
|
}
|
||||||
|
}
|
||||||
data object Search : Screen("search_screen")
|
data object Search : Screen("search_screen")
|
||||||
}
|
}
|
||||||
// [END_FILE_Screen.kt]
|
// [END_FILE_Screen.kt]
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// [PACKAGE] com.homebox.lens.ui.screen.locationedit
|
||||||
|
// [FILE] LocationEditScreen.kt
|
||||||
|
// [SEMANTICS] ui, screen, location, edit
|
||||||
|
|
||||||
|
package com.homebox.lens.ui.screen.locationedit
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.homebox.lens.R
|
||||||
|
|
||||||
|
// [ENTRYPOINT]
|
||||||
|
/**
|
||||||
|
* [CONTRACT]
|
||||||
|
* @summary Composable-функция для экрана "Редактирование местоположения".
|
||||||
|
* @param locationId ID местоположения для редактирования или "new" для создания.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun LocationEditScreen(
|
||||||
|
locationId: String?
|
||||||
|
) {
|
||||||
|
val title = if (locationId == "new") {
|
||||||
|
stringResource(id = R.string.location_edit_title_create)
|
||||||
|
} else {
|
||||||
|
stringResource(id = R.string.location_edit_title_edit)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold { paddingValues ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(text = "TODO: Location Edit Screen for ID: $locationId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,50 @@
|
|||||||
package com.homebox.lens.ui.screen.locationslist
|
package com.homebox.lens.ui.screen.locationslist
|
||||||
|
|
||||||
// [IMPORTS]
|
// [IMPORTS]
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
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.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.R
|
||||||
|
import com.homebox.lens.domain.model.LocationOutCount
|
||||||
import com.homebox.lens.navigation.NavigationActions
|
import com.homebox.lens.navigation.NavigationActions
|
||||||
import com.homebox.lens.ui.common.MainScaffold
|
import com.homebox.lens.ui.common.MainScaffold
|
||||||
|
import com.homebox.lens.ui.theme.HomeboxLensTheme
|
||||||
|
|
||||||
// [ENTRYPOINT]
|
// [ENTRYPOINT]
|
||||||
/**
|
/**
|
||||||
@@ -20,22 +58,230 @@ import com.homebox.lens.ui.common.MainScaffold
|
|||||||
* @param navigationActions Объект с навигационными действиями.
|
* @param navigationActions Объект с навигационными действиями.
|
||||||
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
||||||
* @param onAddNewLocationClick Лямбда-обработчик нажатия на кнопку добавления нового местоположения.
|
* @param onAddNewLocationClick Лямбда-обработчик нажатия на кнопку добавления нового местоположения.
|
||||||
|
* @param viewModel ViewModel для этого экрана.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun LocationsListScreen(
|
fun LocationsListScreen(
|
||||||
currentRoute: String?,
|
currentRoute: String?,
|
||||||
navigationActions: NavigationActions,
|
navigationActions: NavigationActions,
|
||||||
onLocationClick: (String) -> Unit,
|
onLocationClick: (String) -> Unit,
|
||||||
onAddNewLocationClick: () -> Unit
|
onAddNewLocationClick: () -> Unit,
|
||||||
|
viewModel: LocationsListViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
|
// [STATE]
|
||||||
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
// [UI_COMPONENT]
|
// [UI_COMPONENT]
|
||||||
MainScaffold(
|
MainScaffold(
|
||||||
topBarTitle = stringResource(id = R.string.locations_list_title),
|
topBarTitle = stringResource(id = R.string.locations_list_title),
|
||||||
currentRoute = currentRoute,
|
currentRoute = currentRoute,
|
||||||
navigationActions = navigationActions
|
navigationActions = navigationActions
|
||||||
) {
|
) { paddingValues ->
|
||||||
// [CORE-LOGIC]
|
Scaffold(
|
||||||
Text(text = "TODO: Locations List Screen")
|
modifier = Modifier.padding(paddingValues),
|
||||||
|
floatingActionButton = {
|
||||||
|
FloatingActionButton(onClick = onAddNewLocationClick) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Add,
|
||||||
|
contentDescription = stringResource(id = R.string.cd_add_new_location)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
LocationsListContent(
|
||||||
|
modifier = Modifier.padding(innerPadding),
|
||||||
|
uiState = uiState,
|
||||||
|
onLocationClick = onLocationClick,
|
||||||
|
onEditLocation = { /* TODO */ },
|
||||||
|
onDeleteLocation = { /* TODO */ }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [HELPER]
|
||||||
|
/**
|
||||||
|
* [CONTRACT]
|
||||||
|
* @summary Отображает основной контент экрана в зависимости от `uiState`.
|
||||||
|
* @param modifier Модификатор для стилизации.
|
||||||
|
* @param uiState Текущее состояние UI.
|
||||||
|
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
||||||
|
* @param onEditLocation Лямбда-обработчик для редактирования местоположения.
|
||||||
|
* @param onDeleteLocation Лямбда-обработчик для удаления местоположения.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun LocationsListContent(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
uiState: LocationsListUiState,
|
||||||
|
onLocationClick: (String) -> Unit,
|
||||||
|
onEditLocation: (String) -> Unit,
|
||||||
|
onDeleteLocation: (String) -> Unit
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
|
when (uiState) {
|
||||||
|
is LocationsListUiState.Loading -> {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||||
|
}
|
||||||
|
is LocationsListUiState.Error -> {
|
||||||
|
Text(
|
||||||
|
text = uiState.message,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.padding(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is LocationsListUiState.Success -> {
|
||||||
|
if (uiState.locations.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.locations_not_found),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.padding(16.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
items(uiState.locations, key = { it.id }) { location ->
|
||||||
|
LocationCard(
|
||||||
|
location = location,
|
||||||
|
onClick = { onLocationClick(location.id) },
|
||||||
|
onEditClick = { onEditLocation(location.id) },
|
||||||
|
onDeleteClick = { onDeleteLocation(location.id) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [UI_COMPONENT]
|
||||||
|
/**
|
||||||
|
* [CONTRACT]
|
||||||
|
* @summary Карточка для отображения одного местоположения.
|
||||||
|
* @param location Данные о местоположении.
|
||||||
|
* @param onClick Лямбда-обработчик нажатия на карточку.
|
||||||
|
* @param onEditClick Лямбда-обработчик нажатия на "Редактировать".
|
||||||
|
* @param onDeleteClick Лямбда-обработчик нажатия на "Удалить".
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun LocationCard(
|
||||||
|
location: LocationOutCount,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onEditClick: () -> Unit,
|
||||||
|
onDeleteClick: () -> Unit
|
||||||
|
) {
|
||||||
|
var menuExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(start = 16.dp, end = 8.dp, top = 16.dp, bottom = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(text = location.name, style = MaterialTheme.typography.titleMedium)
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.item_count, location.itemCount),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(16.dp))
|
||||||
|
Box {
|
||||||
|
IconButton(onClick = { menuExpanded = true }) {
|
||||||
|
Icon(Icons.Default.MoreVert, contentDescription = stringResource(id = R.string.cd_more_options))
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = menuExpanded,
|
||||||
|
onDismissRequest = { menuExpanded = false }
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(id = R.string.edit)) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onEditClick()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(id = R.string.delete)) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onDeleteClick()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [PREVIEW]
|
||||||
|
@Preview(showBackground = true, name = "Locations List Success")
|
||||||
|
@Composable
|
||||||
|
fun LocationsListSuccessPreview() {
|
||||||
|
val previewLocations = listOf(
|
||||||
|
LocationOutCount("1", "Garage", "#FF0000", false, 12, "", ""),
|
||||||
|
LocationOutCount("2", "Kitchen", "#00FF00", false, 5, "", ""),
|
||||||
|
LocationOutCount("3", "Office", "#0000FF", false, 23, "", "")
|
||||||
|
)
|
||||||
|
HomeboxLensTheme {
|
||||||
|
LocationsListContent(
|
||||||
|
uiState = LocationsListUiState.Success(previewLocations),
|
||||||
|
onLocationClick = {},
|
||||||
|
onEditLocation = {},
|
||||||
|
onDeleteLocation = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [PREVIEW]
|
||||||
|
@Preview(showBackground = true, name = "Locations List Empty")
|
||||||
|
@Composable
|
||||||
|
fun LocationsListEmptyPreview() {
|
||||||
|
HomeboxLensTheme {
|
||||||
|
LocationsListContent(
|
||||||
|
uiState = LocationsListUiState.Success(emptyList()),
|
||||||
|
onLocationClick = {},
|
||||||
|
onEditLocation = {},
|
||||||
|
onDeleteLocation = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [PREVIEW]
|
||||||
|
@Preview(showBackground = true, name = "Locations List Loading")
|
||||||
|
@Composable
|
||||||
|
fun LocationsListLoadingPreview() {
|
||||||
|
HomeboxLensTheme {
|
||||||
|
LocationsListContent(
|
||||||
|
uiState = LocationsListUiState.Loading,
|
||||||
|
onLocationClick = {},
|
||||||
|
onEditLocation = {},
|
||||||
|
onDeleteLocation = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [PREVIEW]
|
||||||
|
@Preview(showBackground = true, name = "Locations List Error")
|
||||||
|
@Composable
|
||||||
|
fun LocationsListErrorPreview() {
|
||||||
|
HomeboxLensTheme {
|
||||||
|
LocationsListContent(
|
||||||
|
uiState = LocationsListUiState.Error("Failed to load locations. Please try again."),
|
||||||
|
onLocationClick = {},
|
||||||
|
onEditLocation = {},
|
||||||
|
onDeleteLocation = {}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// [END_FUNCTION_LocationsListScreen]
|
|
||||||
}
|
}
|
||||||
@@ -1,36 +1,35 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
|
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
|
||||||
// [FILE] LocationsListUiState.kt
|
// [FILE] LocationsListUiState.kt
|
||||||
// [SEMANTICS] ui, state, locations_list
|
// [SEMANTICS] ui, state, locations
|
||||||
|
|
||||||
package com.homebox.lens.ui.screen.locationslist
|
package com.homebox.lens.ui.screen.locationslist
|
||||||
|
|
||||||
import com.homebox.lens.domain.model.LocationOutCount
|
import com.homebox.lens.domain.model.LocationOutCount
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
|
||||||
// [ENTITY: SealedInterface('LocationsListUiState')]
|
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* [CONTRACT]
|
||||||
* Определяет все возможные состояния для экрана "Список локаций".
|
* @summary Определяет возможные состояния UI для экрана списка местоположений.
|
||||||
* @invariant В любой момент времени экран может находиться только в одном из этих состояний.
|
* @see LocationsListViewModel
|
||||||
*/
|
*/
|
||||||
sealed interface LocationsListUiState {
|
sealed interface LocationsListUiState {
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* [STATE]
|
||||||
* Состояние успешной загрузки данных.
|
* @summary Состояние успешной загрузки данных.
|
||||||
* @property locations Список локаций со счетчиками.
|
* @param locations Список местоположений для отображения.
|
||||||
*/
|
*/
|
||||||
data class Success(val locations: List<LocationOutCount>) : LocationsListUiState
|
data class Success(val locations: List<LocationOutCount>) : LocationsListUiState
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* [STATE]
|
||||||
* Состояние ошибки во время загрузки данных.
|
* @summary Состояние ошибки.
|
||||||
* @property message Человекочитаемое сообщение об ошибке.
|
* @param message Сообщение об ошибке.
|
||||||
*/
|
*/
|
||||||
data class Error(val message: String) : LocationsListUiState
|
data class Error(val message: String) : LocationsListUiState
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* [STATE]
|
||||||
* Состояние, когда данные для экрана загружаются.
|
* @summary Состояние загрузки данных.
|
||||||
*/
|
*/
|
||||||
data object Loading : LocationsListUiState
|
object Loading : LocationsListUiState
|
||||||
}
|
}
|
||||||
// [END_FILE_LocationsListUiState.kt]
|
// [END_FILE_LocationsListUiState.kt]
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
|
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
|
||||||
// [FILE] LocationsListViewModel.kt
|
// [FILE] LocationsListViewModel.kt
|
||||||
// [SEMANTICS] ui_logic, locations_list, state_management
|
// [SEMANTICS] ui, viewmodel, locations, hilt
|
||||||
|
|
||||||
package com.homebox.lens.ui.screen.locationslist
|
package com.homebox.lens.ui.screen.locationslist
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
@@ -8,18 +9,18 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
|
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
// [VIEWMODEL]
|
// [CORE-LOGIC]
|
||||||
// [ENTITY: ViewModel('LocationsListViewModel')]
|
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* [CONTRACT]
|
||||||
* @summary ViewModel для экрана со списком локаций.
|
* @summary ViewModel для экрана списка местоположений.
|
||||||
* @description Управляет состоянием экрана, загружает список локаций и обрабатывает ошибки.
|
* @param getAllLocationsUseCase Use case для получения всех местоположений.
|
||||||
* @invariant `uiState` всегда является одним из состояний, определенных в `LocationsListUiState`.
|
* @property uiState Поток, содержащий текущее состояние UI.
|
||||||
|
* @invariant `uiState` всегда отражает результат последней операции загрузки.
|
||||||
*/
|
*/
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class LocationsListViewModel @Inject constructor(
|
class LocationsListViewModel @Inject constructor(
|
||||||
@@ -28,44 +29,28 @@ class LocationsListViewModel @Inject constructor(
|
|||||||
|
|
||||||
// [STATE]
|
// [STATE]
|
||||||
private val _uiState = MutableStateFlow<LocationsListUiState>(LocationsListUiState.Loading)
|
private val _uiState = MutableStateFlow<LocationsListUiState>(LocationsListUiState.Loading)
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState: StateFlow<LocationsListUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
// [LIFECYCLE_HANDLER]
|
// [INITIALIZER]
|
||||||
init {
|
init {
|
||||||
loadLocations()
|
loadLocations()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [ACTION]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* [CONTRACT]
|
||||||
* @summary Загружает список локаций.
|
* @summary Загружает список местоположений из репозитория.
|
||||||
* @description Выполняет `GetAllLocationsUseCase` и обновляет UI, переключая его
|
* @sideeffect Обновляет `_uiState` в зависимости от результата: Loading -> Success/Error.
|
||||||
* между состояниями `Loading`, `Success` и `Error`.
|
|
||||||
* @sideeffect Асинхронно обновляет `_uiState`.
|
|
||||||
*/
|
*/
|
||||||
fun loadLocations() {
|
fun loadLocations() {
|
||||||
// [ENTRYPOINT]
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.value = LocationsListUiState.Loading
|
_uiState.value = LocationsListUiState.Loading
|
||||||
Timber.i("[ACTION] Starting locations list load. State -> Loading.")
|
try {
|
||||||
|
val locations = getAllLocationsUseCase()
|
||||||
// [CORE-LOGIC]
|
_uiState.value = LocationsListUiState.Success(locations)
|
||||||
val result = runCatching {
|
} catch (e: Exception) {
|
||||||
getAllLocationsUseCase()
|
_uiState.value = LocationsListUiState.Error(e.message ?: "Unknown error")
|
||||||
}
|
}
|
||||||
|
|
||||||
// [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_CLASS_LocationsListViewModel]
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
<!-- Common -->
|
<!-- Common -->
|
||||||
<string name="create">Создать</string>
|
<string name="create">Создать</string>
|
||||||
|
<string name="edit">Редактировать</string>
|
||||||
|
<string name="delete">Удалить</string>
|
||||||
<string name="search">Поиск</string>
|
<string name="search">Поиск</string>
|
||||||
<string name="logout">Выйти</string>
|
<string name="logout">Выйти</string>
|
||||||
<string name="no_location">Нет локации</string>
|
<string name="no_location">Нет локации</string>
|
||||||
@@ -42,6 +44,15 @@
|
|||||||
<string name="locations_list_title">Места хранения</string>
|
<string name="locations_list_title">Места хранения</string>
|
||||||
<string name="search_title">Поиск</string>
|
<string name="search_title">Поиск</string>
|
||||||
|
|
||||||
|
<!-- Location Edit Screen -->
|
||||||
|
<string name="location_edit_title_create">Создать локацию</string>
|
||||||
|
<string name="location_edit_title_edit">Редактировать локацию</string>
|
||||||
|
|
||||||
|
<!-- Locations List Screen -->
|
||||||
|
<string name="locations_not_found">Местоположения не найдены. Нажмите +, чтобы добавить новое.</string>
|
||||||
|
<string name="item_count">Предметов: %1$d</string>
|
||||||
|
<string name="cd_more_options">Больше опций</string>
|
||||||
|
|
||||||
<!-- Setup Screen -->
|
<!-- Setup Screen -->
|
||||||
<string name="setup_title">Настройка сервера</string>
|
<string name="setup_title">Настройка сервера</string>
|
||||||
<string name="setup_server_url_label">URL сервера</string>
|
<string name="setup_server_url_label">URL сервера</string>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListViewModel.kt" status="implemented" spec_ref_id="screen_labels_list">
|
<file name="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListViewModel.kt" status="implemented" spec_ref_id="screen_labels_list">
|
||||||
<purpose_summary>ViewModel для экрана списка меток.</purpose_summary>
|
<purpose_summary>ViewModel для экрана списка меток.</purpose_summary>
|
||||||
</file>
|
</file>
|
||||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt" status="stub" spec_ref_id="screen_locations_list">
|
<file name="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt" status="implemented" spec_ref_id="screen_locations_list">
|
||||||
<purpose_summary>UI для экрана списка местоположений.</purpose_summary>
|
<purpose_summary>UI для экрана списка местоположений.</purpose_summary>
|
||||||
<coherence_note>Использует модель LocationOutCount для отображения количества элементов в каждой локации.</coherence_note>
|
<coherence_note>Использует модель LocationOutCount для отображения количества элементов в каждой локации.</coherence_note>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
Reference in New Issue
Block a user