In my project I want to use paging 3 .
before adding paging into my project , I could get the data from server and show into my RecyclerView
but after adding paging I faced with this issue :
in my Paging Source class :
class RepoPagingSource #Inject constructor(
private val repository: ApiRepository,
val context: Context) : PagingSource<Int, RepositoryResponse>() {
private lateinit var sharedPref: SharedPref
private lateinit var data : MutableList<RepositoryResponse>
private lateinit var responseCode : String
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, RepositoryResponse> {
sharedPref = SharedPref(context)
val responseData = mutableListOf<RepositoryResponse>()
return try {
val currentPage = params.key ?: 1
val response = repository
.getRepositories("bearer ${sharedPref.accessToken}", currentPage)
.applyIoScheduler()
.subscribe { response ->
responseCode=response.code().toString()
data = response.body()!!
Log.d("RepoPagingSource",responseCode)
Log.d("RepoPagingSource",data.size.toString())
Log.d("RepoPagingSource",data.toString())
}
responseData.addAll(data)
LoadResult.Page(
data = responseData,
prevKey = if (currentPage == 1) null else -1,
nextKey = currentPage.plus(1)
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, RepositoryResponse>): Int? {
return null
}
}
these log is showed correct data :
Log.d("RepoPagingSource",responseCode)
Log.d("RepoPagingSource",data.size.toString())
Log.d("RepoPagingSource",data.toString())
result of these logs :
RepoPagingSource: 200
RepoPagingSource: 2
RepoPagingSource: [RepositoryResponse(id=5246349....
but my recyclerview is empty and i checked the code in debug mode
here :
responseData.addAll(data)
data is null!
thanks in advance for your help
I have done it like :
class RepoPagingSource #Inject constructor(
private val repository: ApiRepository,
val context: Context ) : RxPagingSource<Int, RepositoryResponse>() {
private lateinit var sharedPref: SharedPref
override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, RepositoryResponse>> {
sharedPref = SharedPref(context)
var nextPageNumber = params.key
if (nextPageNumber == null) {
nextPageNumber = 1
}
return repository.getRepositories("bearer ${sharedPref.accessToken}", nextPageNumber)
.subscribeOn(Schedulers.io())
.map { response: Response<MutableList<RepositoryResponse>> -> response.body()?.let { toLoadResult(it, nextPageNumber) } }
.onErrorReturn { LoadResult.Error(it) }
}
private fun toLoadResult(
response: MutableList<RepositoryResponse>,
position:Int
): LoadResult<Int, RepositoryResponse> {
return LoadResult.Page(
response,
null,
position + 1,
COUNT_UNDEFINED,
COUNT_UNDEFINED
)
}
override fun getRefreshKey(state: PagingState<Int, RepositoryResponse>): Int? {
return null
}}
in its work for me ,also i have changed my ver of library Rx into rxjava2
Related
I have created a remote mediator which gets movies from api call and adds it to database which is then used as a source to load the data on screen.
It is pretty cliche implementation done same as google developers video of paging3 from youtube , diffrent articles etc.
#ExperimentalPagingApi
class RemoteMediator(
val moviesRetrofitClient: MoviesRetrofitClient,
private val movieDatabase: MovieDatabase
) : RemoteMediator<Int, MovieData>() {
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, MovieData>
): MediatorResult {
try {
val pageKeyData = getKeyPageData(loadType , state)
val page = when(pageKeyData){
is MediatorResult.Success -> {
Utils.debug("mediator result success = $pageKeyData")
return pageKeyData
}
else -> {
Utils.debug("mediator result failed = $pageKeyData")
pageKeyData as Int
}
}
Utils.debug("page we got = $page")
val movieResponse = moviesRetrofitClient.getNowPlayingMovies(page)
val movies = movieResponse.movies
var totalPages = movieResponse.totalPages
val endOfPaginationReached = (page == totalPages)
movieDatabase.withTransaction {
if (loadType == LoadType.REFRESH){
movieDatabase.movieDao().deleteMovie()
movieDatabase.moviePagingKeyDao().deleteAllPagingKeys()
}
val prevPage = if (page == 1) null else (page-1)
val nextPage = if (endOfPaginationReached) null else (page+1)
val keys = movies.map {
MoviePagingKeys(it.id , prevPage = prevPage , nextPage = nextPage)
}
movieDatabase.moviePagingKeyDao().addAllPagingKeys(keys)
movieDatabase.movieDao().addMovies(movies)
}
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
}catch (e : Exception){
Utils.error("exception Error : ${e.message.toString()}")
return MediatorResult.Error(e)
}catch (ioException : IOException){
Utils.error("IO Error : ${ioException.message.toString()}")
return MediatorResult.Error(ioException)
}
}
private suspend fun getKeyPageData(loadType: LoadType, state: PagingState<Int,
MovieData>): Any {
return when(loadType){
LoadType.REFRESH -> {
Utils.debug("Refresh called")
val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
remoteKeys?.nextPage?.minus(1) ?: 1
}
LoadType.APPEND -> {
Utils.debug("Append called")
val remoteKeys = getLastRemoteKey(state)
val nextKey = remoteKeys?.nextPage
return nextKey ?: MediatorResult.Success(endOfPaginationReached = false)
}
LoadType.PREPEND -> {
Utils.debug("Prepend Called")
val remoteKeys = getFirstRemoteKey(state)
val prevKey = remoteKeys?.prevPage ?: return MediatorResult.Success(
endOfPaginationReached = false
)
prevKey
}
}
}
private suspend fun getFirstRemoteKey(state: PagingState<Int, MovieData>):
MoviePagingKeys?{
return state.pages
.firstOrNull { it.data.isNotEmpty() }
?.data?.firstOrNull()
?.let { movie -> movieDatabase.moviePagingKeyDao().getMoviePagingKey(movie.id) }
}
private suspend fun getLastRemoteKey(state: PagingState<Int, MovieData>): MoviePagingKeys?
{
return state.pages
.lastOrNull { it.data.isNotEmpty() }
?.data?.lastOrNull()
?.let { movie -> movieDatabase.moviePagingKeyDao().getMoviePagingKey(movie.id) }
}
private suspend fun getRemoteKeyClosestToCurrentPosition(state: PagingState<Int,
MovieData>): MoviePagingKeys? {
return state.anchorPosition?.let {position ->
state.closestItemToPosition(position)?.id?.let { movieId ->
movieDatabase.moviePagingKeyDao().getMoviePagingKey(movieId)
}
}
}
override suspend fun initialize(): InitializeAction {
return InitializeAction.LAUNCH_INITIAL_REFRESH
}
}
This is my api response
{
"dates": {
"maximum": "2022-09-11",
"minimum": "2022-07-25"
},
"page": 1,
"results": [
{
"adult": false,
"backdrop_path": "/2RSirqZG949GuRwN38MYCIGG4Od.jpg",
"genre_ids": [
53
],
"id": 985939,
"original_language": "en",
"original_title": "Fall",
"overview": "For best friends Becky and Hunter, life is all about conquering fears and pushing limits. But after they climb 2,000 feet to the top of a remote, abandoned radio tower, they find themselves stranded with no way down. Now Becky and Hunter’s expert climbing skills will be put to the ultimate test as they desperately fight to survive the elements, a lack of supplies, and vertigo-inducing heights.",
"popularity": 9791.409,
"poster_path": "/9f5sIJEgvUpFv0ozfA6TurG4j22.jpg",
"release_date": "2022-08-11",
"title": "Fall",
"video": false,
"vote_average": 7.5,
"vote_count": 455
},...]
"total_pages": 83,
"total_results": 1645
}
The results are the movies which needs to be displayed . Since an array of movies are already fetched during the api call , I am checking if the remote mediator is success or not by comparing the page number with the total pages.
val endOfPaginationReached = (page == totalPages)
The problem is , the load method is called continously again and again even after 1st page is fetched . Hence making it call the API continously.
I understand the data which i gave might not be enough for a solution , but I do not know how to express the problem.
I want to know how is the load method called , like on what condition. Please help
This is all the classes which is being used , I am not adding the unrelated classes like Daos and ViewModels. I am sure those does not have any problems.
Repository clas with the config :-
class MovieRepository #Inject constructor(
val moviesRetrofitClient: MoviesRetrofitClient,
val movieDatabase: MovieDatabase) {
fun getMovies() = Pager(
config = PagingConfig(pageSize = Constants.PAGE_SIZE, maxSize = Constants.MAX_PAGE_COUNT),
remoteMediator = RemoteMediator(moviesRetrofitClient , movieDatabase)){
movieDatabase.movieDao().getMovies()
}.liveData
}
Retrofit client
#InstallIn(SingletonComponent::class)
#Module
class MoviesRetrofitClient #Inject constructor() {
#Singleton
#Provides
fun getInterceptor() : Interceptor{
val requestInterceptor = Interceptor{
val url = it.request()
.url
.newBuilder()
.addQueryParameter("api_key" , API_KEY)
.build()
val request = it.request()
.newBuilder()
.url(url)
.build()
return#Interceptor it.proceed(request)
}
return requestInterceptor
}
#Singleton
#Provides
fun getGsonConverterFactory() : GsonConverterFactory{
return GsonConverterFactory.create()
}
#Singleton
#Provides
fun getOkHttpClient() : OkHttpClient{
var httLog : HttpLoggingInterceptor = HttpLoggingInterceptor()
httLog.setLevel(HttpLoggingInterceptor.Level.BODY)
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(getInterceptor()).addInterceptor(httLog)
.connectTimeout(60 , TimeUnit.SECONDS)
.build()
return okHttpClient
}
#Singleton
#Provides
fun getMoviesApiServiceRx() : MoviesApiService{
var retrofit : Retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(getOkHttpClient())
.addConverterFactory(getGsonConverterFactory())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
return retrofit.create(MoviesApiService::class.java)
}
#Singleton
#Provides
suspend fun getNowPlayingMovies(pageNo : Int): NowPlayingMoviesData {
return getMoviesApiServiceRx().getNowPlayingMovies(pageNo)
}
}
Paging Adapter
class MoviesAdapter() : PagingDataAdapter<MovieData,MoviesAdapter.MovieViewHolder>(COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val binding = MovieViewBinding.inflate(LayoutInflater.from(parent.context), parent , false)
return MovieViewHolder(context = parent.context , binding)
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
val movie = getItem(position)
if (movie != null){
holder.bindData(movie)
}
}
inner class MovieViewHolder(private val context: Context, private val movieViewDataBinding : MovieViewBinding)
: RecyclerView.ViewHolder(movieViewDataBinding.root){
init {
movieViewDataBinding.root.setOnClickListener{
// TODO: "implement movie details screen"
Utils.toast(context , "movie Clicked")
}
}
fun bindData(movieData: MovieData){
movieViewDataBinding.movie = calculateRating(movieData)
}
//change the ratings to the multiple of 5 , so that it can be fit in the rating view.
private fun calculateRating(movieData: MovieData) : MovieData{
movieData.voteAverage = (movieData.voteAverage?.times(5))?.div(10)
return movieData
}
}
companion object {
private val COMPARATOR = object : DiffUtil.ItemCallback<MovieData>(){
override fun areItemsTheSame(oldItem: MovieData, newItem: MovieData): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: MovieData, newItem: MovieData): Boolean {
return oldItem == newItem
}
}
}
}
Loading adapter for progress circle when scrolling
class LoaderAdapter : LoadStateAdapter<LoaderAdapter.LoaderHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoaderHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.loader , parent , false)
return LoaderHolder(view)
}
override fun onBindViewHolder(holder: LoaderHolder, loadState: LoadState) {
holder.bind(loadState)
}
inner class LoaderHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
val progress = itemView.findViewById<ProgressBar>(R.id.movieProgressBar)
fun bind(loadState: LoadState){
progress.isVisible = loadState is LoadState.Loading
}
}
}
Edit :
This is my main Activity.
class MainActivity : AppCompatActivity(), SwipeRefreshLayout.OnRefreshListener,
View.OnClickListener{
lateinit var movieViewModel : MoviesViewModel
lateinit var moviesAdapter : MoviesAdapter
lateinit var movieRecyclerView: RecyclerView
lateinit var connectivityLiveStatus: ConnectionLiveStatus
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
init()
}
private fun init(){
connectivityLiveStatus = ConnectionLiveStatus(this)
observeConnectivity()
swipeToRefresh.setOnRefreshListener(this)
movieRecyclerView = findViewById(R.id.moviesRecyclerView)
moviesAdapter = MoviesAdapter()
movieViewModel = ViewModelProvider(this)[MoviesViewModel::class.java]
movieRecyclerView.layoutManager = LinearLayoutManager(this)
movieRecyclerView.setHasFixedSize(true)
movieRecyclerView.adapter = moviesAdapter.withLoadStateHeaderAndFooter(
header = LoaderAdapter(),
footer = LoaderAdapter()
)
nowPlayingTV.setOnClickListener(this)
observeViewModel()
}
//observe connectivity change
private fun observeConnectivity(){
connectivityLiveStatus.observe(this , Observer {status ->
handleConnectivityChange(status)
})
}
//Observe the movie data change
private fun observeViewModel(){
movieViewModel.movieList.observe(this) {
moviesAdapter.submitData(lifecycle, it)
if (swipeToRefresh.isRefreshing) swipeToRefresh.isRefreshing = false
}
}
private fun handleConnectivityChange(status : Boolean){
networkConnectivityStatusTv.visibility = if (status) View.INVISIBLE else View.VISIBLE
nowPlayingTV.visibility = if (status) View.VISIBLE else View.GONE
moviesAdapter.retry()
//change the status bar color according to network status.
val window = window
window.statusBarColor = if (status) applicationContext.resources.getColor(R.color.app_background_color) else applicationContext.resources.getColor(
R.color.network_connectivity_alert_color
)
}
//refresh when swipe
override fun onRefresh() {
moviesAdapter.refresh()
}
override fun onClick(p0: View?) {
when(p0?.id) {
R.id.nowPlayingTV -> {
movieRecyclerView.smoothScrollToPosition(0)
}
}
}
}
And this line of code , which I used to display loading progress while scrolling ( using the LoadAdapter) .
When I remove these lines , The entire paging stops working , No api gets called .
What exactly does this line of code do . is there any other way for this ?
Could this be calling the load from remote mediator again and again ?
You are refreshing the list everytime it's visited:
override suspend fun initialize(): InitializeAction {
return InitializeAction.LAUNCH_INITIAL_REFRESH
}
use this one:
return InitializeAction.SKIP_INITIAL_REFRESH
you can read further here: https://developer.android.com/reference/kotlin/androidx/paging/RemoteMediator#initialize()
I'm currently working with paging 3 library. I'm able to populate the list and pagination is working as expected. But when I call adapter.refresh() method twice by using pull to refresh, the pagination stops working.
I have gone through the documentation and read many articles but no success yet.
One more thing I'm not able to empty the list when I hit pull to refresh. I tried calling invalidate() method of PagingSource but it crashes the app.
https://www.dropbox.com/s/0k2g9mlktv5gee6/22-05-22-10-26-37.mp4?dl=0
VideoPagingSource.kt
class VideoPagingSource(
private val apiInterface: ApiInterface,
private val schoolId: String,
private val ordering: String?,
private val courseId: String?,
private val moduleId: String?,
private val searchText: String?
) : PagingSource<Int, Video>() {
override fun getRefreshKey(state: PagingState<Int, Video>): Int? = null
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Video> {
val pageNumber = params.key
return try {
val response = apiInterface.getVideos(schoolId, ordering, courseId, moduleId, searchText, pageNumber)
val pagedResponse = response?.body()
var nextPageNumber: Int? = null
if (pagedResponse?.links?.next != null) {
val uri = Uri.parse(pagedResponse.links.next)
val nextPageQuery = uri.getQueryParameter("page")
nextPageNumber = nextPageQuery?.toInt()
}
LoadResult.Page(
data = response?.body()?.objects.orEmpty(),
prevKey = null,
nextKey = nextPageNumber
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
PreClassViewModel.kt
class PreClassViewModel #Inject constructor(private val repository: PreClassRepository) :
ViewModel() {
private val queryParamLiveData = MutableLiveData<QueryParams>()
val videosLiveData = queryParamLiveData.switchMap {
repository.getVideos(
schoolId = it.extraArgs[0],
ordering = it.extraArgs[1],
courseId = it.courseId,
moduleId = it.chapterId,
searchText = it.searchText
).cachedIn(viewModelScope)
}
fun setQueryParam(queryParams: QueryParams){
queryParamLiveData.value = queryParams
}
}
I experienced a similar problem where the adapter provided only the first page after refresh(). The reason was a wrong pageSize provided to the PagingConfig.
I am trying to implement paging for the TMDB API using paging3 and paging-compose.
Here the source of truth is database and api calls are handled by Remote-mediator.
Repository:
class Repository #Inject constructor(
val database: Database,
private val apiService: ApiService
){
#ExperimentalPagingApi
fun movies(): Flow<PagingData<Movie>> = Pager(
config = PagingConfig(pageSize = 20),
remoteMediator = MovieRemoteMediator(database,apiService),
){
database.movieDao().pagedTopRated()
}.flow
}
RemoteMediator:
#ExperimentalPagingApi
class MovieRemoteMediator(
private val database: Database,
private val networkService: ApiService
) : RemoteMediator<Int, Movie>() {
override suspend fun load(loadType: LoadType, state: PagingState<Int, Movie>): MediatorResult {
val page:Int = when(loadType){
LoadType.REFRESH -> {
val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
remoteKeys?.nextKey?.minus(1) ?: 1
}
LoadType.PREPEND -> {
val remoteKeys = getRemoteKeyForFirstItem(state)
val prevKey = remoteKeys?.prevKey
?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
prevKey
}
LoadType.APPEND -> {
val remoteKeys = getRemoteKeyForLastItem(state)
val nextKey = remoteKeys?.nextKey
?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
nextKey
}
}
return try {
val response
: MovieResponse = networkService.getTopRatedMovies(page)
val toInsert: MutableList<Movie> = mutableListOf();
for (i in response.results)
toInsert.add(i.mapToMovie());
val endOfPaginationReached = response.page + 1 > response.totalPages
database.withTransaction {
if (loadType == LoadType.REFRESH) {
database.movieKeyDao().clearRemoteKeys()
database.movieDao().clearMovies()
}
val prevKey = if (page == 1) null else page - 1
val nextKey = if (endOfPaginationReached) null else page + 1
val keys = response.results.map {
MovieKey(movieId = it.id, prevKey = prevKey, nextKey = nextKey)
}
database.movieDao().insertMovies(toInsert)
database.movieKeyDao().insertAll(keys)
}
MediatorResult.Success(
endOfPaginationReached = endOfPaginationReached
)
} catch (e: IOException) {
MediatorResult.Error(e)
} catch (e: HttpException) {
MediatorResult.Error(e)
}
}
private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, Movie>): MovieKey? {
// Get the last page that was retrieved, that contained items.
// From that last page, get the last item
return state.pages.lastOrNull() { it.data.isNotEmpty() }?.data?.lastOrNull()
?.let { repo ->
// Get the remote keys of the last item retrieved
database.movieKeyDao().remoteKeysMovieId(repo.id)
}
}
private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, Movie>): MovieKey? {
// Get the first page that was retrieved, that contained items.
// From that first page, get the first item
return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull()
?.let { repo ->
// Get the remote keys of the first items retrieved
database.movieKeyDao().remoteKeysMovieId(repo.id)
}
}
private suspend fun getRemoteKeyClosestToCurrentPosition(
state: PagingState<Int, Movie>
): MovieKey? {
// The paging library is trying to load data after the anchor position
// Get the item closest to the anchor position
return state.anchorPosition?.let { position ->
state.closestItemToPosition(position)?.id?.let { repoId ->
database.movieKeyDao().remoteKeysMovieId(repoId)
}
}
}
ViewModel:
#HiltViewModel
class MovieViewModel #Inject constructor(
private val movieRepository: Repository
) : ViewModel() {
#ExperimentalPagingApi
fun getMovies() = movieRepository.movies().cachedIn(viewModelScope)
}
Ui Screen:
#ExperimentalCoilApi
#ExperimentalPagingApi
#Composable
fun MainScreen(){
val viewModel: MovieViewModel = viewModel()
val movieList = viewModel.getMovies().collectAsLazyPagingItems()
LazyColumn{
items(movieList){ movie ->
if (movie != null) {
Card(movie)
}
}
}
}
#ExperimentalCoilApi
#ExperimentalPagingApi
#Composable
fun Main(){
MainScreen()
}
MainActivty.kt
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
#ExperimentalCoilApi
#ExperimentalPagingApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
JetpackComposePagingTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
Main()
}
}
}
}
}
I am following the paging3 Codelab for writing the remoteMediator.
On opening the app, it only loads the first 2 pages and then loops infinitely making infinite retrofit calls
You doesn't do exactly as The Codelab you're referencing suggests: they are creating paging Flow inside init and only update it when the query string changes.
On the other hand, you're calling viewModel.getMovies() on each recomposition which causes your problem. Check out how you should work with side effects in Compose in documentation.
In this particular case, as you don't have any parameters, you can simply create it once in the view model like this:
#HiltViewModel
class MovieViewModel #Inject constructor(
private val movieRepository: Repository
) : ViewModel() {
val moviesPagingFlow = movieRepository.movies().cachedIn(viewModelScope)
}
No idea why but in my case isolating the LazyPagingItems from LazyColumn worked.
Try the following:
#Composable
fun MainScreen(){
val viewModel: MovieViewModel = viewModel()
val movieList = viewModel.getMovies().collectAsLazyPagingItems()
MainScreen(movies = movieList)
}
#Composable
fun MainScreen(movies: LazyPagingItems<Movie>){
LazyColumn{
items(movies){ movie ->
if (movie != null) {
Card(movie)
}
}
}
}
VideoStatusDataSource.kt
class VideoStatusDataSource(
private val categoryKey: String,
private val videosStatusApi: VideoStatusApiService
) : PagingSource<Int, VideoStatus>() {
companion object {
private const val VIDEO_STARTING_PAGE_INDEX = 0
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, VideoStatus> {
return try {
val pageIndex = params.key ?: VIDEO_STARTING_PAGE_INDEX
logger(params.key)
logger(pageIndex)
val response =
videosStatusApi.getVideoStatusByPageNumberAndCategoryName(pageIndex, categoryKey)
val jsonCategoryResponse = response.getAsJsonArray(DATA_KEY)
val videoStatusList: List<VideoStatus> = Gson().fromJson(jsonCategoryResponse)
LoadResult.Page(
data = videoStatusList.orEmpty(),
prevKey = if (pageIndex == VIDEO_STARTING_PAGE_INDEX) null else pageIndex - 1,
nextKey = if (videoStatusList.isEmpty()) null else pageIndex.plus(1)
)
} catch (exception: IOException) {
LoadResult.Error(exception)
} catch (exception: HttpException) {
LoadResult.Error(exception)
} catch (exception: Exception) {
LoadResult.Error(exception)
}
}
override fun getRefreshKey(state: PagingState<Int, VideoStatus>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}
video-API service is providing 10 results on each page but with this data source class it loads all data at once
I want only the first 10 items to load initially and then use scroll first 10 items it needs to load the next 10 items
here is my paging data repository
MainRepopsitory.kt
fun getVideoStatusPagingData(categoryKey: String): Pager<Int, VideoStatus> =
Pager(
config = PagingConfig(
pageSize = 10
),
pagingSourceFactory = { VideoStatusDataSource(categoryKey, videosStatusApi) }
)
ViewModel
#HiltViewModel
class PagingViewModel #Inject constructor(
private val mainRepository: MainRepository,
#IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : ViewModel() {
fun getCurrentCategoryVideoStatus(categoryKey: String): Flow<PagingData<VideoStatus>> =
mainRepository
.getVideoStatusPagingData(categoryKey)
.flow.cachedIn(viewModelScope)
.flowOn(ioDispatcher)
}
This is how I'm using load function in my paging sources you can get help from this I have five to six paging sources in my app and all have same implementations like this
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Message> {
return try {
val nextPage = params.key ?: 0
chatWithSellerRequest.offset = nextPage.times(PAGE_SIZE_LIMIT)
val response = apiService.getSellerChatResponse(chatWithSellerRequest)
_chatWithSellerResultResponse.value = response.chatWithSellerResult
LoadResult.Page(
data = response.chatWithSellerResult?.messages!!,
prevKey = null,
nextKey = if (response.chatWithSellerResult.messages.isEmpty()) null else nextPage + 1
)
} catch (e: Exception) {
LoadResult.Error(e)
}
I want to implement pagination to show a chunk of list of my desire item view in my app. That's why I choose to use Google newly released paging library i.e Paging library 3. I use Rxjava, Livedata, and ViewModel in my app.
After implementing the paging library, I am facing a weird problem. When I call the method for fetching list, it's calling again and again and not stopped calling the call. In fact, it automatically increases the page number although I did not scroll the list.
Here is the code I tried
JobListRestApi.kt
interface JobListRestApi {
#GET("job/list")
fun getJobResponse(
#Query("page") pageNumber: Int
): Single<Response<JobResponse>>
}
JobListDataSource.kt
class JobListDataSource #Inject constructor(
private val jobListRestApi: JobListRestApi
): RxPagingSource<Int, Job>() {
override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, Job>> {
val position = params.key ?: 1
return jobListRestApi.getJobResponse(position).toSingle()
.subscribeOn(Schedulers.io())
.map { jobResponse -> jobResponse.jobData.jobs }
.map { jobs -> toLoadResult(jobs, position) }
.onErrorReturn { LoadResult.Error(it) }
}
private fun toLoadResult(data: ArrayList<Job>, position: Int): LoadResult<Int, Job> {
val prevKey = if (position == 1) null else position-1
val nextKey = if (position == data.size) null else position+1
return LoadResult.Page(data, prevKey, nextKey)
}
}
JobListRepositoryImpl.kt
class JobListRepositoryImpl #Inject constructor(
private val jobListDataSource: JobListDataSource
): JobListRepository {
override fun getJobs(): Flowable<PagingData<Job>> {
return Pager(PagingConfig(pageSize = 20)) {
jobListDataSource
}.flowable
}
}
JobListViewModel.kt
class JobListViewModel #Inject constructor(
private val jobListRepository: JobListRepository
): BaseViewModel() {
val jobs: MutableLiveData<PagingData<Job>> = MutableLiveData()
fun getJobs() {
if (jobs.value == null) {
compositeDisposable += jobListRepository.getJobs()
.subscribe({
jobs.value = it
}, {
handleException(it)
})
}
}
}
JobListFragment.kt
class JobListFragment : BaseFragment<JobListViewModel>() {
private val jobAdapter: JobAdapter by lazy {
JobAdapter { }
}
override fun getLayoutResource() = R.layout.fragment_job_list
override fun initWidget() {
job_recycler_view.adapter = jobAdapter
}
override fun onResume() {
super.onResume()
viewModel.getJobs()
}
override fun observeLiveData() {
observe(viewModel.jobs) {
jobAdapter.submitData(lifecycle, it)
}
}
}
And the output log is
https://base-url/job/list?page=1
https://base-url/job/list?page=2
https://base-url/job/list?page=3
https://base-url/job/list?page=4
https://base-url/job/list?page=5
https://base-url/job/list?page=6
https://base-url/job/list?page=7
how can I stop calling serial api unless I go to the last item of the chunk in RecyclerView and scroll the list
You tell Paging there is no more to load by returning null for prevKey and nextKey inLoadResult.Page
Since its infinitely appending, it looks like you never set nextKey to null. Perhaps you meant to check data.isEmpty() instead of key == data.size?
Eventually, I got the error. In fact, the problem was in my Http response Data. The updated DataSource is given bellow
JobListDataSource.kt
class JobListDataSource #Inject constructor(
private val jobListRestApi: JobListRestApi
): RxPagingSource<Int, Job>() {
override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, Job>> {
val position = params.key ?: 1
return jobListRestApi.getJobResponse(position).toSingle()
.subscribeOn(Schedulers.io())
.map { jobResponse -> jobResponse.jobData }
.map { jobData -> toLoadResult(jobData, position) }
.onErrorReturn { LoadResult.Error(it) }
}
private fun toLoadResult(data: JobData, position: Int): LoadResult<Int, Job> {
val prevKey = if (position == 1) null else position-1
val nextKey = if (data.hasMore) position+1 else null
return LoadResult.Page(data.jobs, prevKey, nextKey)
}
}