Implement deletion of shopping list items (#163)
* Disable unstable Gradle features * Implement deletion of shopping list items * Hide deleted items even before they're deleted * Check/uncheck items locally while the BE is updated
This commit is contained in:
@@ -64,4 +64,6 @@ interface MealieDataSourceV1 {
|
|||||||
suspend fun getShoppingList(id: String): GetShoppingListResponseV1
|
suspend fun getShoppingList(id: String): GetShoppingListResponseV1
|
||||||
|
|
||||||
suspend fun updateIsShoppingListItemChecked(id: String, isChecked: Boolean)
|
suspend fun updateIsShoppingListItemChecked(id: String, isChecked: Boolean)
|
||||||
|
|
||||||
|
suspend fun deleteShoppingListItem(id: String)
|
||||||
}
|
}
|
||||||
@@ -188,4 +188,12 @@ class MealieDataSourceV1Impl @Inject constructor(
|
|||||||
}
|
}
|
||||||
updateShoppingListItem(id, JsonObject(updatedItem))
|
updateShoppingListItem(id, JsonObject(updatedItem))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteShoppingListItem(
|
||||||
|
id: String,
|
||||||
|
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
|
block = { service.deleteShoppingListItem(id) },
|
||||||
|
logMethod = { "deleteShoppingListItem" },
|
||||||
|
logParameters = { "id = $id" }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,4 +89,9 @@ interface MealieServiceV1 {
|
|||||||
@Path("id") id: String,
|
@Path("id") id: String,
|
||||||
@Body request: JsonElement,
|
@Body request: JsonElement,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@DELETE("/api/groups/shopping/items/{id}")
|
||||||
|
suspend fun deleteShoppingListItem(
|
||||||
|
@Path("id") id: String,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -10,4 +10,6 @@ interface ShoppingListsDataSource {
|
|||||||
suspend fun getShoppingList(id: String): FullShoppingListInfo
|
suspend fun getShoppingList(id: String): FullShoppingListInfo
|
||||||
|
|
||||||
suspend fun updateIsShoppingListItemChecked(id: String, checked: Boolean)
|
suspend fun updateIsShoppingListItemChecked(id: String, checked: Boolean)
|
||||||
|
|
||||||
|
suspend fun deleteShoppingListItem(id: String)
|
||||||
}
|
}
|
||||||
@@ -24,5 +24,10 @@ class ShoppingListsDataSourceImpl @Inject constructor(
|
|||||||
id: String,
|
id: String,
|
||||||
checked: Boolean,
|
checked: Boolean,
|
||||||
) = v1Source.updateIsShoppingListItemChecked(id, checked)
|
) = v1Source.updateIsShoppingListItemChecked(id, checked)
|
||||||
|
|
||||||
|
override suspend fun deleteShoppingListItem(
|
||||||
|
id: String
|
||||||
|
) = v1Source.deleteShoppingListItem(id)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,4 +10,6 @@ interface ShoppingListsRepo {
|
|||||||
suspend fun getShoppingLists(): List<ShoppingListInfo>
|
suspend fun getShoppingLists(): List<ShoppingListInfo>
|
||||||
|
|
||||||
suspend fun getShoppingList(id: String): FullShoppingListInfo
|
suspend fun getShoppingList(id: String): FullShoppingListInfo
|
||||||
|
|
||||||
|
suspend fun deleteShoppingListItem(id: String)
|
||||||
}
|
}
|
||||||
@@ -25,4 +25,9 @@ class ShoppingListsRepoImpl @Inject constructor(
|
|||||||
logger.v { "getShoppingListItems() called with: id = $id" }
|
logger.v { "getShoppingListItems() called with: id = $id" }
|
||||||
return dataSource.getShoppingList(id)
|
return dataSource.getShoppingList(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteShoppingListItem(id: String) {
|
||||||
|
logger.v { "deleteShoppingListItem() called with: id = $id" }
|
||||||
|
dataSource.deleteShoppingListItem(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,31 @@
|
|||||||
package gq.kirmanak.mealient.shopping_lists.ui
|
package gq.kirmanak.mealient.shopping_lists.ui
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.DismissDirection
|
||||||
|
import androidx.compose.material3.DismissValue
|
||||||
import androidx.compose.material3.Divider
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SwipeToDismiss
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberDismissState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -50,38 +65,75 @@ internal fun ShoppingListScreen(
|
|||||||
|
|
||||||
LazyColumnWithLoadingState(
|
LazyColumnWithLoadingState(
|
||||||
loadingState = loadingState.map { it.items },
|
loadingState = loadingState.map { it.items },
|
||||||
|
contentPadding = PaddingValues(Dimens.Medium),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Dimens.Medium),
|
||||||
defaultEmptyListError = defaultEmptyListError,
|
defaultEmptyListError = defaultEmptyListError,
|
||||||
errorToShowInSnackbar = shoppingListViewModel.errorToShowInSnackbar,
|
errorToShowInSnackbar = shoppingListViewModel.errorToShowInSnackbar,
|
||||||
onRefresh = shoppingListViewModel::refreshShoppingList,
|
onRefresh = shoppingListViewModel::refreshShoppingList,
|
||||||
onSnackbarShown = shoppingListViewModel::onSnackbarShown,
|
onSnackbarShown = shoppingListViewModel::onSnackbarShown
|
||||||
lazyColumnContent = { items ->
|
) { items ->
|
||||||
val firstCheckedItemIndex = items.indexOfFirst { it.item.checked }
|
val firstCheckedItemIndex = items.indexOfFirst { it.checked }
|
||||||
|
|
||||||
itemsIndexed(items) { index, item ->
|
itemsIndexed(items, { _, item -> item.id }) { index, item ->
|
||||||
ShoppingListItem(
|
ShoppingListItem(
|
||||||
shoppingListItem = item.item,
|
shoppingListItem = item,
|
||||||
isDisabled = item.isDisabled,
|
|
||||||
showDivider = index == firstCheckedItemIndex && index != 0,
|
showDivider = index == firstCheckedItemIndex && index != 0,
|
||||||
) { isChecked ->
|
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
|
||||||
shoppingListViewModel.onItemCheckedChange(item.item, isChecked)
|
onCheckedChange = { isChecked ->
|
||||||
}
|
shoppingListViewModel.onItemCheckedChange(item, isChecked)
|
||||||
}
|
},
|
||||||
|
onDismissed = {
|
||||||
|
shoppingListViewModel.deleteShoppingListItem(item)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ShoppingListItem(
|
fun ShoppingListItem(
|
||||||
shoppingListItem: ShoppingListItemInfo,
|
shoppingListItem: ShoppingListItemInfo,
|
||||||
isDisabled: Boolean,
|
|
||||||
showDivider: Boolean,
|
showDivider: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onCheckedChange: (Boolean) -> Unit = {},
|
onCheckedChange: (Boolean) -> Unit = {},
|
||||||
|
onDismissed: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
val dismissState = rememberDismissState(
|
||||||
|
confirmValueChange = {
|
||||||
|
if (it == DismissValue.DismissedToStart) {
|
||||||
|
onDismissed()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
SwipeToDismiss(
|
||||||
|
state = dismissState,
|
||||||
|
background = {
|
||||||
|
if (dismissState.targetValue == DismissValue.DismissedToStart) {
|
||||||
|
val color by animateColorAsState(MaterialTheme.colorScheme.error)
|
||||||
|
val iconColor by animateColorAsState(MaterialTheme.colorScheme.onSurface)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(color)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Delete,
|
||||||
|
contentDescription = stringResource(R.string.shopping_list_screen_delete_icon_content_description),
|
||||||
|
tint = iconColor,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterEnd)
|
||||||
|
.padding(end = Dimens.Small)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissContent = {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = Dimens.Small, end = Dimens.Small, start = Dimens.Small),
|
.background(MaterialTheme.colorScheme.surface),
|
||||||
) {
|
) {
|
||||||
if (showDivider) {
|
if (showDivider) {
|
||||||
Divider()
|
Divider()
|
||||||
@@ -93,7 +145,6 @@ fun ShoppingListItem(
|
|||||||
Checkbox(
|
Checkbox(
|
||||||
checked = shoppingListItem.checked,
|
checked = shoppingListItem.checked,
|
||||||
onCheckedChange = onCheckedChange,
|
onCheckedChange = onCheckedChange,
|
||||||
enabled = !isDisabled,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val isFood = shoppingListItem.isFood
|
val isFood = shoppingListItem.isFood
|
||||||
@@ -111,13 +162,17 @@ fun ShoppingListItem(
|
|||||||
Text(text = text)
|
Text(text = text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
directions = setOf(DismissDirection.EndToStart),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
fun PreviewShoppingListItemChecked() {
|
fun PreviewShoppingListItemChecked() {
|
||||||
AppTheme {
|
AppTheme {
|
||||||
ShoppingListItem(shoppingListItem = PreviewData.milk, false, false)
|
ShoppingListItem(shoppingListItem = PreviewData.milk, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,23 +180,7 @@ fun PreviewShoppingListItemChecked() {
|
|||||||
@Preview
|
@Preview
|
||||||
fun PreviewShoppingListItemUnchecked() {
|
fun PreviewShoppingListItemUnchecked() {
|
||||||
AppTheme {
|
AppTheme {
|
||||||
ShoppingListItem(shoppingListItem = PreviewData.blackTeaBags, false, false)
|
ShoppingListItem(shoppingListItem = PreviewData.blackTeaBags, false)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@Preview
|
|
||||||
fun PreviewShoppingListItemCheckedDisabled() {
|
|
||||||
AppTheme {
|
|
||||||
ShoppingListItem(shoppingListItem = PreviewData.milk, true, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@Preview
|
|
||||||
fun PreviewShoppingListItemUncheckedDisabled() {
|
|
||||||
AppTheme {
|
|
||||||
ShoppingListItem(shoppingListItem = PreviewData.blackTeaBags, true, false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,5 @@ import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
|||||||
|
|
||||||
internal data class ShoppingListScreenState(
|
internal data class ShoppingListScreenState(
|
||||||
val name: String,
|
val name: String,
|
||||||
val items: List<ShoppingListItemState>,
|
val items: List<ShoppingListItemInfo>,
|
||||||
)
|
|
||||||
|
|
||||||
internal data class ShoppingListItemState(
|
|
||||||
val item: ShoppingListItemInfo,
|
|
||||||
val isDisabled: Boolean,
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ internal class ShoppingListViewModel @Inject constructor(
|
|||||||
|
|
||||||
private val args: ShoppingListNavArgs = ShoppingListScreenDestination.argsFrom(savedStateHandle)
|
private val args: ShoppingListNavArgs = ShoppingListScreenDestination.argsFrom(savedStateHandle)
|
||||||
|
|
||||||
private val _disabledItemIds: MutableStateFlow<Set<String>> = MutableStateFlow(mutableSetOf())
|
private val checkedOverride = MutableStateFlow<MutableMap<String, Boolean>>(mutableMapOf())
|
||||||
|
|
||||||
|
private val deletedItemIds = MutableStateFlow<Set<String>>(mutableSetOf())
|
||||||
|
|
||||||
private val loadingHelper = loadingHelperFactory.create(viewModelScope) {
|
private val loadingHelper = loadingHelperFactory.create(viewModelScope) {
|
||||||
shoppingListsRepo.getShoppingList(args.shoppingListId)
|
shoppingListsRepo.getShoppingList(args.shoppingListId)
|
||||||
@@ -47,7 +49,8 @@ internal class ShoppingListViewModel @Inject constructor(
|
|||||||
|
|
||||||
val loadingState: StateFlow<LoadingState<ShoppingListScreenState>> = combine(
|
val loadingState: StateFlow<LoadingState<ShoppingListScreenState>> = combine(
|
||||||
loadingHelper.loadingState,
|
loadingHelper.loadingState,
|
||||||
_disabledItemIds,
|
checkedOverride,
|
||||||
|
deletedItemIds,
|
||||||
::buildLoadingState,
|
::buildLoadingState,
|
||||||
).stateIn(viewModelScope, SharingStarted.Eagerly, LoadingStateNoData.InitialLoad)
|
).stateIn(viewModelScope, SharingStarted.Eagerly, LoadingStateNoData.InitialLoad)
|
||||||
|
|
||||||
@@ -64,7 +67,7 @@ internal class ShoppingListViewModel @Inject constructor(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
authRepo.isAuthorizedFlow.valueUpdatesOnly().collect {
|
authRepo.isAuthorizedFlow.valueUpdatesOnly().collect {
|
||||||
logger.d { "Authorization state changed to $it" }
|
logger.d { "Authorization state changed to $it" }
|
||||||
if (it) refreshShoppingList()
|
if (it) doRefresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,19 +75,25 @@ internal class ShoppingListViewModel @Inject constructor(
|
|||||||
fun refreshShoppingList() {
|
fun refreshShoppingList() {
|
||||||
logger.v { "refreshShoppingList() called" }
|
logger.v { "refreshShoppingList() called" }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_errorToShowInSnackbar = loadingHelper.refresh().exceptionOrNull()
|
doRefresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun doRefresh() {
|
||||||
|
_errorToShowInSnackbar = loadingHelper.refresh().exceptionOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildLoadingState(
|
private fun buildLoadingState(
|
||||||
loadingState: LoadingState<FullShoppingListInfo>,
|
loadingState: LoadingState<FullShoppingListInfo>,
|
||||||
disabledItemIds: Set<String>,
|
checkedOverrideMap: Map<String, Boolean>,
|
||||||
|
deletedItemIds: Set<String>,
|
||||||
): LoadingState<ShoppingListScreenState> {
|
): LoadingState<ShoppingListScreenState> {
|
||||||
logger.v { "buildLoadingState() called with: loadingState = $loadingState, disabledItems = $disabledItemIds" }
|
logger.v { "buildLoadingState() called with: loadingState = $loadingState, checkedOverrideMap = $checkedOverrideMap, deletedItemIds = $deletedItemIds" }
|
||||||
return loadingState.map { shoppingList ->
|
return loadingState.map { shoppingList ->
|
||||||
val items = shoppingList.items
|
val items = shoppingList.items
|
||||||
|
.filter { it.id !in deletedItemIds }
|
||||||
|
.map { it.copy(checked = checkedOverrideMap[it.id] ?: it.checked) }
|
||||||
.sortedBy { it.checked }
|
.sortedBy { it.checked }
|
||||||
.map { ShoppingListItemState(item = it, isDisabled = it.id in disabledItemIds) }
|
|
||||||
ShoppingListScreenState(name = shoppingList.name, items = items)
|
ShoppingListScreenState(name = shoppingList.name, items = items)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,17 +101,22 @@ internal class ShoppingListViewModel @Inject constructor(
|
|||||||
fun onItemCheckedChange(item: ShoppingListItemInfo, isChecked: Boolean) {
|
fun onItemCheckedChange(item: ShoppingListItemInfo, isChecked: Boolean) {
|
||||||
logger.v { "onItemCheckedChange() called with: item = $item, isChecked = $isChecked" }
|
logger.v { "onItemCheckedChange() called with: item = $item, isChecked = $isChecked" }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_disabledItemIds.update { it + item.id }
|
checkedOverride.update { originalMap ->
|
||||||
|
originalMap.toMutableMap().also { newMap -> newMap[item.id] = isChecked }
|
||||||
|
}
|
||||||
|
checkedOverride.value[item.id] = isChecked
|
||||||
val result = runCatchingExceptCancel {
|
val result = runCatchingExceptCancel {
|
||||||
shoppingListsRepo.updateIsShoppingListItemChecked(item.id, isChecked)
|
shoppingListsRepo.updateIsShoppingListItemChecked(item.id, isChecked)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
logger.e(it) { "Failed to update item's checked state" }
|
logger.e(it) { "Failed to update item's checked state" }
|
||||||
}
|
}
|
||||||
_disabledItemIds.update { it - item.id }
|
|
||||||
_errorToShowInSnackbar = result.exceptionOrNull()
|
_errorToShowInSnackbar = result.exceptionOrNull()
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
logger.v { "Item's checked state updated" }
|
logger.v { "Item's checked state updated" }
|
||||||
refreshShoppingList()
|
doRefresh()
|
||||||
|
}
|
||||||
|
checkedOverride.update { originalMap ->
|
||||||
|
originalMap.toMutableMap().also { newMap -> newMap.remove(item.id) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,4 +125,22 @@ internal class ShoppingListViewModel @Inject constructor(
|
|||||||
logger.v { "onSnackbarShown() called" }
|
logger.v { "onSnackbarShown() called" }
|
||||||
_errorToShowInSnackbar = null
|
_errorToShowInSnackbar = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteShoppingListItem(item: ShoppingListItemInfo) {
|
||||||
|
logger.v { "deleteShoppingListItem() called with: item = $item" }
|
||||||
|
viewModelScope.launch {
|
||||||
|
deletedItemIds.update { it + item.id }
|
||||||
|
val result = runCatchingExceptCancel {
|
||||||
|
shoppingListsRepo.deleteShoppingListItem(item.id)
|
||||||
|
}.onFailure {
|
||||||
|
logger.e(it) { "Failed to delete item" }
|
||||||
|
}
|
||||||
|
_errorToShowInSnackbar = result.exceptionOrNull()
|
||||||
|
if (result.isSuccess) {
|
||||||
|
logger.v { "Item deleted" }
|
||||||
|
doRefresh()
|
||||||
|
}
|
||||||
|
deletedItemIds.update { it - item.id }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package gq.kirmanak.mealient.shopping_lists.ui.composables
|
package gq.kirmanak.mealient.shopping_lists.ui.composables
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
@@ -14,15 +16,21 @@ import androidx.compose.ui.Modifier
|
|||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
fun LazyColumnPullRefresh(
|
fun LazyColumnPullRefresh(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
refreshState: PullRefreshState,
|
refreshState: PullRefreshState,
|
||||||
isRefreshing: Boolean,
|
isRefreshing: Boolean,
|
||||||
lazyColumnContent: LazyListScope.() -> Unit
|
contentPadding: PaddingValues,
|
||||||
|
verticalArrangement: Arrangement.Vertical,
|
||||||
|
lazyColumnContent: LazyListScope.() -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier.pullRefresh(refreshState),
|
modifier = modifier.pullRefresh(refreshState),
|
||||||
) {
|
) {
|
||||||
LazyColumn(modifier = modifier, content = lazyColumnContent)
|
LazyColumn(
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
verticalArrangement = verticalArrangement,
|
||||||
|
content = lazyColumnContent
|
||||||
|
)
|
||||||
|
|
||||||
PullRefreshIndicator(
|
PullRefreshIndicator(
|
||||||
modifier = Modifier.align(Alignment.TopCenter),
|
modifier = Modifier.align(Alignment.TopCenter),
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package gq.kirmanak.mealient.shopping_lists.ui.composables
|
package gq.kirmanak.mealient.shopping_lists.ui.composables
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
@@ -11,6 +13,7 @@ import androidx.compose.material3.SnackbarHostState
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import gq.kirmanak.mealient.shopping_lists.util.LoadingState
|
import gq.kirmanak.mealient.shopping_lists.util.LoadingState
|
||||||
import gq.kirmanak.mealient.shopping_lists.util.LoadingStateNoData
|
import gq.kirmanak.mealient.shopping_lists.util.LoadingStateNoData
|
||||||
import gq.kirmanak.mealient.shopping_lists.util.data
|
import gq.kirmanak.mealient.shopping_lists.util.data
|
||||||
@@ -24,6 +27,8 @@ fun <T> LazyColumnWithLoadingState(
|
|||||||
loadingState: LoadingState<List<T>>,
|
loadingState: LoadingState<List<T>>,
|
||||||
defaultEmptyListError: String,
|
defaultEmptyListError: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||||
|
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
|
||||||
errorToShowInSnackbar: Throwable? = null,
|
errorToShowInSnackbar: Throwable? = null,
|
||||||
onSnackbarShown: () -> Unit = {},
|
onSnackbarShown: () -> Unit = {},
|
||||||
onRefresh: () -> Unit = {},
|
onRefresh: () -> Unit = {},
|
||||||
@@ -61,10 +66,12 @@ fun <T> LazyColumnWithLoadingState(
|
|||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
LazyColumnPullRefresh(
|
LazyColumnPullRefresh(
|
||||||
modifier = innerModifier,
|
|
||||||
refreshState = refreshState,
|
refreshState = refreshState,
|
||||||
isRefreshing = loadingState.isRefreshing,
|
isRefreshing = loadingState.isRefreshing,
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
verticalArrangement = verticalArrangement,
|
||||||
lazyColumnContent = { lazyColumnContent(list) },
|
lazyColumnContent = { lazyColumnContent(list) },
|
||||||
|
modifier = innerModifier,
|
||||||
)
|
)
|
||||||
|
|
||||||
ErrorSnackbar(
|
ErrorSnackbar(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<string name="shopping_lists_screen_cart_icon">Shopping cart</string>
|
<string name="shopping_lists_screen_cart_icon">Shopping cart</string>
|
||||||
<string name="shopping_list_screen_unknown_error">Unknown error</string>
|
<string name="shopping_list_screen_unknown_error">Unknown error</string>
|
||||||
<string name="shopping_list_screen_empty_list">%1$s is empty</string>
|
<string name="shopping_list_screen_empty_list">%1$s is empty</string>
|
||||||
|
<string name="shopping_list_screen_delete_icon_content_description">Delete</string>
|
||||||
<string name="shopping_lists_screen_empty">No shopping lists found</string>
|
<string name="shopping_lists_screen_empty">No shopping lists found</string>
|
||||||
<string name="shopping_lists_screen_unauthorized_error">Authentication is required</string>
|
<string name="shopping_lists_screen_unauthorized_error">Authentication is required</string>
|
||||||
<string name="shopping_lists_screen_no_connection">No server connection</string>
|
<string name="shopping_lists_screen_no_connection">No server connection</string>
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g
|
org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g
|
||||||
org.gradle.configureondemand=true
|
|
||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
org.gradle.unsafe.configuration-cache=true
|
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=false
|
android.enableJetifier=false
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
|
|||||||
Reference in New Issue
Block a user