mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-12-30 19:31:19 -05:00
add paging to home screen tabs
This commit is contained in:
@@ -7,12 +7,12 @@ plugins {
|
|||||||
def composeVersion = "1.2.0-beta03"
|
def composeVersion = "1.2.0-beta03"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 32
|
compileSdkVersion 33
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.owenlejeune.tvtime"
|
applicationId "com.owenlejeune.tvtime"
|
||||||
minSdkVersion 23
|
minSdkVersion 23
|
||||||
targetSdkVersion 32
|
targetSdkVersion 33
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ dependencies {
|
|||||||
def compose_material3 = "1.0.0-alpha13"
|
def compose_material3 = "1.0.0-alpha13"
|
||||||
def compose_accompanist = "0.24.10-beta"
|
def compose_accompanist = "0.24.10-beta"
|
||||||
def compose_navigation = "2.4.2"
|
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_constraint_layout = "1.0.1"
|
||||||
def compose_activity = "1.4.0"
|
def compose_activity = "1.4.0"
|
||||||
implementation "androidx.compose.ui:ui:$compose"
|
implementation "androidx.compose.ui:ui:$compose"
|
||||||
@@ -89,6 +89,7 @@ dependencies {
|
|||||||
implementation "androidx.navigation:navigation-compose:$compose_navigation"
|
implementation "androidx.navigation:navigation-compose:$compose_navigation"
|
||||||
implementation "androidx.paging:paging-compose:$compose_paging"
|
implementation "androidx.paging:paging-compose:$compose_paging"
|
||||||
implementation "androidx.constraintlayout:constraintlayout-compose:$compose_constraint_layout"
|
implementation "androidx.constraintlayout:constraintlayout-compose:$compose_constraint_layout"
|
||||||
|
implementation "androidx.paging:paging-compose:$compose_paging"
|
||||||
|
|
||||||
// material you
|
// material you
|
||||||
def monet_compat = "0.4.1"
|
def monet_compat = "0.4.1"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.paging.LoadState
|
||||||
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
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.Person
|
||||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TmdbItem
|
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TmdbItem
|
||||||
import com.owenlejeune.tvtime.extensions.dpToPx
|
import com.owenlejeune.tvtime.extensions.dpToPx
|
||||||
|
import com.owenlejeune.tvtime.extensions.lazyPagingItems
|
||||||
import com.owenlejeune.tvtime.extensions.listItems
|
import com.owenlejeune.tvtime.extensions.listItems
|
||||||
import com.owenlejeune.tvtime.utils.TmdbUtils
|
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)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PeoplePosterGrid(
|
fun PeoplePosterGrid(
|
||||||
|
|||||||
@@ -1,24 +1,41 @@
|
|||||||
package com.owenlejeune.tvtime.ui.navigation
|
package com.owenlejeune.tvtime.ui.navigation
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.navigation.NavHostController
|
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.R
|
||||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.HomePageService
|
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.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.MediaViewType
|
||||||
import com.owenlejeune.tvtime.ui.screens.main.MediaTabContent
|
import com.owenlejeune.tvtime.ui.screens.main.MediaTabContent
|
||||||
import com.owenlejeune.tvtime.utils.ResourceUtils
|
import com.owenlejeune.tvtime.utils.ResourceUtils
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import retrofit2.Response
|
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()
|
private val resourceUtils: ResourceUtils by inject()
|
||||||
|
|
||||||
override val name = resourceUtils.getString(stringRes)
|
override val name = resourceUtils.getString(stringRes)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val MovieItems = listOf(Popular, TopRated, NowPlaying, Upcoming)
|
val MovieItems = listOf(Popular, NowPlaying, Upcoming, TopRated)
|
||||||
val TvItems = listOf(Popular, TopRated, AiringToday, OnTheAir)
|
val TvItems = listOf(Popular, AiringToday, OnTheAir, TopRated)
|
||||||
|
|
||||||
private val Items = listOf(NowPlaying, Popular, TopRated, Upcoming, AiringToday, OnTheAir)
|
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 Popular: MediaTabNavItem(
|
||||||
object TopRated: MediaTabNavItem(R.string.nav_top_rated_title, "top_rated_route", screenContent, { s, p -> s.getTopRated(p) } )
|
stringRes = R.string.nav_popular_title,
|
||||||
object NowPlaying: MediaTabNavItem(R.string.nav_now_playing_title, "now_playing_route", screenContent, { s, p -> s.getNowPlaying(p) } )
|
route = "popular_route",
|
||||||
object Upcoming: MediaTabNavItem(R.string.nav_upcoming_title, "upcoming_route", screenContent, { s, p -> s.getUpcoming(p) } )
|
screen = screenContent,
|
||||||
object AiringToday: MediaTabNavItem(R.string.nav_tv_airing_today_title, "airing_today_route", screenContent, { s, p -> s.getNowPlaying(p) } )
|
movieViewModel = MediaTabViewModel.PopularMoviesVM,
|
||||||
object OnTheAir: MediaTabNavItem(R.string.nav_tv_on_the_air, "on_the_air_route", screenContent, { s, p -> s.getUpcoming(p) } )
|
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 ->
|
private val screenContent: MediaNavComposableFun = { appNavController, mediaViewType, mediaTabItem ->
|
||||||
MediaTabContent(appNavController = appNavController, mediaType = mediaViewType, mediaFetchFun = mediaFetchFun)
|
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) })
|
||||||
|
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
import com.google.accompanist.pager.HorizontalPager
|
import com.google.accompanist.pager.HorizontalPager
|
||||||
import com.google.accompanist.pager.PagerState
|
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.HomePageService
|
||||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.MoviesService
|
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.TvService
|
||||||
|
import com.owenlejeune.tvtime.ui.components.PagingPosterGrid
|
||||||
import com.owenlejeune.tvtime.ui.components.PosterGrid
|
import com.owenlejeune.tvtime.ui.components.PosterGrid
|
||||||
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
|
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
|
||||||
import com.owenlejeune.tvtime.ui.navigation.MediaFetchFun
|
import com.owenlejeune.tvtime.ui.navigation.MediaFetchFun
|
||||||
import com.owenlejeune.tvtime.ui.navigation.MediaTabNavItem
|
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 com.owenlejeune.tvtime.ui.screens.main.tabs.top.Tabs
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -56,23 +59,16 @@ fun MediaTab(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MediaTabContent(appNavController: NavHostController, mediaType: MediaViewType, mediaFetchFun: MediaFetchFun) {
|
fun MediaTabContent(appNavController: NavHostController, mediaType: MediaViewType, mediaTabItem: MediaTabNavItem) {
|
||||||
val service: HomePageService = when(mediaType) {
|
val viewModel: MediaTabViewModel? = when(mediaType) {
|
||||||
MediaViewType.MOVIE -> MoviesService()
|
MediaViewType.MOVIE -> mediaTabItem.movieViewModel
|
||||||
MediaViewType.TV -> TvService()
|
MediaViewType.TV -> mediaTabItem.tvViewModel
|
||||||
else -> throw IllegalArgumentException("Media type given: ${mediaType}, \n expected one of MediaViewType.MOVIE, MediaViewType.TV") // shouldn't happen
|
else -> throw IllegalArgumentException("Media type given: ${mediaType}, \n expected one of MediaViewType.MOVIE, MediaViewType.TV") // shouldn't happen
|
||||||
}
|
}
|
||||||
PosterGrid(
|
val mediaListItems = viewModel?.mediaItems?.collectAsLazyPagingItems()
|
||||||
fetchMedia = { mediaList ->
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
PagingPosterGrid(
|
||||||
val response = mediaFetchFun.invoke(service, 1)
|
lazyPagingItems = mediaListItems,
|
||||||
if (response.isSuccessful) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
mediaList.value = response.body()?.results ?: emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClick = { id ->
|
onClick = { id ->
|
||||||
appNavController.navigate(
|
appNavController.navigate(
|
||||||
"${MainNavItem.DetailView.route}/${mediaType}/${id}"
|
"${MainNavItem.DetailView.route}/${mediaType}/${id}"
|
||||||
@@ -90,7 +86,7 @@ fun MediaTabs(
|
|||||||
appNavController: NavHostController = rememberNavController()
|
appNavController: NavHostController = rememberNavController()
|
||||||
) {
|
) {
|
||||||
HorizontalPager(count = tabs.size, state = pagerState) { page ->
|
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 tabs = MediaTabNavItem.MovieItems
|
||||||
val pagerState = rememberPagerState()
|
val pagerState = rememberPagerState()
|
||||||
MediaTabs(tabs = tabs, pagerState = pagerState, MediaViewType.MOVIE)
|
MediaTabs(tabs = tabs, pagerState = pagerState, MediaViewType.MOVIE)
|
||||||
}
|
}
|
||||||
|
|
||||||
// val moviesViewModel = viewModel(PopularMovieViewModel::class.java)
|
|
||||||
// val moviesList = moviesViewModel.moviePage
|
|
||||||
// val movieListItems: LazyPagingItems<PopularMovie> = moviesList.collectAsLazyPagingItems()
|
|
||||||
Reference in New Issue
Block a user