Skip to content

Commit

Permalink
Paginated trending movies (#4)
Browse files Browse the repository at this point in the history
Shows page of trending movies using api pagination.
  • Loading branch information
julioromano authored Mar 9, 2023
1 parent 4a6e64f commit a6540a6
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 134 deletions.
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ androidx-compose-compiler = "1.4.3"
androidx-hilt = "1.0.0"
androidx-lifecycle = "2.6.0"
androidx-navigation = "2.5.3"
androidx-paging = "3.1.1"
androidx-test = "1.5.0"
androidx-testEspresso = "3.5.1"
androidx-workManager = "2.8.0"
Expand Down Expand Up @@ -96,6 +97,9 @@ androidx-lifecycleViewmodelCompose = { module = "androidx.lifecycle:lifecycle-vi
androidx-media = "androidx.media:media:1.6.0"
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
androidx-navigation-testing = { module = "androidx.navigation:navigation-testing", version.ref = "androidx-navigation" }
androidx-pagingCommon = { module = "androidx.paging:paging-common", version.ref = "androidx-paging" }
androidx-pagingCompose = "androidx.paging:paging-compose:1.0.0-alpha18"
androidx-pagingRuntime = { module = "androidx.paging:paging-runtime", version.ref = "androidx-paging" }
androidx-palette = "androidx.palette:palette-ktx:1.0.0"
androidx-testCore = { module = "androidx.test:core-ktx", version.ref = "androidx-test" }
androidx-testEspressoContrib = { module = "androidx.test.espresso:espresso-contrib", version.ref = "androidx-testEspresso" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public fun HttpApiImpl(
}
if (enableLogging) {
addNetworkInterceptor(
HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY },
HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC },
)
}
}.build(),
Expand Down
2 changes: 2 additions & 0 deletions trending/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ dependencies {
implementation(projects.database.public)
implementation(projects.httpapi.public)
implementation(libs.coilCompose)
implementation(libs.androidx.pagingRuntime)
implementation(libs.androidx.pagingCompose)
implementation(libs.square.sqlDelightAndroidPaging3)
implementation(libs.square.sqlDelightCoroutines)
testImplementation(libs.square.turbine)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,64 +1,41 @@
package net.marcoromano.tmdb.trending

import android.content.res.Configuration
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle

@Composable
internal fun TrendingScreen(
navToDetail: (id: Long) -> Unit,
) {
val vm = hiltViewModel<TrendingViewModel>()
val state by vm.state.collectAsStateWithLifecycle()
TrendingScreen(
state = state,
loadTrending = vm::loadTrending,
navToDetail = navToDetail,
)
}
import net.marcoromano.tmdb.trending.widgets.trending.TrendingLazyVerticalGrid

@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun TrendingScreen(
state: TrendingState,
loadTrending: () -> Unit,
navToDetail: (id: Long) -> Unit,
) {
LaunchedEffect(Unit) { loadTrending() }
val behavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Scaffold(
modifier = Modifier.nestedScroll(behavior.nestedScrollConnection),
topBar = {
CenterAlignedTopAppBar(title = { Text(text = stringResource(R.string.trending)) })
CenterAlignedTopAppBar(
title = { Text(text = stringResource(R.string.trending)) },
scrollBehavior = behavior,
)
},
) { paddingValues ->
LazyVerticalGrid(
TrendingLazyVerticalGrid(
columns = GridCells.Adaptive(150.dp),
modifier = Modifier
.padding(paddingValues)
.fillMaxSize(),
) {
items(state.movies) { movie ->
Movie(
movie = movie,
navToDetail = { navToDetail(it) },
)
}
}
modifier = Modifier.padding(paddingValues),
onMovieClick = { navToDetail(it) },
)
}
}

Expand All @@ -67,8 +44,6 @@ internal fun TrendingScreen(
@Composable
private fun Preview() {
TrendingScreen(
state = TrendingState(),
loadTrending = {},
navToDetail = {},
)
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package net.marcoromano.tmdb.trending
package net.marcoromano.tmdb.trending.widgets.trending

import android.content.res.Configuration
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import androidx.compose.foundation.Image
Expand All @@ -9,12 +8,18 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Error
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
Expand All @@ -26,12 +31,61 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import net.marcoromano.tmdb.httpapi.TrendingMovies

@Composable
internal fun Movie(
internal fun TrendingLazyVerticalGrid(
columns: GridCells,
modifier: Modifier = Modifier,
onMovieClick: (id: Long) -> Unit,
) {
val vm = hiltViewModel<TrendingLazyVerticalGridViewModel>()
TrendingLazyVerticalGrid(
columns,
modifier,
vm.pager,
onMovieClick,
)
}

@Composable
private fun TrendingLazyVerticalGrid(
columns: GridCells,
modifier: Modifier,
pager: Flow<PagingData<TrendingMovies.Movie>>,
onMovieClick: (id: Long) -> Unit,
) {
val trendingPagingData = pager.collectAsLazyPagingItems()
LazyVerticalGrid(
columns = columns,
modifier = modifier,
) {
items(trendingPagingData.itemCount) { index ->
val movie = trendingPagingData[index]
if (movie != null) {
Movie(
movie = movie,
navToDetail = { onMovieClick(it) },
)
} else {
Icon(
imageVector = Icons.Default.Error,
contentDescription = null,
)
}
}
}
}

@Composable
private fun Movie(
movie: TrendingMovies.Movie,
navToDetail: (id: Long) -> Unit,
) {
Expand Down Expand Up @@ -87,10 +141,9 @@ internal fun Movie(
}
}

@Preview(name = "Day mode")
@Preview(name = "Night mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview
@Composable
private fun Preview() {
private fun PreviewMovie() {
Movie(
movie = TrendingMovies.Movie(
id = 0,
Expand All @@ -103,3 +156,35 @@ private fun Preview() {
navToDetail = {},
)
}

@Preview
@Composable
private fun PreviewTrending() {
TrendingLazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.fillMaxSize(),
pager = flowOf(
PagingData.from(
listOf(
TrendingMovies.Movie(
id = 0,
title = "A very long title that must be wrapped in multiple lines",
poster_path = "https://dummyimage.com/500x750/000/fff.jpg",
overview = "Once upon a time...",
vote_average = 1.2,
release_date = "2022-02-03",
),
TrendingMovies.Movie(
id = 0,
title = "A very long title that must be wrapped in multiple lines",
poster_path = "https://dummyimage.com/500x750/000/fff.jpg",
overview = "Once upon a time...",
vote_average = 1.2,
release_date = "2022-02-03",
),
),
),
),
onMovieClick = {},
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package net.marcoromano.tmdb.trending.widgets.trending

import androidx.lifecycle.ViewModel
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingState
import dagger.hilt.android.lifecycle.HiltViewModel
import net.marcoromano.tmdb.httpapi.HttpApi
import net.marcoromano.tmdb.httpapi.TrendingMovies
import javax.inject.Inject

@HiltViewModel
internal class TrendingLazyVerticalGridViewModel @Inject constructor(
private val httpApi: HttpApi,
) : ViewModel() {
val pager = Pager(PagingConfig(pageSize = 20)) { // 20 comes from tmdb api docs
NetworkPagingSource(httpApi)
}.flow
}

private class NetworkPagingSource(
private val httpApi: HttpApi,
) : PagingSource<Int, TrendingMovies.Movie>() {
override fun getRefreshKey(state: PagingState<Int, TrendingMovies.Movie>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, TrendingMovies.Movie> {
return runCatching {
httpApi.trendingMovies(page = params.key ?: 1)
}.fold(
{
LoadResult.Page(
data = it.results,
prevKey = if (it.page > 1) it.page - 1 else null,
nextKey = if (it.page < it.total_pages) it.page + 1 else null,
)
},
{
LoadResult.Error(it)
},
)
}
}
Loading

0 comments on commit a6540a6

Please sign in to comment.