add paging to home screen tabs

This commit is contained in:
Owen LeJeune
2022-09-02 20:44:12 -04:00
parent ec12621743
commit 00e728b48c
5 changed files with 168 additions and 37 deletions

View File

@@ -7,12 +7,12 @@ plugins {
def composeVersion = "1.2.0-beta03"
android {
compileSdkVersion 32
compileSdkVersion 33
defaultConfig {
applicationId "com.owenlejeune.tvtime"
minSdkVersion 23
targetSdkVersion 32
targetSdkVersion 33
versionCode = 1
versionName = "1.0"
@@ -72,7 +72,7 @@ dependencies {
def compose_material3 = "1.0.0-alpha13"
def compose_accompanist = "0.24.10-beta"
def compose_navigation = "2.4.2"
def compose_paging = "1.0.0-alpha15"
def compose_paging = "1.0.0-alpha16"
def compose_constraint_layout = "1.0.1"
def compose_activity = "1.4.0"
implementation "androidx.compose.ui:ui:$compose"
@@ -89,6 +89,7 @@ dependencies {
implementation "androidx.navigation:navigation-compose:$compose_navigation"
implementation "androidx.paging:paging-compose:$compose_paging"
implementation "androidx.constraintlayout:constraintlayout-compose:$compose_constraint_layout"
implementation "androidx.paging:paging-compose:$compose_paging"
// material you
def monet_compat = "0.4.1"

View File

@@ -0,0 +1,35 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.owenlejeune.tvtime.api.tmdb.api.v3.HomePageService
import com.owenlejeune.tvtime.ui.navigation.MediaFetchFun
import retrofit2.Response
class HomePagePagingSource(private val service: HomePageService, private val mediaFetch: MediaFetchFun): PagingSource<Int, TmdbItem>() {
override fun getRefreshKey(state: PagingState<Int, TmdbItem>): Int? {
return state.anchorPosition
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, TmdbItem> {
return try {
val nextPage = params.key ?: 1
val mediaResponse = mediaFetch.invoke(service, nextPage)
if (mediaResponse.isSuccessful) {
val responseBody = mediaResponse.body()
val results = responseBody?.results ?: emptyList()
LoadResult.Page(
data = results,
prevKey = if (nextPage == 1) null else nextPage - 1,
nextKey = if (results.isEmpty() || responseBody == null) null else responseBody.page + 1
)
} else {
LoadResult.Invalid()
}
} catch (e: Exception) {
return LoadResult.Error(e)
}
}
}

View File

@@ -22,6 +22,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import coil.compose.AsyncImage
import coil.compose.rememberAsyncImagePainter
import com.google.accompanist.pager.ExperimentalPagerApi
@@ -33,6 +35,7 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Person
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TmdbItem
import com.owenlejeune.tvtime.extensions.dpToPx
import com.owenlejeune.tvtime.extensions.lazyPagingItems
import com.owenlejeune.tvtime.extensions.listItems
import com.owenlejeune.tvtime.utils.TmdbUtils
@@ -62,6 +65,37 @@ fun PosterGrid(
}
}
@Composable
fun PagingPosterGrid(
lazyPagingItems: LazyPagingItems<TmdbItem>?,
onClick: (id: Int) -> Unit = {}
) {
lazyPagingItems?.let {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = POSTER_WIDTH),
contentPadding = PaddingValues(8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
lazyPagingItems(lazyPagingItems) { item ->
item?.let {
PosterItem(
modifier = Modifier.padding(5.dp),
mediaItem = item,
onClick = onClick
)
}
}
lazyPagingItems.apply {
when {
loadState.refresh is LoadState.Loading -> {}
loadState.append is LoadState.Loading -> {}
loadState.append is LoadState.Error -> {}
}
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PeoplePosterGrid(

View File

@@ -1,24 +1,41 @@
package com.owenlejeune.tvtime.ui.navigation
import androidx.compose.runtime.Composable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.HomePageService
import com.owenlejeune.tvtime.api.tmdb.api.v3.MoviesService
import com.owenlejeune.tvtime.api.tmdb.api.v3.TvService
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.HomePagePagingSource
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.HomePageResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TmdbItem
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
import com.owenlejeune.tvtime.ui.screens.main.MediaTabContent
import com.owenlejeune.tvtime.utils.ResourceUtils
import kotlinx.coroutines.flow.Flow
import org.koin.core.component.inject
import retrofit2.Response
sealed class MediaTabNavItem(stringRes: Int, route: String, val screen: MediaNavComposableFun, val mediaFetchFun: MediaFetchFun): TabNavItem(route) {
sealed class MediaTabNavItem(
stringRes: Int,
route: String,
val screen: MediaNavComposableFun,
val movieViewModel: MediaTabViewModel?,
val tvViewModel: MediaTabViewModel?
): TabNavItem(route) {
private val resourceUtils: ResourceUtils by inject()
override val name = resourceUtils.getString(stringRes)
companion object {
val MovieItems = listOf(Popular, TopRated, NowPlaying, Upcoming)
val TvItems = listOf(Popular, TopRated, AiringToday, OnTheAir)
val MovieItems = listOf(Popular, NowPlaying, Upcoming, TopRated)
val TvItems = listOf(Popular, AiringToday, OnTheAir, TopRated)
private val Items = listOf(NowPlaying, Popular, TopRated, Upcoming, AiringToday, OnTheAir)
@@ -27,18 +44,70 @@ sealed class MediaTabNavItem(stringRes: Int, route: String, val screen: MediaNav
}
}
object Popular: MediaTabNavItem(R.string.nav_popular_title, "popular_route", screenContent, { s, p -> s.getPopular(p) } )
object TopRated: MediaTabNavItem(R.string.nav_top_rated_title, "top_rated_route", screenContent, { s, p -> s.getTopRated(p) } )
object NowPlaying: MediaTabNavItem(R.string.nav_now_playing_title, "now_playing_route", screenContent, { s, p -> s.getNowPlaying(p) } )
object Upcoming: MediaTabNavItem(R.string.nav_upcoming_title, "upcoming_route", screenContent, { s, p -> s.getUpcoming(p) } )
object AiringToday: MediaTabNavItem(R.string.nav_tv_airing_today_title, "airing_today_route", screenContent, { s, p -> s.getNowPlaying(p) } )
object OnTheAir: MediaTabNavItem(R.string.nav_tv_on_the_air, "on_the_air_route", screenContent, { s, p -> s.getUpcoming(p) } )
object Popular: MediaTabNavItem(
stringRes = R.string.nav_popular_title,
route = "popular_route",
screen = screenContent,
movieViewModel = MediaTabViewModel.PopularMoviesVM,
tvViewModel = MediaTabViewModel.PopularTvVM
)
object TopRated: MediaTabNavItem(
stringRes = R.string.nav_top_rated_title,
route = "top_rated_route",
screen = screenContent,
movieViewModel = MediaTabViewModel.TopRatedMoviesVM,
tvViewModel = MediaTabViewModel.TopRatedTvVM
)
object NowPlaying: MediaTabNavItem(
stringRes = R.string.nav_now_playing_title,
route = "now_playing_route",
screen = screenContent,
movieViewModel = MediaTabViewModel.NowPlayingMoviesVM,
tvViewModel = null
)
object Upcoming: MediaTabNavItem(
stringRes = R.string.nav_upcoming_title,
route = "upcoming_route",
screen = screenContent,
movieViewModel = MediaTabViewModel.UpcomingMoviesVM,
tvViewModel = null
)
object AiringToday: MediaTabNavItem(
stringRes = R.string.nav_tv_airing_today_title,
route = "airing_today_route",
screen = screenContent,
movieViewModel = null,
tvViewModel = MediaTabViewModel.AiringTodayTvVM
)
object OnTheAir: MediaTabNavItem(
stringRes = R.string.nav_tv_on_the_air,
route = "on_the_air_route",
screen = screenContent,
movieViewModel = null,
tvViewModel = MediaTabViewModel.OnTheAirTvVM
)
}
private val screenContent: MediaNavComposableFun = { appNavController, mediaViewType, mediaFetchFun ->
MediaTabContent(appNavController = appNavController, mediaType = mediaViewType, mediaFetchFun = mediaFetchFun)
private val screenContent: MediaNavComposableFun = { appNavController, mediaViewType, mediaTabItem ->
MediaTabContent(appNavController = appNavController, mediaType = mediaViewType, mediaTabItem = mediaTabItem)
}
typealias MediaNavComposableFun = @Composable (NavHostController, MediaViewType, MediaFetchFun) -> Unit
typealias MediaNavComposableFun = @Composable (NavHostController, MediaViewType, MediaTabNavItem) -> Unit
typealias MediaFetchFun = suspend (service: HomePageService, page: Int) -> Response<out HomePageResponse>
typealias MediaFetchFun = suspend (service: HomePageService, page: Int) -> Response<out HomePageResponse>
sealed class MediaTabViewModel(service: HomePageService, mediaFetchFun: MediaFetchFun): ViewModel() {
val mediaItems: Flow<PagingData<TmdbItem>> = Pager(PagingConfig(pageSize = Int.MAX_VALUE)) {
HomePagePagingSource(service = service, mediaFetch = mediaFetchFun)
}.flow.cachedIn(viewModelScope)
object PopularMoviesVM: MediaTabViewModel(MoviesService(), { s, p -> s.getPopular(p) })
object TopRatedMoviesVM: MediaTabViewModel(MoviesService(), { s, p -> s.getTopRated(p) })
object NowPlayingMoviesVM: MediaTabViewModel(MoviesService(), { s, p -> s.getNowPlaying(p) })
object UpcomingMoviesVM: MediaTabViewModel(MoviesService(), { s, p -> s.getUpcoming(p) })
object PopularTvVM: MediaTabViewModel(TvService(), { s, p -> s.getPopular(p) })
object TopRatedTvVM: MediaTabViewModel(TvService(), { s, p -> s.getTopRated(p) })
object AiringTodayTvVM: MediaTabViewModel(TvService(), { s, p -> s.getNowPlaying(p) })
object OnTheAirTvVM: MediaTabViewModel(TvService(), { s, p -> s.getUpcoming(p) })
}

View File

@@ -7,6 +7,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import androidx.paging.compose.collectAsLazyPagingItems
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.PagerState
@@ -15,10 +16,12 @@ import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.HomePageService
import com.owenlejeune.tvtime.api.tmdb.api.v3.MoviesService
import com.owenlejeune.tvtime.api.tmdb.api.v3.TvService
import com.owenlejeune.tvtime.ui.components.PagingPosterGrid
import com.owenlejeune.tvtime.ui.components.PosterGrid
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
import com.owenlejeune.tvtime.ui.navigation.MediaFetchFun
import com.owenlejeune.tvtime.ui.navigation.MediaTabNavItem
import com.owenlejeune.tvtime.ui.navigation.MediaTabViewModel
import com.owenlejeune.tvtime.ui.screens.main.tabs.top.Tabs
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -56,23 +59,16 @@ fun MediaTab(
}
@Composable
fun MediaTabContent(appNavController: NavHostController, mediaType: MediaViewType, mediaFetchFun: MediaFetchFun) {
val service: HomePageService = when(mediaType) {
MediaViewType.MOVIE -> MoviesService()
MediaViewType.TV -> TvService()
fun MediaTabContent(appNavController: NavHostController, mediaType: MediaViewType, mediaTabItem: MediaTabNavItem) {
val viewModel: MediaTabViewModel? = when(mediaType) {
MediaViewType.MOVIE -> mediaTabItem.movieViewModel
MediaViewType.TV -> mediaTabItem.tvViewModel
else -> throw IllegalArgumentException("Media type given: ${mediaType}, \n expected one of MediaViewType.MOVIE, MediaViewType.TV") // shouldn't happen
}
PosterGrid(
fetchMedia = { mediaList ->
CoroutineScope(Dispatchers.IO).launch {
val response = mediaFetchFun.invoke(service, 1)
if (response.isSuccessful) {
withContext(Dispatchers.Main) {
mediaList.value = response.body()?.results ?: emptyList()
}
}
}
},
val mediaListItems = viewModel?.mediaItems?.collectAsLazyPagingItems()
PagingPosterGrid(
lazyPagingItems = mediaListItems,
onClick = { id ->
appNavController.navigate(
"${MainNavItem.DetailView.route}/${mediaType}/${id}"
@@ -90,7 +86,7 @@ fun MediaTabs(
appNavController: NavHostController = rememberNavController()
) {
HorizontalPager(count = tabs.size, state = pagerState) { page ->
tabs[page].screen(appNavController, mediaViewType, tabs[page].mediaFetchFun)
tabs[page].screen(appNavController, mediaViewType, tabs[page])
}
}
@@ -101,8 +97,4 @@ fun MediaTabsPreview() {
val tabs = MediaTabNavItem.MovieItems
val pagerState = rememberPagerState()
MediaTabs(tabs = tabs, pagerState = pagerState, MediaViewType.MOVIE)
}
// val moviesViewModel = viewModel(PopularMovieViewModel::class.java)
// val moviesList = moviesViewModel.moviePage
// val movieListItems: LazyPagingItems<PopularMovie> = moviesList.collectAsLazyPagingItems()
}