Is it possible to display chat messages through the Android Paging Library? - android

I want to merge two message list(stored chat message, incoming chat message)
these two data is split based on the last check time.
I want stored chat message to be Paging and merge with incoming data
This is my code:
#Query("SELECT * FROM msgentity WHERE room_id =:roomId and time <= :lastReadTime ORDER BY time")
fun getPassedMsg(roomId: String, lastReadTime: Date) : PagingSource<Int, MsgEntity>
#Query("SELECT * FROM msgentity WHERE room_id=:roomId and time > :lastReadTime ORDER BY time")
suspend fun getNewMsg(roomId: String, lastReadTime: Date) : Flow<List<MsgEntity>>
Is it possible to merge these two items of data?
Or do I have to make my own paging object?

Yeah definitely, you can show messages using paging.
if your question is that how ?
So here I am using MVVM architecture , Kotlin coroutine
Step 1. you need to create a paging source for request and getting response
(here MessageList is api response model )
class MessagePaging(private val apiService: ApiService) : PagingSource<Int, MessageList.Data>() {
private var STARTING_PAGE_INDEX = 1
override fun getRefreshKey(state: PagingState<Int, MessageList.Data>): Int? {
return 1
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MessageList.Data> {
val page = params.key ?: STARTING_PAGE_INDEX
return try {
val response = apiService.getMessageList(page)
val data = response.body()?.data!!
if (!data.isNullOrEmpty()){
LoadResult.Page(
data = data,
prevKey = if (page == STARTING_PAGE_INDEX) null else page -1,
nextKey = if (data.isEmpty()) null else page + 1
)
}else{
return LoadResult.Error(NullPointerException("Data is Null or Empty"))
}
} catch (e : HttpException){
return LoadResult.Error(e)
} catch (e : IOException){
return LoadResult.Error(e)
} catch (e : Exception){
return LoadResult.Error(e)
}
}
}
Step 2 :- Create request in apiservice
#GET( GET_CONSULTATION_LIST )
suspend fun getMessageList(#Query("page") page : Int) : Response<MessageList>
Step 3:- in ApiHelperImpl interface
fun getConsultationList(): Flow<PagingData<ConsultationList.Data>> {
return Pager(
config = PagingConfig(
pageSize = 40,
enablePlaceholders = false,
prefetchDistance = 1
),
pagingSourceFactory = { ConsultantPaging(apiService) }
).flow
}
Step 4 :- In View model
fun getConsultationList() = apiHelper.getConsultationList().cachedIn(viewModelScope)
Step 5 :- In Fragment
private fun observeConsultations() {
lifecycleScope.launch {
viewModel.getConsultationList().collectLatest {
launch(Dispatchers.Main) {
adapter.loadStateFlow.collectLatest { loadStates ->
if (loadStates.refresh is LoadState.Loading) {
// loader.show()
} else {
// loader.dismiss()
if (loadStates.refresh is LoadState.Error) {
if (adapter.itemCount < 1) {
binding.clNoConsult.visibility = View.VISIBLE
} else {
binding.clNoConsult.visibility = View.GONE
}
}
}
}
}
adapter.submitData(it)
}
}
}
Step 6:- Create Paging Adapter
class ConsultationPagingAdapter (private val click: GetClicksOnItem ) : PagingDataAdapter<MessageList.Data, ConsultationPagingAdapter.ViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(ListConsultationsBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
#SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val result = getItem(position)!!
holder.binding.apply {
tvName.text = result.name.capitalizeWords()
root.setOnClickListener {
click.action(result)
}
executePendingBindings()
}
}
class ViewHolder(val binding: ListConsultationsBinding) : RecyclerView.ViewHolder(binding.root)
private class DiffCallback : DiffUtil.ItemCallback<MessageList.Data>() {
override fun areItemsTheSame(
oldItem: MessageList.Data,
newItem: MessageList.Data
): Boolean = oldItem == newItem
override fun areContentsTheSame(
oldItem: MessageList.Data,
newItem: MessageList.Data
): Boolean = oldItem == newItem
}
interface GetClicksOnItem {
fun action(data: MessageList.Data)
}
}
If you getting understating then approve the answer

Related

Api Call With Paging 3 Not Getting Triggred [ Closed ]

I am trying to make an api call with paging 3 and retrofit. But for some reason the api call is not getting triggered. I checked my code multiple times to find an issue but couldn't understand why am I doing wrong. When I make that api call seperately then its getting called. But not with paging 3. Can someone give some idea why it might be the case? And how can I find the problem here ?
My Api Service Class
interface ApiService {
companion object{
const val BASE_URL = "https://picsum.photos/v2/"
}
#GET("list")
suspend fun getImageList(
#Query("page") pageNo:Int,
#Query("limit") limit: Int=20
):Response<ImageListResponse>
}
My Paging Source
class ImagePagingSource(private val api:ApiService): PagingSource<Int, ImageItem>(){
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ImageItem> {
try {
Log.d("ImagePagingSource","Load is Called")
val position = params.key ?: 1
val previousKey = if (position == 1) null else position - 1
/*
* We don't have the total page key in this api response.
* So restricting the call after page 10 statically.
* */
val nextKey = if (position == 10) null else position + 1
val response = api.getImageList(position)
if (!response.isSuccessful) return LoadResult.Error(Exception("Api Error : ${response.message()}"))
if (response.body().isNullOrEmpty()) return LoadResult.Error(Exception("No Image Found"))
Log.d("ImagePagingSource",response.body().toString())
return LoadResult.Page(
data = response.body()!!,
prevKey = previousKey,
nextKey = nextKey
)
} catch (e: Exception) {
return LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, ImageItem>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}
My Repository Class
class HomePageRepo
#Inject
constructor(
private val apiService: ApiService
)
{
fun getImages() = Pager(
config = PagingConfig(pageSize = 20, maxSize = 100),
pagingSourceFactory = { ImagePagingSource(apiService) }
).liveData
suspend fun callApi(){
apiService.getImageList(1)
}
}
View Model
fun getSearchResult():LiveData<PagingData<ImageItem>> = repo.getImages().cachedIn(viewModelScope)
Paging Adapter For Recycler View
class ImageShowPagingAdapter(private val context:Context) : PagingDataAdapter<
ImageItem,
ImageShowPagingAdapter.ImageShowViewHolder
>(COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageShowViewHolder {
val inflater = LayoutInflater.from(context)
val view = RecViewItemImagesBinding.inflate(inflater, parent, false)
return ImageShowViewHolder(view)
}
override fun onBindViewHolder(holder: ImageShowViewHolder, position: Int) {
val item = getItem(position)
Glide.with(context)
.load(item?.download_url)
.apply(
RequestOptions()
.placeholder(R.drawable.all_type_content_default)
.error(R.drawable.all_type_content_default)
)
.into(holder.binding.ivImage)
}
inner class ImageShowViewHolder(val binding: RecViewItemImagesBinding) : RecyclerView.ViewHolder(binding.root)
companion object {
private val COMPARATOR = object : DiffUtil.ItemCallback<ImageItem>() {
override fun areItemsTheSame(oldItem: ImageItem, newItem: ImageItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ImageItem, newItem: ImageItem): Boolean {
return oldItem == newItem
}
}
}
}
Here is the complete url that I am trying to hit
https://picsum.photos/v2/list?page=1&limit=20
Can someone please help me out to understand why it is happening or how to find the problem here. Thank you in advance
It seems the problem was inside of my recycler view paging adapter what was causing the problem. I have solved the problem now.
It seems the problem was inside of my recycler view paging adapter what was causing the problem. I was instanciating the paging adapter but when setting it with recycler view there were some error in my code. Hence resulting the api not getting called at all.If anyone facing the same issue please check your paging adapter and see if it is getting assigned properly with your recyclerview.

Android RemoteMediator calls API again and again

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

Android Paging 3 leading to duplicate rows

I was trying out the Paging 3.0.1 version. The API calls are happening right when I printed the log. But the data shown is duplicate. Could someone tell me where I went wrong?
Page data source class
class MyPageDataSource(private val api: RetrofitInstance) :
PagingSource<Int, APIDataResponse>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, APIDataResponse> {
return try {
val nextPageNumber = params.key ?: FIRST_PAGE_NUMBER
val response = api.getData(nextPageNumber, PAGE_SIZE)
LoadResult.Page(
data = response.APIS!!,
prevKey = if (nextPageNumber > FIRST_PAGE_NUMBER) nextPageNumber - 1 else null,
nextKey = if (nextPageNumber * PAGE_SIZE < response.total!!) nextPageNumber + 1 else null
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, APIDataResponse>): Int? {
return state.anchorPosition
}
companion object {
const val FIRST_PAGE_NUMBER = 1
const val PAGE_SIZE = 20
}
}
Adapter:
class MyListingAdapter() : PagingDataAdapter<APIDataResponse, MyListingAdapter.MyViewHolder>(MyComparator) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(
FragmentItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(item = getItem(position))
}
inner class MyViewHolder(binding: FragmentItemBinding) :
RecyclerView.ViewHolder(binding.root) {
private val title: TextView = binding.title
fun bind(item: APIDataResponse?) {
if(item != null) {
title.text = item.title
}
}
}
object MyComparator : DiffUtil.ItemCallback<APIDataResponse>() {
override fun areItemsTheSame(
oldItem: APIDataResponse,
newItem: APIDataResponse
): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(
oldItem: APIDataResponse,
newItem: APIDataResponse
): Boolean {
return oldItem == newItem
}
}
}
View Model:
class PagingViewModel : ViewModel() {
fun getData() : Flow<PagingData<APIDataResponse>> {
return Pager(
PagingConfig(
pageSize = 20,
enablePlaceholders = false,
maxSize = 40,
initialLoadSize = 20,
prefetchDistance = 10
)
) {
MyPageDataSource(RetrofitInstance())
}.flow.cachedIn(viewModelScope)
}
}
Recycler view setting up in the fragment:
val myAdapter = MyListingAdapter(myActivity)
//Setup the recyclerview
binding.myList.apply {
layoutManager = when {
columnCount <= 1 -> LinearLayoutManager(context)
else -> GridLayoutManager(context, columnCount)
}
myAdapter.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
val decoration =
DividerItemDecoration(myActivity, DividerItemDecoration.VERTICAL)
addItemDecoration(decoration)
setHasFixedSize(true)
adapter = myAdapter
}
lifecycleScope.launch {
viewModel.getData().distinctUntilChanged().collectLatest { pagedData ->
myAdapter.submitData(pagedData)
}
}
I had the same problem.
It solved by setting the correct pageSize.
This problem can also happen by setting the wrong maxSize, initialLoadSize or other attributes.
You have set setHasFixedSize(true) it means data won't change because of a change in the adapter content. For example, the RecyclerView size can change because of a size change on its parent. Maybe that's why you are getting the same records. try to remove it and then check it works or not.

Android Paging 3 - experiencing flickers, glitches or jumps in position when scrolling & loading new pages

Hello Guys im using Android Jetpack Paging library 3, I'm creating a news app that implements network + database scenario, and im following the codelab by google https://codelabs.developers.google.com/codelabs/android-paging , im doing it almost like in the codelab i almost matched all the operations shown in the examples https://github.com/android/architecture-components-samples/tree/main/PagingWithNetworkSample.
It works almost as it should...but my backend response is page keyed, i mean response comes with the list of news and the next page url, remote mediator fetches the data, populates the database, repository is set, viewmodel is set...
The problem is :
when recyclerview loads the data , following happens:recyclerview flickers, items jump, are removed , added again and so on.
I dont know why recyclerview or its itemanimator behaves like that , that looks so ugly and glitchy.
More than that, when i scroll to the end of the list new items are fetched and that glitchy and jumping effect is happening again.
I would be very grateful if you could help me, im sitting on it for three days , thank you very much in advance.Here are my code snippets:
#Entity(tableName = "blogs")
data class Blog(
#PrimaryKey(autoGenerate = true)
val databaseid:Int,
#field:SerializedName("id")
val id: Int,
#field:SerializedName("title")
val title: String,
#field:SerializedName("image")
val image: String,
#field:SerializedName("date")
val date: String,
#field:SerializedName("share_link")
val shareLink: String,
#field:SerializedName("status")
val status: Int,
#field:SerializedName("url")
val url: String
) {
var categoryId: Int? = null
var tagId: Int? = null
}
Here's the DAO
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(blogs: List<Blog>)
#Query("DELETE FROM blogs")
suspend fun deleteAllBlogs()
#Query("SELECT * FROM blogs WHERE categoryId= :categoryId ORDER BY id DESC")
fun getBlogsSourceUniversal(categoryId:Int?): PagingSource<Int, Blog>
#Query("SELECT * FROM blogs WHERE categoryId= :categoryId AND tagId= :tagId ORDER BY id DESC")
fun getBlogsSourceUniversalWithTags(categoryId:Int?,tagId:Int?): PagingSource<Int, Blog>
NewsDatabaseKt
abstract class NewsDatabaseKt : RoomDatabase() {
abstract fun articleDAOKt(): ArticleDAOKt
abstract fun remoteKeyDao(): RemoteKeyDao
companion object {
#Volatile
private var INSTANCE: NewsDatabaseKt? = null
fun getDatabase(context: Context): NewsDatabaseKt =
INSTANCE ?: synchronized(this) {
INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
}
private fun buildDatabase(context: Context) =
Room.databaseBuilder(context.applicationContext,
NewsDatabaseKt::class.java,
"news_database_kt")
.build()
}
RemoteMediator
#ExperimentalPagingApi
class BlogsRemoteMediator(private val categoryId: Int,
private val service: NewsAPIInterfaceKt,
private val newsDatabase: NewsDatabaseKt,
private val tagId : Int? = null ,
private val initialPage:Int = 1
) : RemoteMediator<Int, Blog>() {
override suspend fun initialize(): InitializeAction {
return InitializeAction.LAUNCH_INITIAL_REFRESH
}
override suspend fun load(loadType: LoadType, state: PagingState<Int, Blog>): MediatorResult {
try {
val page = when (loadType) {
REFRESH ->{
initialPage
}
PREPEND -> {
return MediatorResult.Success(endOfPaginationReached = true)}
APPEND -> {
val remoteKey = newsDatabase.withTransaction {
newsDatabase.remoteKeyDao().remoteKeyByLatest(categoryId.toString())
}
if(remoteKey.nextPageKey == null){
return MediatorResult.Success(endOfPaginationReached = true)
}
remoteKey.nextPageKey.toInt()
}
}
val apiResponse =
if(tagId == null) {
service.getCategoryResponsePage(RU, categoryId, page.toString())
}else{
service.getCategoryTagResponsePage(RU,categoryId,tagId,page.toString())
}
val blogs = apiResponse.blogs
val endOfPaginationReached = blogs.size < state.config.pageSize
newsDatabase.withTransaction {
// clear all tables in the database
if (loadType == LoadType.REFRESH) {
newsDatabase.remoteKeyDao().deleteByLatest(categoryId.toString())
if(tagId == null) {
newsDatabase.articleDAOKt().clearBlogsByCatId(categoryId)
}else {
newsDatabase.articleDAOKt().clearBlogsByCatId(categoryId,tagId)
}
}
blogs.map {blog ->
blog.categoryId = categoryId
if(tagId != null) {
blog.tagId = tagId
}
}
newsDatabase.remoteKeyDao().insert(LatestRemoteKey(categoryId.toString(),
apiResponse.nextPageParam))
newsDatabase.articleDAOKt().insertAll(blogs)
}
return MediatorResult.Success(
endOfPaginationReached = endOfPaginationReached
)
} catch (exception: IOException) {
return MediatorResult.Error(exception)
} catch (exception: HttpException) {
return MediatorResult.Error(exception)
}
}
PagingRepository
class PagingRepository(
private val service: NewsAPIInterfaceKt,
private val databaseKt: NewsDatabaseKt
){
#ExperimentalPagingApi
fun getBlogsResultStreamUniversal(int: Int, tagId : Int? = null) : Flow<PagingData<Blog>>{
val pagingSourceFactory = {
if(tagId == null) {
databaseKt.articleDAOKt().getBlogsSourceUniversal(int)
}else databaseKt.articleDAOKt().getBlogsSourceUniversalWithTags(int,tagId)
}
return Pager(
config = PagingConfig(
pageSize = 1
)
,remoteMediator =
BlogsRemoteMediator(int, service, databaseKt,tagId)
,pagingSourceFactory = pagingSourceFactory
).flow
}
}
BlogsViewmodel
class BlogsViewModel(private val repository: PagingRepository):ViewModel(){
private var currentResultUiModel: Flow<PagingData<UiModel.BlogModel>>? = null
private var categoryId:Int?=null
#ExperimentalPagingApi
fun getBlogsUniversalWithUiModel(int: Int, tagId : Int? = null):
Flow<PagingData<UiModel.BlogModel>> {
val lastResult = currentResultUiModel
if(lastResult != null && int == categoryId){
return lastResult
}
val newResult: Flow<PagingData<UiModel.BlogModel>> =
repository.getBlogsResultStreamUniversal(int, tagId)
.map { pagingData -> pagingData.map { UiModel.BlogModel(it)}}
.cachedIn(viewModelScope)
currentResultUiModel = newResult
categoryId = int
return newResult
}
sealed class UiModel{
data class BlogModel(val blog: Blog) : UiModel()
}
PoliticsFragmentKotlin
#ExperimentalPagingApi
class PoliticsFragmentKotlin : Fragment() {
private lateinit var recyclerView: RecyclerView
private lateinit var pagedBlogsAdapter:BlogsAdapter
lateinit var viewModelKt: BlogsViewModel
lateinit var viewModel:NewsViewModel
private var searchJob: Job? = null
#ExperimentalPagingApi
private fun loadData(categoryId:Int, tagId : Int? = null) {
searchJob?.cancel()
searchJob = lifecycleScope.launch {
viewModelKt.getBlogsUniversalWithUiModel(categoryId, tagId).collectLatest {
pagedBlogsAdapter.submitData(it)
}
}
}
#ExperimentalPagingApi
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_blogs, container, false)
viewModelKt = ViewModelProvider(requireActivity(),Injection.provideViewModelFactory(requireContext())).get(BlogsViewModel::class.java)
viewModel = ViewModelProvider(requireActivity()).get(NewsViewModel::class.java)
pagedBlogsAdapter = BlogsAdapter(context,viewModel)
val decoration = DividerItemDecoration(context, DividerItemDecoration.VERTICAL)
recyclerView = view.findViewById(R.id.politics_recyclerView)
recyclerView.addItemDecoration(decoration)
initAdapter()
loadData(categoryId)
initLoad()
return view
}
private fun initLoad() {
lifecycleScope.launchWhenCreated {
Log.d("meylis", "lqunched loadstate scope")
pagedBlogsAdapter.loadStateFlow
// Only emit when REFRESH LoadState for RemoteMediator changes.
.distinctUntilChangedBy { it.refresh }
// Only react to cases where Remote REFRESH completes i.e., NotLoading.
.filter { it.refresh is LoadState.NotLoading }
.collect { recyclerView.scrollToPosition(0) }
}
}
private fun initAdapter() {
recyclerView.adapter = pagedBlogsAdapter.withLoadStateHeaderAndFooter(
header = BlogsLoadStateAdapter { pagedBlogsAdapter.retry() },
footer = BlogsLoadStateAdapter { pagedBlogsAdapter.retry() }
)
lifecycleScope.launchWhenCreated {
pagedBlogsAdapter.loadStateFlow.collectLatest {
swipeRefreshLayout.isRefreshing = it.refresh is LoadState.Loading
}
}
pagedBlogsAdapter.addLoadStateListener { loadState ->
// Only show the list if refresh succeeds.
recyclerView.isVisible = loadState.source.refresh is LoadState.NotLoading
// Show loading spinner during initial load or refresh.
progressBar.isVisible = loadState.source.refresh is LoadState.Loading
// Show the retry state if initial load or refresh fails.
retryButton.isVisible = loadState.source.refresh is LoadState.Error
// Toast on any error, regardless of whether it came from RemoteMediator or PagingSource
val errorState = loadState.source.append as? LoadState.Error
?: loadState.source.prepend as? LoadState.Error
?: loadState.append as? LoadState.Error
?: loadState.prepend as? LoadState.Error
errorState?.let {
Toast.makeText(context, "\uD83D\uDE28 Wooops ${it.error}", Toast.LENGTH_LONG
).show()
}
}
}
companion object {
#JvmStatic
fun newInstance(categoryId: Int, tags : ArrayList<Tag>): PoliticsFragmentKotlin {
val args = Bundle()
args.putInt(URL, categoryId)
args.putSerializable(TAGS,tags)
val fragmentKotlin = PoliticsFragmentKotlin()
fragmentKotlin.arguments = args
Log.d("meylis", "created instance")
return fragmentKotlin
}
}
BlogsAdapter
class BlogsAdapter(var context: Context?, var newsViewModel:NewsViewModel) :
PagingDataAdapter<BlogsViewModel.UiModel.BlogModel, RecyclerView.ViewHolder>
(REPO_COMPARATOR) {
private val VIEW = 10
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
VIEW -> MyViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.card_layout, parent, false))
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val uiModel = getItem(position)
if(uiModel == null){
if(uiModel is BlogsViewModel.UiModel.BlogModel){(holder as MyViewHolder).bind(null)}
}
if(uiModel is BlogsViewModel.UiModel.BlogModel){(holder as
MyViewHolder).bind(uiModel.blog)}
}
override fun getItemViewType(position: Int): Int {
return VIEW
}
companion object {
private val REPO_COMPARATOR = object : DiffUtil.ItemCallback<BlogsViewModel.UiModel.BlogModel>() {
override fun areItemsTheSame(oldItem: BlogsViewModel.UiModel.BlogModel, newItem: BlogsViewModel.UiModel.BlogModel): Boolean =
oldItem.blog.title == newItem.blog.title
override fun areContentsTheSame(oldItem: BlogsViewModel.UiModel.BlogModel, newItem: BlogsViewModel.UiModel.BlogModel): Boolean =
oldItem == newItem
}
}
MyViewHolder
class MyViewHolder(var container: View) : RecyclerView.ViewHolder(container) {
var cv: CardView
#JvmField
var mArticle: TextView
var date: TextView? = null
#JvmField
var time: TextView
#JvmField
var articleImg: ImageView
#JvmField
var shareView: View
var button: MaterialButton? = null
#JvmField
var checkBox: CheckBox
var progressBar: ProgressBar
private var blog:Blog? = null
init {
cv = container.findViewById<View>(R.id.cardvmain) as CardView
mArticle = container.findViewById<View>(R.id.article) as TextView
articleImg = container.findViewById<View>(R.id.imgvmain) as ImageView
//button = (MaterialButton) itemView.findViewById(R.id.sharemain);
checkBox = container.findViewById<View>(R.id.checkboxmain) as CheckBox
time = container.findViewById(R.id.card_time)
shareView = container.findViewById(R.id.shareView)
progressBar = container.findViewById(R.id.blog_progress)
}
fun bind(blog: Blog?){
if(blog == null){
mArticle.text = "loading"
time.text = "loading"
articleImg.visibility = View.GONE
}else {
this.blog = blog
mArticle.text = blog.title
time.text = blog.date
if (blog.image.startsWith("http")) {
articleImg.visibility = View.VISIBLE
val options: RequestOptions = RequestOptions()
.centerCrop()
.priority(Priority.HIGH)
GlideImageLoader(articleImg,
progressBar).load(blog.image, options)
} else {
articleImg.visibility = View.GONE
}
}
}
}
NewsApiInterface
interface NewsAPIInterfaceKt {
#GET("sort?")
suspend fun getCategoryResponsePage(#Header("Language") language: String, #Query("category")
categoryId: Int, #Query("page") pageNumber: String): BlogsResponse
#GET("sort?")
suspend fun getCategoryTagResponsePage(#Header("Language") language: String,
#Query("category") categoryId: Int,#Query("tag") tagId:Int, #Query("page") pageNumber: String)
:BlogsResponse
companion object {
fun create(): NewsAPIInterfaceKt {
val logger = HttpLoggingInterceptor()
logger.level = HttpLoggingInterceptor.Level.BASIC
val okHttpClient = UnsafeOkHttpClient.getUnsafeOkHttpClient()
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(NewsAPIInterfaceKt::class.java)
}
}
}
I have tried setting initialLoadSize = 1
But the problem still persists
EDIT: Thanks for your answer #dlam , yes, it does , my network API returns the list of results ordered by id. BTW, items do this jump when the application is run offline as well.
Videos when refreshing and loading online
online loading and paging
online loading and paging(2)
Videos when refreshing and loading offline
offline loading and refreshing
Thanks again, here is my gist link https://gist.github.com/Aydogdyshka/7ca3eb654adb91477a42128de2f06ea9
EDIT
Thanks a lot to #dlam, when I set pageSize=10, jumping has disappeared...Then i remembered why i set pageSize=1 in the first place... when i refresh , 3 x pageSize of items are loaded, even if i overrided initialLoadSize = 10 , it still loads 3 x pageSize calling append 2x times after refresh , what could i be doing wrong, what's the correct way to only load first page when i refresh ?
Just following up here from comments:
Setting pageSize = 10 fixes the issue.
The issue was with pageSize being too small, resulting in PagingSource refreshes loading pages that did not cover the viewport. Since source refresh replaces the list and goes through DiffUtil, you need to provide an initialLoadSize that is large enough so that there is some overlap (otherwise scroll position will be lost).
BTW - Paging loads additional data automatically based on PagingConfig.prefetchDistance. If RecyclerView binds items close enough to the edge of the list, it will automatically trigger APPEND / PREPEND loads. This is why the default of initialLoadSize is 3 * pageSize, but if you're still experiencing additional loads, I would suggest either adjusting prefetchDistance, or increasing initialLoadSize further.
config = PagingConfig(
pageSize = PAGE_SIZE,
enablePlaceholders = true,
prefetchDistance = 3* PAGE_SIZE,
initialLoadSize = 2*PAGE_SIZE,
)
make sure enablePlaceholders is set to true and set the page size to around 10 to 20
recyclerview flickers becouse from dao you get items not the same order it was responded from network.
I will suggest you my solution.
we will get items from database order by primary key, databaseid, descending.
first of all delete autogenerated = true.
we will set databaseid manualy, in same order we got items from network.
next lets edit remoteMediator load function.
when (loadType) {
LoadType.PREPEND -> {
blogs.map {
val databaseid = getFirstBlogDatabaseId(state)?.databaseid?:0
movies.forEachIndexed{
index, blog ->
blog.databaseid = roomId - (movies.size -index.toLong())
}
}
}
LoadType.APPEND -> {
val roomId = getLastBlogDatabaseId(state)?.databaseid ?:0
blogs.forEachIndexed{
index, blog ->
blog.databaseid = roomId + index.toLong() + 1
}
}
LoadType.REFRESH -> {
blogs.forEachIndexed{
index, blog ->
blog.databaseid = index.toLong()
}
}
}
private fun getFirstBlogDatabaseId(state: PagingState<Int, Blog>): Blog? {
return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull()
}
private fun getLastBlogDatabaseId(state: PagingState<Int, Blog>): Blog? {
return state.lastItemOrNull()
}

Api call is not stopped calling again and again without any scrolling in Google Paging Library 3

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)
}
}

Categories

Resources