mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-08 04:32:43 -05:00
add pull to refresh to home screen
This commit is contained in:
@@ -2,6 +2,8 @@ package com.owenlejeune.tvtime.api.tmdb.api
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
@@ -19,19 +21,22 @@ import retrofit2.Response
|
||||
|
||||
fun <T: Any, S> ViewModel.createPagingFlow(
|
||||
fetcher: suspend (Int) -> Response<S>,
|
||||
processor: (S) -> List<T>
|
||||
processor: (S) -> List<T>,
|
||||
refreshState: MutableState<Boolean> = mutableStateOf(false)
|
||||
): Flow<PagingData<T>> {
|
||||
return Pager(PagingConfig(pageSize = ViewModelConstants.PAGING_SIZE)) {
|
||||
BasePagingSource(
|
||||
fetcher = fetcher,
|
||||
processor = processor
|
||||
processor = processor,
|
||||
refreshState = refreshState
|
||||
)
|
||||
}.flow.cachedIn(viewModelScope)
|
||||
}
|
||||
|
||||
class BasePagingSource<T: Any, S>(
|
||||
private val fetcher: suspend (Int) -> Response<S>,
|
||||
private val processor: (S) -> List<T>
|
||||
private val processor: (S) -> List<T>,
|
||||
private val refreshState: MutableState<Boolean>
|
||||
): PagingSource<Int, T>(), KoinComponent {
|
||||
|
||||
private val context: Context by inject()
|
||||
@@ -43,16 +48,21 @@ class BasePagingSource<T: Any, S>(
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> {
|
||||
return try {
|
||||
val page = params.key ?: 1
|
||||
if (page == 1) {
|
||||
refreshState.value = true
|
||||
}
|
||||
val response = fetcher(page)
|
||||
if (response.isSuccessful) {
|
||||
val responseBody = response.body()
|
||||
val results = responseBody?.let(processor) ?: emptyList()
|
||||
refreshState.value = false
|
||||
LoadResult.Page(
|
||||
data = results,
|
||||
prevKey = if (page == 1) { null } else { page - 1},
|
||||
nextKey = if (results.isEmpty()) { null } else { page + 1}
|
||||
)
|
||||
} else {
|
||||
refreshState.value = false
|
||||
// Toast.makeText(context, context.getString(R.string.no_result_found), Toast.LENGTH_SHORT).show()
|
||||
LoadResult.Invalid()
|
||||
}
|
||||
|
||||
@@ -63,6 +63,12 @@ class MoviesService: KoinComponent, DetailService, HomePageService {
|
||||
val releaseDatesLoadingState = mutableStateOf(LoadingState.INACTIVE)
|
||||
val accountStatesLoadingState = mutableStateOf(LoadingState.INACTIVE)
|
||||
|
||||
val isPopularMoviesLoading = mutableStateOf(false)
|
||||
val isTopRatedMoviesLoading = mutableStateOf(false)
|
||||
val isNowPlayingMoviesLoading = mutableStateOf(false)
|
||||
val isUpcomingMoviesLoading = mutableStateOf(false)
|
||||
val isTrendingMoviesLoading = mutableStateOf(false)
|
||||
|
||||
override suspend fun getById(id: Int, refreshing: Boolean) {
|
||||
loadRemoteData(
|
||||
{ movieService.getMovieById(id) },
|
||||
|
||||
@@ -35,6 +35,10 @@ class PeopleService: KoinComponent {
|
||||
val imagesLoadingState = mutableStateOf(LoadingState.INACTIVE)
|
||||
val externalIdsLoadingState = mutableStateOf(LoadingState.INACTIVE)
|
||||
|
||||
val isPopularPeopleLoading = mutableStateOf(false)
|
||||
val isTrendingPeopleLoading = mutableStateOf(false
|
||||
)
|
||||
|
||||
suspend fun getPerson(id: Int, refreshing: Boolean) {
|
||||
loadRemoteData(
|
||||
{ service.getPerson(id) },
|
||||
|
||||
@@ -101,6 +101,12 @@ class TvService: KoinComponent, DetailService, HomePageService {
|
||||
val seasonVideosLoadingState = mutableStateOf(LoadingState.INACTIVE)
|
||||
val seasonWatchProvidersLoadingState = mutableStateOf(LoadingState.INACTIVE)
|
||||
|
||||
val isPopularTvLoading = mutableStateOf(false)
|
||||
val isTopRatedTvLoading = mutableStateOf(false)
|
||||
val isAiringTodayTvLoading = mutableStateOf(false)
|
||||
val isOnTheAirTvLoading = mutableStateOf(false)
|
||||
val isTrendingTvLoading = mutableStateOf(false)
|
||||
|
||||
override suspend fun getById(id: Int, refreshing: Boolean) {
|
||||
loadRemoteData(
|
||||
{ service.getTvShowById(id) },
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
package com.owenlejeune.tvtime.ui.screens.tabs
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -63,6 +69,7 @@ fun MediaTab(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun MediaTabContent(
|
||||
appNavController: NavHostController,
|
||||
@@ -71,17 +78,34 @@ fun MediaTabContent(
|
||||
) {
|
||||
val viewModel = viewModel<MainViewModel>()
|
||||
val mediaListItems = viewModel.produceMediaTabFlowFor(mediaType, mediaTabItem.type).collectAsLazyPagingItems()
|
||||
val isRefreshing = remember { viewModel.produceMediaTabLoadingFor(mediaType, mediaTabItem.type) }
|
||||
|
||||
PagingPosterGrid(
|
||||
lazyPagingItems = mediaListItems,
|
||||
onClick = { id ->
|
||||
appNavController.navigate(
|
||||
AppNavItem.DetailView.withArgs(mediaType, id)
|
||||
)
|
||||
}
|
||||
val pullRefreshState = rememberPullRefreshState(
|
||||
refreshing = isRefreshing.value,
|
||||
onRefresh = { mediaListItems.refresh() }
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.pullRefresh(state = pullRefreshState)
|
||||
) {
|
||||
PagingPosterGrid(
|
||||
lazyPagingItems = mediaListItems,
|
||||
onClick = { id ->
|
||||
appNavController.navigate(
|
||||
AppNavItem.DetailView.withArgs(mediaType, id)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
PullRefreshIndicator(
|
||||
refreshing = isRefreshing.value,
|
||||
state = pullRefreshState,
|
||||
modifier = Modifier.align(alignment = Alignment.TopCenter)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun MediaTabTrendingContent(
|
||||
appNavController: NavHostController,
|
||||
@@ -92,6 +116,7 @@ fun MediaTabTrendingContent(
|
||||
|
||||
val timeWindow = remember { mutableStateOf(TimeWindow.DAY) }
|
||||
val flow = remember { mutableStateOf(viewModel.produceTrendingFor(mediaType, timeWindow.value)) }
|
||||
val isRefreshing = remember { viewModel.produceTrendingLoadingFor(mediaType) }
|
||||
|
||||
LaunchedEffect(timeWindow.value) {
|
||||
flow.value = viewModel.produceTrendingFor(mediaType, timeWindow.value)
|
||||
@@ -99,33 +124,50 @@ fun MediaTabTrendingContent(
|
||||
|
||||
val mediaListItems = flow.value.collectAsLazyPagingItems()
|
||||
|
||||
PagingPosterGrid(
|
||||
lazyPagingItems = mediaListItems,
|
||||
headerContent = {
|
||||
val options = listOf(TimeWindow.DAY, TimeWindow.WEEK)
|
||||
|
||||
val context = LocalContext.current
|
||||
PillSegmentedControl(
|
||||
items = options,
|
||||
itemLabel = { _, i ->
|
||||
when (i) {
|
||||
TimeWindow.DAY -> context.getString(R.string.time_window_day)
|
||||
TimeWindow.WEEK -> context.getString(R.string.time_window_week)
|
||||
}
|
||||
},
|
||||
onItemSelected = { _, i -> timeWindow.value = i },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 4.dp)
|
||||
.padding(bottom = 4.dp)
|
||||
)
|
||||
},
|
||||
onClick = { id ->
|
||||
appNavController.navigate(
|
||||
AppNavItem.DetailView.withArgs(mediaType, id)
|
||||
)
|
||||
val pullRefreshState = rememberPullRefreshState(
|
||||
refreshing = isRefreshing.value,
|
||||
onRefresh = {
|
||||
mediaListItems.refresh()
|
||||
}
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.pullRefresh(pullRefreshState)
|
||||
) {
|
||||
PagingPosterGrid(
|
||||
lazyPagingItems = mediaListItems,
|
||||
headerContent = {
|
||||
val options = listOf(TimeWindow.DAY, TimeWindow.WEEK)
|
||||
|
||||
val context = LocalContext.current
|
||||
PillSegmentedControl(
|
||||
items = options,
|
||||
itemLabel = { _, i ->
|
||||
when (i) {
|
||||
TimeWindow.DAY -> context.getString(R.string.time_window_day)
|
||||
TimeWindow.WEEK -> context.getString(R.string.time_window_week)
|
||||
}
|
||||
},
|
||||
onItemSelected = { _, i -> timeWindow.value = i },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 4.dp)
|
||||
.padding(bottom = 4.dp)
|
||||
)
|
||||
},
|
||||
onClick = { id ->
|
||||
appNavController.navigate(
|
||||
AppNavItem.DetailView.withArgs(mediaType, id)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
PullRefreshIndicator(
|
||||
refreshing = isRefreshing.value,
|
||||
state = pullRefreshState,
|
||||
modifier = Modifier.align(alignment = Alignment.TopCenter)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.owenlejeune.tvtime.ui.viewmodel
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.paging.PagingData
|
||||
@@ -28,6 +29,7 @@ import com.owenlejeune.tvtime.utils.types.MediaViewType
|
||||
import com.owenlejeune.tvtime.utils.types.TimeWindow
|
||||
import com.owenlejeune.tvtime.utils.types.ViewableMediaTypeException
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.cancellable
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
@@ -62,28 +64,38 @@ class MainViewModel: ViewModel(), KoinComponent {
|
||||
val movieReleaseDatesLoadingState = movieService.releaseDatesLoadingState
|
||||
val movieAccountStatesLoadingState = movieService.accountStatesLoadingState
|
||||
|
||||
val isPopularMoviesLoading = movieService.isPopularMoviesLoading
|
||||
val isTopRatedMoviesLoading = movieService.isTopRatedMoviesLoading
|
||||
val isNowPlayingMoviesLoading = movieService.isNowPlayingMoviesLoading
|
||||
val isUpcomingMoviesLoading = movieService.isUpcomingMoviesLoading
|
||||
val isTrendingMoviesLoading = movieService.isTrendingMoviesLoading
|
||||
|
||||
val popularMovies by lazy {
|
||||
createPagingFlow(
|
||||
fetcher = { p -> movieService.getPopular(p) },
|
||||
processor = { it.results }
|
||||
processor = { it.results },
|
||||
refreshState = isPopularMoviesLoading
|
||||
)
|
||||
}
|
||||
val topRatedMovies by lazy {
|
||||
createPagingFlow(
|
||||
fetcher = { p -> movieService.getTopRated(p) },
|
||||
processor = { it.results }
|
||||
processor = { it.results },
|
||||
refreshState = isTopRatedMoviesLoading
|
||||
)
|
||||
}
|
||||
val nowPlayingMovies by lazy {
|
||||
createPagingFlow(
|
||||
fetcher = { p -> movieService.getNowPlaying(p) },
|
||||
processor = { it.results }
|
||||
processor = { it.results },
|
||||
refreshState = isNowPlayingMoviesLoading
|
||||
)
|
||||
}
|
||||
val upcomingMovies by lazy {
|
||||
createPagingFlow(
|
||||
fetcher = { p -> movieService.getUpcoming(p) },
|
||||
processor = { it.results }
|
||||
processor = { it.results },
|
||||
refreshState = isUpcomingMoviesLoading
|
||||
)
|
||||
}
|
||||
|
||||
@@ -125,28 +137,38 @@ class MainViewModel: ViewModel(), KoinComponent {
|
||||
val tvSeasonVideosLoadingState = tvService.seasonVideosLoadingState
|
||||
val tvSeasonWatchProvidersLoadingState = tvService.seasonWatchProvidersLoadingState
|
||||
|
||||
val isPopularTvLoading = tvService.isPopularTvLoading
|
||||
val isTopRatedTvLoading = tvService.isTopRatedTvLoading
|
||||
val isAiringTodayTvLoading = tvService.isAiringTodayTvLoading
|
||||
val isOnTheAirTvLoading = tvService.isOnTheAirTvLoading
|
||||
val isTrendingTvLoading = tvService.isTrendingTvLoading
|
||||
|
||||
val popularTv by lazy {
|
||||
createPagingFlow(
|
||||
fetcher = { p -> tvService.getPopular(p) },
|
||||
processor = { it.results }
|
||||
processor = { it.results },
|
||||
isPopularTvLoading
|
||||
)
|
||||
}
|
||||
val topRatedTv by lazy{
|
||||
createPagingFlow(
|
||||
fetcher = { p -> tvService.getTopRated(p) },
|
||||
processor = { it.results }
|
||||
processor = { it.results },
|
||||
isTopRatedTvLoading
|
||||
)
|
||||
}
|
||||
val airingTodayTv by lazy {
|
||||
createPagingFlow(
|
||||
fetcher = { p -> tvService.getNowPlaying(p) },
|
||||
processor = { it.results }
|
||||
processor = { it.results },
|
||||
refreshState = isAiringTodayTvLoading
|
||||
)
|
||||
}
|
||||
val onTheAirTv by lazy {
|
||||
createPagingFlow(
|
||||
fetcher = { p -> tvService.getUpcoming(p) },
|
||||
processor = { it.results }
|
||||
processor = { it.results },
|
||||
refreshState = isOnTheAirTvLoading
|
||||
)
|
||||
}
|
||||
|
||||
@@ -161,10 +183,14 @@ class MainViewModel: ViewModel(), KoinComponent {
|
||||
val peopleImagesLoadingState = peopleService.imagesLoadingState
|
||||
val peopleExternalIdsLoadingState = peopleService.externalIdsLoadingState
|
||||
|
||||
val isPopularPeopleLoading = peopleService.isPopularPeopleLoading
|
||||
val isTrendingPeopleLoading = peopleService.isTrendingPeopleLoading
|
||||
|
||||
val popularPeople by lazy {
|
||||
createPagingFlow(
|
||||
fetcher = { p -> peopleService.getPopular(p) },
|
||||
processor = { it.results }
|
||||
processor = { it.results },
|
||||
refreshState = isPopularPeopleLoading
|
||||
)
|
||||
}
|
||||
|
||||
@@ -249,6 +275,30 @@ class MainViewModel: ViewModel(), KoinComponent {
|
||||
)
|
||||
}
|
||||
|
||||
fun produceMediaTabLoadingFor(mediaType: MediaViewType, contentType: MediaTabNavItem.Type): MutableState<Boolean> {
|
||||
return providesForType(
|
||||
mediaType,
|
||||
{
|
||||
when (contentType) {
|
||||
MediaTabNavItem.Type.UPCOMING -> isUpcomingMoviesLoading
|
||||
MediaTabNavItem.Type.TOP_RATED -> isTopRatedMoviesLoading
|
||||
MediaTabNavItem.Type.NOW_PLAYING -> isNowPlayingMoviesLoading
|
||||
MediaTabNavItem.Type.POPULAR -> isPopularMoviesLoading
|
||||
else -> throw Exception("Can't produce media flow for $mediaType")
|
||||
}
|
||||
},
|
||||
{
|
||||
when (contentType) {
|
||||
MediaTabNavItem.Type.UPCOMING -> isOnTheAirTvLoading
|
||||
MediaTabNavItem.Type.TOP_RATED -> isTopRatedTvLoading
|
||||
MediaTabNavItem.Type.NOW_PLAYING -> isAiringTodayTvLoading
|
||||
MediaTabNavItem.Type.POPULAR -> isPopularTvLoading
|
||||
else -> throw Exception("Can't produce media flow for $mediaType")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun produceKeywordsResultsFor(mediaType: MediaViewType): Map<Int, Flow<PagingData<SearchResultMedia>>> {
|
||||
return providesForType(mediaType, { movieKeywordResults }, { tvKeywordResults })
|
||||
}
|
||||
@@ -258,25 +308,37 @@ class MainViewModel: ViewModel(), KoinComponent {
|
||||
MediaViewType.MOVIE -> {
|
||||
createPagingFlow(
|
||||
fetcher = { p -> movieService.getTrending(timeWindow, p) },
|
||||
processor = { it.results }
|
||||
processor = { it.results },
|
||||
refreshState = isTrendingMoviesLoading
|
||||
)
|
||||
}
|
||||
MediaViewType.TV -> {
|
||||
createPagingFlow(
|
||||
fetcher = { p -> tvService.getTrending(timeWindow, p) },
|
||||
processor = { it.results }
|
||||
processor = { it.results },
|
||||
refreshState = isTrendingTvLoading
|
||||
)
|
||||
}
|
||||
MediaViewType.PERSON -> {
|
||||
createPagingFlow(
|
||||
fetcher = { p -> peopleService.getTrending(timeWindow, p) },
|
||||
processor = { it.results }
|
||||
processor = { it.results },
|
||||
refreshState = isTrendingPeopleLoading
|
||||
)
|
||||
}
|
||||
else -> throw ViewableMediaTypeException(mediaType)
|
||||
}
|
||||
}
|
||||
|
||||
fun produceTrendingLoadingFor(mediaType: MediaViewType): MutableState<Boolean> {
|
||||
return when (mediaType) {
|
||||
MediaViewType.MOVIE -> isTrendingMoviesLoading
|
||||
MediaViewType.TV -> isTrendingTvLoading
|
||||
MediaViewType.PERSON -> isTrendingPeopleLoading
|
||||
else -> throw ViewableMediaTypeException(mediaType)
|
||||
}
|
||||
}
|
||||
|
||||
fun produceDetailsLoadingStateFor(mediaType: MediaViewType): MutableState<LoadingState> {
|
||||
return providesForType(mediaType, { movieDetailsLoadingState }, { tvDetailsLoadingState})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user