Add Undo snackbar on Bookmark removal

Change-Id: I1fefd6e72378e26ae35b66e032529a116cff9a79
pull/640/head
Neelansh Sahai 2 years ago
parent 2c18740d62
commit bf747434cd

@ -14,8 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
import com.android.build.api.dsl.ManagedVirtualDevice
plugins { plugins {
id("nowinandroid.android.feature") id("nowinandroid.android.feature")
id("nowinandroid.android.library.compose") id("nowinandroid.android.library.compose")
@ -28,4 +26,4 @@ android {
dependencies { dependencies {
implementation(libs.androidx.compose.material3.windowSizeClass) implementation(libs.androidx.compose.material3.windowSizeClass)
} }

@ -19,6 +19,7 @@ package com.google.samples.apps.nowinandroid.feature.bookmarks
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
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.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -34,13 +35,23 @@ import androidx.compose.foundation.lazy.grid.GridCells.Adaptive
import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration.Short
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult.ActionPerformed
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -50,6 +61,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalTintTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalTintTheme
@ -76,12 +89,16 @@ internal fun BookmarksRoute(
onNewsResourceViewed = { viewModel.setNewsResourceViewed(it, true) }, onNewsResourceViewed = { viewModel.setNewsResourceViewed(it, true) },
onTopicClick = onTopicClick, onTopicClick = onTopicClick,
modifier = modifier, modifier = modifier,
shouldDisplayUndoBookmark = viewModel.shouldDisplayUndoBookmark,
undoBookmarkRemoval = viewModel::undoBookmarkRemoval,
clearUndoState = viewModel::clearUndoState,
) )
} }
/** /**
* Displays the user's bookmarked articles. Includes support for loading and empty states. * Displays the user's bookmarked articles. Includes support for loading and empty states.
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
@Composable @Composable
internal fun BookmarksScreen( internal fun BookmarksScreen(
@ -90,13 +107,51 @@ internal fun BookmarksScreen(
onNewsResourceViewed: (String) -> Unit, onNewsResourceViewed: (String) -> Unit,
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
shouldDisplayUndoBookmark: Boolean = false,
undoBookmarkRemoval: () -> Unit = {},
clearUndoState: () -> Unit = {},
) { ) {
when (feedState) { val bookmarkRemovedMessage = stringResource(id = R.string.bookmark_removed)
Loading -> LoadingState(modifier) val undoText = stringResource(id = R.string.undo)
is Success -> if (feedState.feed.isNotEmpty()) { val snackbarHostState = remember { SnackbarHostState() }
BookmarksGrid(feedState, removeFromBookmarks, onNewsResourceViewed, onTopicClick, modifier)
} else { LaunchedEffect(shouldDisplayUndoBookmark) {
EmptyState(modifier) if (shouldDisplayUndoBookmark) {
val snackBarResult = snackbarHostState.showSnackbar(
message = bookmarkRemovedMessage,
actionLabel = undoText,
duration = Short,
)
when (snackBarResult) {
ActionPerformed -> { undoBookmarkRemoval() }
else -> { clearUndoState() }
}
}
}
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_STOP) {
clearUndoState()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
Scaffold(snackbarHost = { SnackbarHost(hostState = snackbarHostState) }) {
Box(
modifier = Modifier.padding(it).fillMaxSize(),
) {
when (feedState) {
Loading -> LoadingState(modifier)
is Success -> if (feedState.feed.isNotEmpty()) {
BookmarksGrid(feedState, removeFromBookmarks, onNewsResourceViewed, onTopicClick, modifier)
} else {
EmptyState(modifier)
}
}
} }
} }
TrackScreenViewEvent(screenName = "Saved") TrackScreenViewEvent(screenName = "Saved")

@ -16,6 +16,9 @@
package com.google.samples.apps.nowinandroid.feature.bookmarks package com.google.samples.apps.nowinandroid.feature.bookmarks
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
@ -38,6 +41,9 @@ class BookmarksViewModel @Inject constructor(
userNewsResourceRepository: UserNewsResourceRepository, userNewsResourceRepository: UserNewsResourceRepository,
) : ViewModel() { ) : ViewModel() {
var shouldDisplayUndoBookmark by mutableStateOf(false)
private var lastRemovedBookmarkId: String? = null
val feedUiState: StateFlow<NewsFeedUiState> = val feedUiState: StateFlow<NewsFeedUiState> =
userNewsResourceRepository.observeAllBookmarked() userNewsResourceRepository.observeAllBookmarked()
.map<List<UserNewsResource>, NewsFeedUiState>(NewsFeedUiState::Success) .map<List<UserNewsResource>, NewsFeedUiState>(NewsFeedUiState::Success)
@ -50,6 +56,8 @@ class BookmarksViewModel @Inject constructor(
fun removeFromSavedResources(newsResourceId: String) { fun removeFromSavedResources(newsResourceId: String) {
viewModelScope.launch { viewModelScope.launch {
shouldDisplayUndoBookmark = true
lastRemovedBookmarkId = newsResourceId
userDataRepository.updateNewsResourceBookmark(newsResourceId, false) userDataRepository.updateNewsResourceBookmark(newsResourceId, false)
} }
} }
@ -59,4 +67,18 @@ class BookmarksViewModel @Inject constructor(
userDataRepository.setNewsResourceViewed(newsResourceId, viewed) userDataRepository.setNewsResourceViewed(newsResourceId, viewed)
} }
} }
fun undoBookmarkRemoval() {
viewModelScope.launch {
lastRemovedBookmarkId?.let {
userDataRepository.updateNewsResourceBookmark(it, true)
}
}
clearUndoState()
}
fun clearUndoState() {
shouldDisplayUndoBookmark = false
lastRemovedBookmarkId = null
}
} }

@ -22,4 +22,6 @@
<string name="top_app_bar_action_menu">Menu</string> <string name="top_app_bar_action_menu">Menu</string>
<string name="bookmarks_empty_error">No saved updates</string> <string name="bookmarks_empty_error">No saved updates</string>
<string name="bookmarks_empty_description">Updates you save will be stored here\nto read later</string> <string name="bookmarks_empty_description">Updates you save will be stored here\nto read later</string>
</resources> <string name="bookmark_removed">Bookmark removed</string>
<string name="undo">UNDO</string>
</resources>

Loading…
Cancel
Save