From 4a90e7c4e1ae52ef3859d42f5c010cf4ec29ca1b Mon Sep 17 00:00:00 2001 From: Owen LeJeune Date: Sat, 5 Nov 2022 15:52:22 -0400 Subject: [PATCH] paging for home page people --- .../owenlejeune/tvtime/OnboardingActivity.kt | 6 ++- .../tmdb/api/v3/model/HomePagePagingSource.kt | 13 ++++- .../v3/model/HomePagePeoplePagingSource.kt | 43 +++++++++++++++++ .../tvtime/extensions/ComposeExtensions.kt | 12 +++++ .../tvtime/preferences/AppPreferences.kt | 2 +- .../tvtime/ui/components/Posters.kt | 48 +++++++++++++++++-- .../tvtime/ui/navigation/MediaTabNavItem.kt | 32 ++----------- .../tvtime/ui/screens/main/MediaTab.kt | 19 +------- .../tvtime/ui/screens/main/PeopleTab.kt | 34 +++++++------ .../tvtime/ui/viewmodel/MediaTabViewModel.kt | 31 ++++++++++++ .../tvtime/ui/viewmodel/PeopleTabViewModel.kt | 19 ++++++++ app/src/main/res/values/strings.xml | 3 ++ 12 files changed, 191 insertions(+), 71 deletions(-) create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/HomePagePeoplePagingSource.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/MediaTabViewModel.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/PeopleTabViewModel.kt diff --git a/app/src/main/java/com/owenlejeune/tvtime/OnboardingActivity.kt b/app/src/main/java/com/owenlejeune/tvtime/OnboardingActivity.kt index 571aad8..c52fb18 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/OnboardingActivity.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/OnboardingActivity.kt @@ -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( diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/HomePagePagingSource.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/HomePagePagingSource.kt index 0192dfe..efd1f5b 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/HomePagePagingSource.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/HomePagePagingSource.kt @@ -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() { +class HomePagePagingSource( + private val service: HomePageService, + private val mediaFetch: MediaFetchFun +): PagingSource(), KoinComponent { + + private val context: Context by inject() override fun getRefreshKey(state: PagingState): 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) { diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/HomePagePeoplePagingSource.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/HomePagePeoplePagingSource.kt new file mode 100644 index 0000000..2fbd2a8 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/HomePagePeoplePagingSource.kt @@ -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(), KoinComponent { + + private val service: PeopleApi by inject() + private val context: Context by inject() + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition + } + + override suspend fun load(params: LoadParams): LoadResult { + 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) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/extensions/ComposeExtensions.kt b/app/src/main/java/com/owenlejeune/tvtime/extensions/ComposeExtensions.kt index ce26313..c727a43 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/extensions/ComposeExtensions.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/extensions/ComposeExtensions.kt @@ -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 LazyGridScope.listItems( } } +fun LazyGridScope.header( + content: @Composable LazyGridItemScope.() -> Unit +) { + item( + span = { + GridItemSpan(maxLineSpan) + }, + content = content + ) +} + fun LazyListScope.listItems( items: Collection, itemContent: @Composable (value: T) -> Unit diff --git a/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt b/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt index 8e89f7c..03c7aa2 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt @@ -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) } diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Posters.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Posters.kt index 5512b35..cc4c6ac 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Posters.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Posters.kt @@ -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?, + 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( diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/MediaTabNavItem.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/MediaTabNavItem.kt index 573899f..9010379 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/MediaTabNavItem.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/MediaTabNavItem.kt @@ -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 - -sealed class MediaTabViewModel(service: HomePageService, mediaFetchFun: MediaFetchFun): ViewModel() { - val mediaItems: Flow> = 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) }) - -} \ No newline at end of file +typealias MediaFetchFun = suspend (service: HomePageService, page: Int) -> Response \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/MediaTab.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/MediaTab.kt index 7bf150e..8893722 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/MediaTab.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/MediaTab.kt @@ -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 diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/PeopleTab.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/PeopleTab.kt index 9453a83..d7da9f5 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/PeopleTab.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/PeopleTab.kt @@ -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( diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/MediaTabViewModel.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/MediaTabViewModel.kt new file mode 100644 index 0000000..61634d7 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/MediaTabViewModel.kt @@ -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> = 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) }) + +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/PeopleTabViewModel.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/PeopleTabViewModel.kt new file mode 100644 index 0000000..5b76804 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/PeopleTabViewModel.kt @@ -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> = Pager(PagingConfig(pageSize = Int.MAX_VALUE)) { + HomePagePeoplePagingSource() + }.flow.cachedIn(viewModelScope) + +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6201fb2..07863fc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -148,4 +148,7 @@ TV Series No search results found + Skip + Skip (testing) + Popular Today \ No newline at end of file