MovieListScreen.kt
package com.louisfn.somovie.feature.movielist
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey
import com.louisfn.somovie.domain.model.Movie
import com.louisfn.somovie.feature.movielist.MovieListUiState.LoadNextPageState
import com.louisfn.somovie.ui.common.extension.PagingItemsLoadStateErrorEffect
import com.louisfn.somovie.ui.common.extension.collectAsStateLifecycleAware
import com.louisfn.somovie.ui.common.extension.plus
import com.louisfn.somovie.ui.common.util.ExploreCategoryUiHelper.canDisplayVotes
import com.louisfn.somovie.ui.common.util.ExploreCategoryUiHelper.label
import com.louisfn.somovie.ui.component.DefaultTopAppBar
import com.louisfn.somovie.ui.component.IndeterminateProgressIndicator
import com.louisfn.somovie.ui.component.TextRetryButton
import com.louisfn.somovie.ui.component.movie.MovieCard
import com.louisfn.somovie.ui.component.movie.MovieCardPlaceholder
import com.louisfn.somovie.ui.theme.Dimens.DefaultScreenHorizontalPadding
import com.louisfn.somovie.ui.theme.Dimens.DefaultScreenVerticalPadding
private const val MovieGridNbrColumn = 3
@Composable
internal fun MovieListScreen(
showDetail: (Movie) -> Unit,
navigateUp: () -> Unit,
viewModel: MovieListViewModel = hiltViewModel(),
) {
val state by viewModel.uiState.collectAsStateLifecycleAware()
val pagingItems = viewModel.pagedMovies.collectAsLazyPagingItems()
PagingItemsLoadStateErrorEffect(
pagingItems = pagingItems,
onRefreshError = viewModel::onPagingError,
onAppendError = viewModel::onPagingError,
)
LaunchedEffect(pagingItems.loadState, pagingItems) {
viewModel.onLoadStateChanged(pagingItems.loadState)
}
MovieListScreen(
uiState = state,
pagingItems = pagingItems,
showDetail = showDetail,
navigateUp = navigateUp,
)
}
@Composable
private fun MovieListScreen(
uiState: MovieListUiState,
pagingItems: LazyPagingItems<Movie>,
showDetail: (Movie) -> Unit,
navigateUp: () -> Unit,
) {
Scaffold(
modifier = Modifier.systemBarsPadding(),
topBar = {
DefaultTopAppBar(
text = uiState.category.label,
navigateUp = navigateUp,
)
},
) {
Box(modifier = Modifier.fillMaxSize()) {
MovieListLazyGrid(
pagingItems = pagingItems,
loadNextPageState = uiState.loadNextPageState,
contentPadding = it,
showVotes = uiState.category.canDisplayVotes,
showDetail = showDetail,
)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun MovieListLazyGrid(
pagingItems: LazyPagingItems<Movie>,
loadNextPageState: LoadNextPageState,
contentPadding: PaddingValues,
showVotes: Boolean,
showDetail: (Movie) -> Unit,
) {
// https://issuetracker.google.com/issues/177245496#comment23
if (pagingItems.itemCount == 0) return
LazyVerticalGrid(
columns = GridCells.Fixed(MovieGridNbrColumn),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
contentPadding = contentPadding + PaddingValues(
horizontal = DefaultScreenHorizontalPadding,
vertical = DefaultScreenVerticalPadding,
),
) {
items(
count = pagingItems.itemCount,
key = pagingItems.itemKey { it.id },
contentType = pagingItems.itemContentType {},
) { index ->
val movie = pagingItems[index]
if (movie != null) {
MovieCard(
movie = movie,
showVotes = showVotes,
showDetail = showDetail,
)
} else {
MovieCardPlaceholder()
}
}
if (loadNextPageState == LoadNextPageState.LOADING) {
item(
span = { GridItemSpan(MovieGridNbrColumn) },
) {
Loader()
}
}
if (loadNextPageState == LoadNextPageState.FAILED) {
item(
span = { GridItemSpan(MovieGridNbrColumn) },
) {
TextRetryButton(
modifier = Modifier.wrapContentWidth(),
onClick = pagingItems::retry,
)
}
}
}
}
@Composable
private fun Loader() {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
horizontalArrangement = Arrangement.Center,
) {
IndeterminateProgressIndicator()
}
}