My Android app is written using Jetpack Compose and utilising an MVVM architecture. It is a basic movie list app utilising the TMDB API.
The app displays a list of items using a LazyVerticalGrid
with two columns of tiles. The user will scroll the list and click on one at which point a details page is navigated to. I am using a NavController
for this. This is an independent screen, not a bottom sheet dialogue (although I am considering changing to that).
Navigation is handled via the MainActivity
with each screen being a separate Screen
file utilising a viewModel and repository as appropriate.
The user scrolls and let's say he's scrolled down three pairs rows and clicked on the 7th item. When the user is finished with the details screen he/she will return back to the previous screen, the list. The problem I have is that this screen is redrawn which means that it will resend a request to the cloud and repaint and the user is returned to the top of the list.
I can't see what the reason for this is but i =. Any ideas please on how I can resolve the issue or is my only option to remember the last scrolled to index and then re-scroll to it?
This is the MovieListScreen
@Composable
fun MovieListScreen(movieDetailsNavigationCallback: (Int) -> Unit) {
val viewModel: MovieListViewModel = hiltViewModel()
val movieDataList = viewModel.getMovieListPage().collectAsLazyPagingItems()
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.verticalGradient(
colors = listOf(
SplashGradientStart,
SplashGradientEnd
)
)
),
contentAlignment = Alignment.TopCenter
) {
VerticalGridButtons(movieDataList = movieDataList, movieDetailsNavigationCallback)
}
}
@Composable
fun VerticalGridButtons(
movieDataList: LazyPagingItems<MovieData>,
navigationCallback: (Int) -> Unit,
) {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(
start = 8.dp,
end = 8.dp,
bottom = 8.dp,
top = 100.dp
),
) {
items(
movieDataList.itemCount
) { index ->
movieDataList[index]?.let { MenuItemTile(it, navigationCallback) }
}
}
// TODO: Add error handling
}
@Composable
fun MenuItemTile(movieData: MovieData, navigationCallback: (Int) -> Unit) {
Card(
Modifier
.padding(8.dp)
.background(Color.Black.copy(alpha = 0.4f)),
shape = RoundedCornerShape(8.dp),
elevation = CardDefaults.cardElevation(
defaultElevation = 8.dp
),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.padding(4.dp)
.background(color = Color.Black.copy(alpha = 1.0f))
.fillMaxWidth(),
) {
AsyncImage(
model = BASE_URL + movieData.posterPath,
contentDescription = "Menu Thumbnail",
contentScale = ContentScale.FillBounds,
modifier = Modifier
.size(250.dp)
.padding(top = 0.dp)
.align(Alignment.CenterHorizontally)
.clickable(onClick = {
Timber.d("onClick event for movie ID == " + movieData.id)
navigationCallback(movieData.id)
}
)
)
Spacer(modifier = Modifier.height(height = 15.dp))
val movieTitleScroll = rememberScrollState(0)
Text(modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = 10.dp)
.horizontalScroll(movieTitleScroll),
text = movieData.originalTitle,
style = MaterialTheme.typography.displaySmall,
color = Color.White,
maxLines = 1
)
}
}
}
@Preview(showBackground = true)
@Composable
fun MovieListScreenPreview() {
DisneyMoviesTheme() {
MovieListScreen({})
}
}
This is the MovieListViewModel
@HiltViewModel
class MovieListViewModel @Inject constructor(private val repository: MoviesListRepository) : ViewModel() {
init {
viewModelScope.launch(Dispatchers.IO) {
}
}
fun getMovieListPage(): Flow<PagingData<MovieData>> = repository.getDiscoverMoviesPage().cachedIn(viewModelScope)
}
MainActivity
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val stringResourceProvider: StringResourceProviderImpl = StringResourceProviderImpl(resources)
setContent {
DMoviesTheme {
DMDBApp(stringResourceProvider)
}
}
}
}
@Composable
private fun DMDBApp(stringResourceProvider: StringResourceProviderImpl) {
val navController = rememberNavController()
NavHost(navController, startDestination = "splash_screen") {
composable(route = "splash_screen") {
SplashScreen {
navController.navigate("movie_list_screen")
}
}
composable(route = "movie_list_screen") {
MovieListScreen() { id ->
navController.navigate("destination_movie_details_screen/${id}")
}
}
composable(
route = "destination_movie_details_screen/{id}",
arguments = listOf (
navArgument("id") { type = NavType.IntType }
)
) {
val viewModel: MovieDetailsViewModel = hiltViewModel()
MovieDetailsScreen(viewModel.movieDetailsState.value)
}
}
}
I'm leaving the question up as I hope that this will help someone else to save the same issue.
Another piece of my code that was not referenced was the Repository.
class MoviesListRepository(private val tmdbWebService: TMDBWebService = TMDBWebService()) {
fun getDiscoverMoviesPage() = Pager(
config = PagingConfig(
pageSize = 20,
),
pagingSourceFactory = {
MoviesPagingSource(tmdbWebService)
}
).flow
}
This was the cause of the problem. The Pager is being recreated each time the compose parent function was called. To resolve this I had to make the following changes.
Stop using the repository class and instead change the viewModel as below.
@HiltViewModel
class MovieListViewModel @Inject constructor(
private val repository: MoviesListRepository,
private val tmdbWebService: TMDBWebService) : ViewModel() {
val items = Pager(
config = PagingConfig(
pageSize = 20,
),
pagingSourceFactory = {
MoviesPagingSource(tmdbWebService)
}
).flow.cachedIn(viewModelScope)
init {
viewModelScope.launch(Dispatchers.IO) {
}
}
//fun getMovieListPage(): Flow<PagingData<MovieData>> = repository.getDiscoverMoviesPage().cachedIn(viewModelScope)
}
Then simply update the MovieListScreen
as follows:
//val movieDataList = viewModel.getMovieListPage().collectAsLazyPagingItems() val movieDataList = viewModel.items.collectAsLazyPagingItems()
The result is that the movieList then remains at its previous position between navigating to and back from the description screen.
I found the following post which resolved my issue:
Paging 3 list auto refresh on navigation back in jetpack compose navigation