mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-15 08:12:45 -05:00
add paging to home screen tabs
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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.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(
|
||||
|
||||
@@ -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) })
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user