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) 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( HorizontalPagerIndicator(

View File

@@ -1,12 +1,22 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import android.content.Context
import android.widget.Toast
import androidx.paging.PagingSource import androidx.paging.PagingSource
import androidx.paging.PagingState import androidx.paging.PagingState
import com.owenlejeune.tvtime.api.tmdb.api.v3.HomePageService import com.owenlejeune.tvtime.api.tmdb.api.v3.HomePageService
import com.owenlejeune.tvtime.ui.navigation.MediaFetchFun 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 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? { override fun getRefreshKey(state: PagingState<Int, TmdbItem>): Int? {
return state.anchorPosition 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 nextKey = if (results.isEmpty() || responseBody == null) null else responseBody.page + 1
) )
} else { } else {
Toast.makeText(context, "No more results found", Toast.LENGTH_SHORT).show()
LoadResult.Invalid() LoadResult.Invalid()
} }
} catch (e: Exception) { } 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.isSystemInDarkTheme
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope 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.LazyGridItemScope
import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.runtime.Composable 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( fun <T: Any> LazyListScope.listItems(
items: Collection<T>, items: Collection<T>,
itemContent: @Composable (value: T) -> Unit itemContent: @Composable (value: T) -> Unit

View File

@@ -129,7 +129,7 @@ class AppPreferences(context: Context) {
get() = preferences.getBoolean(FIRST_LAUNCH, true) get() = preferences.getBoolean(FIRST_LAUNCH, true)
set(value) { preferences.put(FIRST_LAUNCH, value) } set(value) { preferences.put(FIRST_LAUNCH, value) }
val useV4ApiDefault: Boolean = false val useV4ApiDefault: Boolean = true
var useV4Api: Boolean var useV4Api: Boolean
get() = preferences.getBoolean(USE_V4_API, useV4ApiDefault) get() = preferences.getBoolean(USE_V4_API, useV4ApiDefault)
set(value) { preferences.put(USE_V4_API, value) } 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.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.header
import com.owenlejeune.tvtime.extensions.lazyPagingItems 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
@@ -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) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun PeoplePosterGrid( fun PeoplePosterGrid(
@@ -187,13 +228,14 @@ fun PosterItem(
Card( Card(
elevation = elevation, elevation = elevation,
modifier = modifier modifier = modifier
.size(width = width, height = height), .width(width = width)
.wrapContentHeight(),
shape = RoundedCornerShape(5.dp) shape = RoundedCornerShape(5.dp)
) { ) {
if (url != null) { if (url != null) {
AsyncImage( AsyncImage(
modifier = Modifier modifier = Modifier
.size(width = width, height = height) .width(width = width)
.clip(RoundedCornerShape(5.dp)) .clip(RoundedCornerShape(5.dp))
.clickable( .clickable(
onClick = onClick onClick = onClick
@@ -201,7 +243,7 @@ fun PosterItem(
model = url, model = url,
placeholder = rememberAsyncImagePainter(model = placeholder), placeholder = rememberAsyncImagePainter(model = placeholder),
contentDescription = contentDescription, contentDescription = contentDescription,
contentScale = ContentScale.FillBounds contentScale = ContentScale.FillWidth
) )
} else { } else {
Image( Image(

View File

@@ -1,24 +1,14 @@
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.MediaTabContent 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 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
@@ -95,19 +85,3 @@ private val screenContent: MediaNavComposableFun = { appNavController, mediaView
typealias MediaNavComposableFun = @Composable (NavHostController, MediaViewType, MediaTabNavItem) -> 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

@@ -1,14 +1,9 @@
package com.owenlejeune.tvtime.ui.screens.main package com.owenlejeune.tvtime.ui.screens.main
import androidx.compose.foundation.layout.Column 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.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
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 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.PagerState
import com.google.accompanist.pager.rememberPagerState import com.google.accompanist.pager.rememberPagerState
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.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.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.components.SearchView
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.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 com.owenlejeune.tvtime.ui.viewmodel.MediaTabViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.java.KoinJavaComponent.get
@OptIn(ExperimentalPagerApi::class) @OptIn(ExperimentalPagerApi::class)
@Composable @Composable

View File

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