restore stateFlow

This commit is contained in:
Stefano Sansone
2024-04-03 10:05:49 +02:00
parent 89dfc98622
commit fa2659f887
2 changed files with 141 additions and 152 deletions

View File

@ -8,6 +8,8 @@ import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
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.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@ -26,16 +28,16 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Archive
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Unarchive
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material.rememberDismissState
import androidx.compose.material.rememberScaffoldState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -52,6 +54,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
@ -70,6 +73,7 @@ import app.omnivore.omnivore.feature.save.SaveState
import app.omnivore.omnivore.feature.save.SaveViewModel
import app.omnivore.omnivore.feature.savedItemViews.SavedItemCard
import app.omnivore.omnivore.navigation.Routes
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@ -141,8 +145,7 @@ internal fun LibraryView(
is LibraryUiState.Success -> {
LibraryViewContent(
viewModel,
modifier = Modifier
.padding(top = paddingValues.calculateTopPadding()),
paddingValues = paddingValues,
uiState = uiState
)
}
@ -293,152 +296,155 @@ fun EditBottomSheet(
}
@OptIn(ExperimentalMaterialApi::class)
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
@Composable
fun LibraryViewContent(
libraryViewModel: LibraryViewModel,
modifier: Modifier,
paddingValues: PaddingValues,
uiState: LibraryUiState
) {
val context = LocalContext.current
val listState = rememberLazyListState()
val pullRefreshState = rememberPullRefreshState(
refreshing = libraryViewModel.isRefreshing,
onRefresh = { libraryViewModel.refresh() }
)
val pullToRefreshState = rememberPullToRefreshState()
if (pullToRefreshState.isRefreshing) {
LaunchedEffect(true) {
// fetch something
delay(1500)
libraryViewModel.refresh()
pullToRefreshState.endRefresh()
}
}
val selectedItem: SavedItemWithLabelsAndHighlights? by libraryViewModel.actionsMenuItemLiveData.observeAsState()
Box(
modifier = Modifier
.padding(top = paddingValues.calculateTopPadding())
.fillMaxSize()
.pullRefresh(pullRefreshState)
.nestedScroll(pullToRefreshState.nestedScrollConnection)
) {
Column {
LibraryFilterBar()
HorizontalDivider()
LazyColumn(
state = listState,
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
items(
items = (uiState as LibraryUiState.Success).items,
key = { item -> item.savedItem.savedItemId }
) { cardDataWithLabels ->
val swipeThreshold = 0.45f
LazyColumn(
state = listState,
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxSize()
) {
item {
LibraryFilterBar(libraryViewModel)
}
items(
items = (uiState as LibraryUiState.Success).items,
key = { item -> item.savedItem.savedItemId }
) { cardDataWithLabels ->
val swipeThreshold = 0.45f
val currentThresholdFraction = remember { mutableStateOf(0f) }
val currentItem by rememberUpdatedState(cardDataWithLabels.savedItem)
val swipeState = rememberDismissState(
confirmStateChange = {
when(it) {
DismissValue.Default -> {
return@rememberDismissState false
}
DismissValue.DismissedToEnd -> {
if (currentThresholdFraction.value < swipeThreshold) {
val currentThresholdFraction = remember { mutableStateOf(0f) }
val currentItem by rememberUpdatedState(cardDataWithLabels.savedItem)
val swipeState = rememberDismissState(
confirmStateChange = {
when(it) {
DismissValue.Default -> {
return@rememberDismissState false
}
}
DismissValue.DismissedToStart -> {
if (currentThresholdFraction.value < swipeThreshold) {
return@rememberDismissState false
DismissValue.DismissedToEnd -> {
if (currentThresholdFraction.value < swipeThreshold) {
return@rememberDismissState false
}
}
DismissValue.DismissedToStart -> {
if (currentThresholdFraction.value < swipeThreshold) {
return@rememberDismissState false
}
}
}
}
if (it == DismissValue.DismissedToEnd) { // Archiving/UnArchiving.
if (currentItem.isArchived) {
libraryViewModel.unarchiveSavedItem(currentItem.savedItemId)
} else {
libraryViewModel.archiveSavedItem(currentItem.savedItemId)
if (it == DismissValue.DismissedToEnd) { // Archiving/UnArchiving.
if (currentItem.isArchived) {
libraryViewModel.unarchiveSavedItem(currentItem.savedItemId)
} else {
libraryViewModel.archiveSavedItem(currentItem.savedItemId)
}
} else if (it == DismissValue.DismissedToStart) { // Deleting.
libraryViewModel.deleteSavedItem(currentItem.savedItemId)
}
} else if (it == DismissValue.DismissedToStart) { // Deleting.
libraryViewModel.deleteSavedItem(currentItem.savedItemId)
}
true
}
)
SwipeToDismiss(
state = swipeState,
directions = setOf(DismissDirection.StartToEnd, DismissDirection.EndToStart),
dismissThresholds = { FractionalThreshold(swipeThreshold) },
background = {
val direction = swipeState.dismissDirection ?: return@SwipeToDismiss
val color by animateColorAsState(
when (swipeState.targetValue) {
DismissValue.Default -> Color.LightGray
DismissValue.DismissedToEnd -> Color.Green
DismissValue.DismissedToStart -> Color.Red
}, label = "backgroundColor"
)
val alignment = when (direction) {
DismissDirection.StartToEnd -> Alignment.CenterStart
DismissDirection.EndToStart -> Alignment.CenterEnd
true
}
val icon = when (direction) {
DismissDirection.StartToEnd -> if (currentItem.isArchived) Icons.Default.Unarchive else Icons.Default.Archive
DismissDirection.EndToStart -> Icons.Default.Delete
}
val scale by animateFloatAsState(
if (swipeState.targetValue == DismissValue.Default) 0.75f else 1f,
label = "scaleAnimation"
)
Box(
Modifier
.fillMaxSize()
.background(color)
.padding(horizontal = 20.dp),
contentAlignment = alignment
) {
currentThresholdFraction.value = swipeState.progress.fraction
Icon(
icon,
contentDescription = null,
modifier = Modifier.scale(scale)
)
SwipeToDismiss(
state = swipeState,
directions = setOf(DismissDirection.StartToEnd, DismissDirection.EndToStart),
dismissThresholds = { FractionalThreshold(swipeThreshold) },
background = {
val direction = swipeState.dismissDirection ?: return@SwipeToDismiss
val color by animateColorAsState(
when (swipeState.targetValue) {
DismissValue.Default -> Color.LightGray
DismissValue.DismissedToEnd -> Color.Green
DismissValue.DismissedToStart -> Color.Red
}, label = "backgroundColor"
)
}
},
dismissContent = {
val selected =
currentItem.savedItemId == selectedItem?.savedItem?.savedItemId
val savedItem = SavedItemWithLabelsAndHighlights(
savedItem = cardDataWithLabels.savedItem,
labels = cardDataWithLabels.labels,
highlights = cardDataWithLabels.highlights
)
SavedItemCard(
selected = selected,
savedItemViewModel = libraryViewModel,
savedItem = savedItem,
onClickHandler = {
libraryViewModel.actionsMenuItemLiveData.postValue(null)
val activityClass =
if (currentItem.contentReader == "PDF") PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java
val intent = Intent(context, activityClass)
intent.putExtra("SAVED_ITEM_SLUG", currentItem.slug)
context.startActivity(intent)
},
actionHandler = {
libraryViewModel.handleSavedItemAction(
currentItem.savedItemId,
it
val alignment = when (direction) {
DismissDirection.StartToEnd -> Alignment.CenterStart
DismissDirection.EndToStart -> Alignment.CenterEnd
}
val icon = when (direction) {
DismissDirection.StartToEnd -> if (currentItem.isArchived) Icons.Default.Unarchive else Icons.Default.Archive
DismissDirection.EndToStart -> Icons.Default.Delete
}
val scale by animateFloatAsState(
if (swipeState.targetValue == DismissValue.Default) 0.75f else 1f,
label = "scaleAnimation"
)
Box(
Modifier
.fillMaxSize()
.background(color)
.padding(horizontal = 20.dp),
contentAlignment = alignment
) {
currentThresholdFraction.value = swipeState.progress.fraction
Icon(
icon,
contentDescription = null,
modifier = Modifier.scale(scale)
)
}
)
},
)
when {
swipeState.isDismissed(DismissDirection.EndToStart) -> Reset(state = swipeState)
swipeState.isDismissed(DismissDirection.StartToEnd) -> Reset(state = swipeState)
},
dismissContent = {
val selected =
currentItem.savedItemId == selectedItem?.savedItem?.savedItemId
val savedItem = SavedItemWithLabelsAndHighlights(
savedItem = cardDataWithLabels.savedItem,
labels = cardDataWithLabels.labels,
highlights = cardDataWithLabels.highlights
)
SavedItemCard(
selected = selected,
savedItemViewModel = libraryViewModel,
savedItem = savedItem,
onClickHandler = {
libraryViewModel.actionsMenuItemLiveData.postValue(null)
val activityClass =
if (currentItem.contentReader == "PDF") PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java
val intent = Intent(context, activityClass)
intent.putExtra("SAVED_ITEM_SLUG", currentItem.slug)
context.startActivity(intent)
},
actionHandler = {
libraryViewModel.handleSavedItemAction(
currentItem.savedItemId,
it
)
}
)
},
)
when {
swipeState.isDismissed(DismissDirection.EndToStart) -> Reset(state = swipeState)
swipeState.isDismissed(DismissDirection.StartToEnd) -> Reset(state = swipeState)
}
}
}
}
@ -453,10 +459,9 @@ fun LibraryViewContent(
}
}
PullRefreshIndicator(
refreshing = libraryViewModel.isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter)
PullToRefreshContainer(
modifier = Modifier.align(Alignment.TopCenter),
state = pullToRefreshState,
)
// LabelsSelectionSheet(viewModel = libraryViewModel)

View File

@ -32,16 +32,22 @@ import com.apollographql.apollo3.api.Optional
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.time.Instant
import javax.inject.Inject
@OptIn(ExperimentalCoroutinesApi::class)
@HiltViewModel
class LibraryViewModel @Inject constructor(
private val networker: Networker,
@ -67,8 +73,7 @@ class LibraryViewModel @Inject constructor(
)
)
// Correct way - but not working
/* val uiState: StateFlow<LibraryUiState> = _libraryQuery.flatMapLatest { query ->
val uiState: StateFlow<LibraryUiState> = _libraryQuery.flatMapLatest { query ->
libraryRepository.getSavedItems(query)
}
.map(LibraryUiState::Success)
@ -76,24 +81,8 @@ class LibraryViewModel @Inject constructor(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = LibraryUiState.Loading
)*/
)
// This approach needs to be replaced with the StateFlow above after fixing Room Flow
private val _uiState = MutableStateFlow<LibraryUiState>(LibraryUiState.Loading)
val uiState: StateFlow<LibraryUiState> = _uiState
init {
loadSavedItems()
}
private fun loadSavedItems() {
viewModelScope.launch {
libraryRepository.getSavedItems(_libraryQuery.value)
.collect { favoriteNews ->
_uiState.value = LibraryUiState.Success(favoriteNews)
}
}
}
val appliedFilterLiveData = MutableLiveData(SavedItemFilter.INBOX)
val appliedSortFilterLiveData = MutableLiveData(SavedItemSortFilter.NEWEST)
@ -265,7 +254,6 @@ class LibraryViewModel @Inject constructor(
excludedLabels = excludeLabels,
allowedContentReaders = allowedContentReaders
)
loadSavedItems()
}
}
@ -345,17 +333,13 @@ class LibraryViewModel @Inject constructor(
SavedItemAction.MarkRead -> {
viewModelScope.launch {
_uiState.value = LibraryUiState.Success(emptyList())
libraryRepository.updateReadingProgress(itemID, 100.0, 0)
loadSavedItems()
}
}
SavedItemAction.MarkUnread -> {
viewModelScope.launch {
_uiState.value = LibraryUiState.Success(emptyList())
libraryRepository.updateReadingProgress(itemID, 0.0, 0)
loadSavedItems()
}
}
}