@ -16,14 +16,17 @@
package com.google.samples.apps.nowinandroid.feature.bookmarks
package com.google.samples.apps.nowinandroid.feature.bookmarks
import androidx.annotation.VisibleForTesting
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.PaddingValues
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.layout.wrapContentSize
@ -31,19 +34,29 @@ 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.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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.platform.testTag
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
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.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
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.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success
import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank
import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank
import com.google.samples.apps.nowinandroid.core.ui.newsFeed
import com.google.samples.apps.nowinandroid.core.ui.newsFeed
@ -61,8 +74,39 @@ internal fun BookmarksRoute(
)
)
}
}
/ * *
* Displays the user ' s bookmarked articles . Includes support for loading and empty states .
* /
@VisibleForTesting ( otherwise = VisibleForTesting . PRIVATE )
@Composable
internal fun BookmarksScreen (
feedState : NewsFeedUiState ,
removeFromBookmarks : ( String ) -> Unit ,
modifier : Modifier = Modifier
) {
when ( feedState ) {
Loading -> LoadingState ( modifier )
is Success -> if ( feedState . feed . isNotEmpty ( ) ) {
BookmarksGrid ( feedState , removeFromBookmarks , modifier )
} else {
EmptyState ( modifier )
}
}
}
@Composable
@Composable
fun BookmarksScreen (
private fun LoadingState ( modifier : Modifier = Modifier ) {
NiaLoadingWheel (
modifier = modifier
. fillMaxWidth ( )
. wrapContentSize ( )
. testTag ( " forYou:loading " ) ,
contentDesc = stringResource ( id = R . string . saved _loading ) ,
)
}
@Composable
private fun BookmarksGrid (
feedState : NewsFeedUiState ,
feedState : NewsFeedUiState ,
removeFromBookmarks : ( String ) -> Unit ,
removeFromBookmarks : ( String ) -> Unit ,
modifier : Modifier = Modifier
modifier : Modifier = Modifier
@ -79,41 +123,80 @@ fun BookmarksScreen(
. fillMaxSize ( )
. fillMaxSize ( )
. testTag ( " bookmarks:feed " )
. testTag ( " bookmarks:feed " )
) {
) {
newsFeed (
feedState = feedState ,
onNewsResourcesCheckedChanged = { id , _ -> removeFromBookmarks ( id ) } ,
)
item ( span = { GridItemSpan ( maxLineSpan ) } ) {
Spacer ( Modifier . windowInsetsBottomHeight ( WindowInsets . safeDrawing ) )
}
}
}
when ( feedState ) {
@Composable
is NewsFeedUiState . Loading -> {
private fun EmptyState ( modifier : Modifier = Modifier ) {
item ( span = { GridItemSpan ( maxLineSpan ) } ) {
Column (
NiaLoadingWheel (
modifier = modifier
modifier = Modifier
. padding ( 16. dp )
. fillMaxWidth ( )
. fillMaxSize ( )
. wrapContentSize ( )
. testTag ( " bookmarks:empty " ) ,
. testTag ( " forYou:loading " ) ,
verticalArrangement = Arrangement . Center ,
contentDesc = stringResource ( id = R . string . saved _loading ) ,
horizontalAlignment = Alignment . CenterHorizontally
)
) {
}
Image (
}
modifier = Modifier . fillMaxWidth ( ) ,
painter = painterResource ( id = R . drawable . img _empty _bookmarks ) ,
contentDescription = null
)
Spacer ( modifier = Modifier . height ( 16. dp ) )
Text (
text = stringResource ( id = R . string . bookmarks _empty _error ) ,
modifier = Modifier . fillMaxWidth ( ) ,
textAlign = TextAlign . Center ,
style = MaterialTheme . typography . titleMedium ,
fontWeight = FontWeight . Bold
)
is NewsFeedUiState . Success -> {
Spacer ( modifier = Modifier . height ( 8. dp ) )
if ( feedState . feed . isNotEmpty ( ) ) {
newsFeed (
Text (
feedState = feedState ,
text = stringResource ( id = R . string . bookmarks _empty _description ) ,
onNewsResourcesCheckedChanged = { id , _ -> removeFromBookmarks ( id ) } ,
modifier = Modifier . fillMaxWidth ( ) ,
)
textAlign = TextAlign . Center ,
item ( span = { GridItemSpan ( maxLineSpan ) } ) {
style = MaterialTheme . typography . bodyMedium
Spacer ( Modifier . windowInsetsBottomHeight ( WindowInsets . safeDrawing ) )
)
}
}
} else item ( span = { GridItemSpan ( maxLineSpan ) } ) {
}
Box (
modifier = Modifier
@Preview
. fillMaxHeight ( )
@Composable
. wrapContentSize ( )
private fun LoadingStatePreview ( ) {
. testTag ( " bookmarks:empty " ) ,
NiaTheme {
contentAlignment = Alignment . Center
LoadingState ( )
) {
}
Text ( text = stringResource ( id = R . string . bookmarks _empty ) )
}
}
@Preview
@Composable
private fun BookmarksGridPreview ( ) {
NiaTheme {
BookmarksGrid (
feedState = Success (
previewNewsResources . map {
SaveableNewsResource ( it , false )
}
}
}
) ,
}
removeFromBookmarks = { }
)
}
}
@Preview
@Composable
fun EmptyStatePreview ( ) {
NiaTheme {
EmptyState ( )
}
}
}
}