paging for home page people

This commit is contained in:
Owen LeJeune
2022-11-05 15:52:22 -04:00
parent 216457c148
commit 4a90e7c4e1
12 changed files with 191 additions and 71 deletions

View File

@@ -108,7 +108,11 @@ class OnboardingActivity: MonetCompatActivity() {
launchActivity(MainActivity::class.java)
}
) {
Text(text = "Skip")
val skipText = if (preferences.firstLaunchTesting)
stringResource(id = R.string.action_skip_testing)
else
stringResource(id = R.string.action_skip)
Text(text = skipText)
}
HorizontalPagerIndicator(

View File

@@ -1,12 +1,22 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import android.content.Context
import android.widget.Toast
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 org.koin.core.Koin
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import retrofit2.Response
class HomePagePagingSource(private val service: HomePageService, private val mediaFetch: MediaFetchFun): PagingSource<Int, TmdbItem>() {
class HomePagePagingSource(
private val service: HomePageService,
private val mediaFetch: MediaFetchFun
): PagingSource<Int, TmdbItem>(), KoinComponent {
private val context: Context by inject()
override fun getRefreshKey(state: PagingState<Int, TmdbItem>): Int? {
return state.anchorPosition
@@ -25,6 +35,7 @@ class HomePagePagingSource(private val service: HomePageService, private val med
nextKey = if (results.isEmpty() || responseBody == null) null else responseBody.page + 1
)
} else {
Toast.makeText(context, "No more results found", Toast.LENGTH_SHORT).show()
LoadResult.Invalid()
}
} catch (e: Exception) {

View File

@@ -0,0 +1,43 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.owenlejeune.tvtime.api.tmdb.api.v3.PeopleApi
import com.owenlejeune.tvtime.api.tmdb.api.v3.PeopleService
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class HomePagePeoplePagingSource: PagingSource<Int, HomePagePerson>(), KoinComponent {
private val service: PeopleApi by inject()
private val context: Context by inject()
override fun getRefreshKey(state: PagingState<Int, HomePagePerson>): Int? {
return state.anchorPosition
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, HomePagePerson> {
return try {
val nextPage = params.key ?: 1
val peopleResponse = service.getPopular(page = nextPage)
if (peopleResponse.isSuccessful) {
val responseBody = peopleResponse.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 {
Toast.makeText(context, "No more results found", Toast.LENGTH_SHORT).show()
LoadResult.Invalid()
}
} catch (e: Exception) {
return LoadResult.Error(e)
}
}
}

View File

@@ -3,6 +3,7 @@ package com.owenlejeune.tvtime.extensions
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyGridItemScope
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.runtime.Composable
@@ -27,6 +28,17 @@ fun <T: Any> LazyGridScope.listItems(
}
}
fun LazyGridScope.header(
content: @Composable LazyGridItemScope.() -> Unit
) {
item(
span = {
GridItemSpan(maxLineSpan)
},
content = content
)
}
fun <T: Any> LazyListScope.listItems(
items: Collection<T>,
itemContent: @Composable (value: T) -> Unit

View File

@@ -129,7 +129,7 @@ class AppPreferences(context: Context) {
get() = preferences.getBoolean(FIRST_LAUNCH, true)
set(value) { preferences.put(FIRST_LAUNCH, value) }
val useV4ApiDefault: Boolean = false
val useV4ApiDefault: Boolean = true
var useV4Api: Boolean
get() = preferences.getBoolean(USE_V4_API, useV4ApiDefault)
set(value) { preferences.put(USE_V4_API, value) }

View File

@@ -35,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.header
import com.owenlejeune.tvtime.extensions.lazyPagingItems
import com.owenlejeune.tvtime.extensions.listItems
import com.owenlejeune.tvtime.utils.TmdbUtils
@@ -96,6 +97,46 @@ fun PagingPosterGrid(
}
}
@Composable
fun PagingPeoplePosterGrid(
lazyPagingItems: LazyPagingItems<HomePagePerson>?,
header: @Composable () -> Unit = {},
onClick: (id: Int) -> Unit = {}
) {
lazyPagingItems?.let {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = POSTER_WIDTH),
contentPadding = PaddingValues(8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
header {
header()
}
lazyPagingItems(lazyPagingItems) { person ->
person?.let {
PosterItem(
url = TmdbUtils.getFullPersonImagePath(person.profilePath),
noDataImage = R.drawable.no_person_photo,
modifier = Modifier.padding(5.dp),
onClick = {
onClick(person.id)
},
contentDescription = person.name
)
}
}
lazyPagingItems.apply {
when {
loadState.refresh is LoadState.Loading -> {}
loadState.append is LoadState.Loading -> {}
loadState.append is LoadState.Error -> {}
}
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PeoplePosterGrid(
@@ -187,13 +228,14 @@ fun PosterItem(
Card(
elevation = elevation,
modifier = modifier
.size(width = width, height = height),
.width(width = width)
.wrapContentHeight(),
shape = RoundedCornerShape(5.dp)
) {
if (url != null) {
AsyncImage(
modifier = Modifier
.size(width = width, height = height)
.width(width = width)
.clip(RoundedCornerShape(5.dp))
.clickable(
onClick = onClick
@@ -201,7 +243,7 @@ fun PosterItem(
model = url,
placeholder = rememberAsyncImagePainter(model = placeholder),
contentDescription = contentDescription,
contentScale = ContentScale.FillBounds
contentScale = ContentScale.FillWidth
)
} else {
Image(

View File

@@ -1,24 +1,14 @@
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.ui.screens.main.MediaViewType
import com.owenlejeune.tvtime.ui.viewmodel.MediaTabViewModel
import com.owenlejeune.tvtime.utils.ResourceUtils
import kotlinx.coroutines.flow.Flow
import org.koin.core.component.inject
import retrofit2.Response
@@ -94,20 +84,4 @@ private val screenContent: MediaNavComposableFun = { appNavController, mediaView
typealias MediaNavComposableFun = @Composable (NavHostController, MediaViewType, MediaTabNavItem) -> Unit
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) })
}
typealias MediaFetchFun = suspend (service: HomePageService, page: Int) -> Response<out HomePageResponse>

View File

@@ -1,14 +1,9 @@
package com.owenlejeune.tvtime.ui.screens.main
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
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
@@ -17,24 +12,12 @@ import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.PagerState
import com.google.accompanist.pager.rememberPagerState
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.preferences.AppPreferences
import com.owenlejeune.tvtime.ui.components.PagingPosterGrid
import com.owenlejeune.tvtime.ui.components.PosterGrid
import com.owenlejeune.tvtime.ui.components.SearchBar
import com.owenlejeune.tvtime.ui.components.SearchView
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
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.java.KoinJavaComponent.get
import com.owenlejeune.tvtime.ui.viewmodel.MediaTabViewModel
@OptIn(ExperimentalPagerApi::class)
@Composable

View File

@@ -1,19 +1,20 @@
package com.owenlejeune.tvtime.ui.screens.main
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.PeopleService
import com.owenlejeune.tvtime.ui.components.PeoplePosterGrid
import com.owenlejeune.tvtime.ui.components.PagingPeoplePosterGrid
import com.owenlejeune.tvtime.ui.components.SearchView
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import com.owenlejeune.tvtime.ui.viewmodel.PeopleTabViewModel
@Composable
fun PeopleTab(
@@ -23,8 +24,6 @@ fun PeopleTab(
) {
appBarTitle.value = stringResource(id = R.string.nav_people_title)
val service = PeopleService()
Column {
SearchView(
title = appBarTitle.value,
@@ -33,16 +32,15 @@ fun PeopleTab(
fab = fab
)
PeoplePosterGrid(
fetchPeople = { peopleList ->
CoroutineScope(Dispatchers.IO).launch {
val response = service.getPopular()
if (response.isSuccessful) {
withContext(Dispatchers.Main) {
peopleList.value = response.body()?.results ?: emptyList()
}
}
}
val peopleList = PeopleTabViewModel().popularPeople.collectAsLazyPagingItems()
PagingPeoplePosterGrid(
lazyPagingItems = peopleList,
header = {
Text(
text = stringResource(R.string.popular_today_header),
modifier = Modifier.padding(start = 8.dp)
)
},
onClick = { id ->
appNavController.navigate(

View File

@@ -0,0 +1,31 @@
package com.owenlejeune.tvtime.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
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.TmdbItem
import com.owenlejeune.tvtime.ui.navigation.MediaFetchFun
import kotlinx.coroutines.flow.Flow
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

@@ -0,0 +1,19 @@
package com.owenlejeune.tvtime.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.HomePagePeoplePagingSource
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.HomePagePerson
import kotlinx.coroutines.flow.Flow
class PeopleTabViewModel: ViewModel() {
val popularPeople: Flow<PagingData<HomePagePerson>> = Pager(PagingConfig(pageSize = Int.MAX_VALUE)) {
HomePagePeoplePagingSource()
}.flow.cachedIn(viewModelScope)
}

View File

@@ -148,4 +148,7 @@
<!-- search results -->
<string name="search_result_tv_series">TV Series</string>
<string name="no_search_results">No search results found</string>
<string name="action_skip">Skip</string>
<string name="action_skip_testing">Skip (testing)</string>
<string name="popular_today_header">Popular Today</string>
</resources>