I'm using the newest Paging 3 library and trying to implement non-infinite paging by using PagingSource class which executes requests to my API.
What do I want to do?
When the user scrolls to the end of RecyclerView list instead of automatically getting the next chunk of data a next-button should be shown. By clicking on it PagingSource.load method should be invoked as if it would be in case of infinite scrolling.
This is how my PagingSource class looks like:
class LiveTickerPagingSource(
private val service: LiveTickerService
) : PagingSource<Int, LiveTickerEntry>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, LiveTickerEntry> {
val position = params.key ?: STARTING_PAGE_INDEX
return try {
val response = service.getEntries()//params.loadSize
val entries = response.items
LoadResult.Page(
data = entries,
prevKey = null,
nextKey = null
)
} catch (exception: IOException) {
LoadResult.Error(exception)
} catch (exception: HttpException) {
LoadResult.Error(exception)
}
}
}
ViewModel:
class LiveTickerModuleViewModel(private val repository: LiveTickerModuleRepository) : ViewModel() {
private var liveTickerResult: Flow<PagingData<LiveTickerEntry>>? = null
fun getEntries(): Flow<PagingData<LiveTickerEntry>> {
val newResult: Flow<PagingData<LiveTickerEntry>> = repository.getLiveTickerEntriesStream()
.cachedIn(viewModelScope)
liveTickerResult = newResult
return newResult
}
}
Repository:
class LiveTickerModuleRepository(private val service: LiveTickerModuleService) {
fun getLiveTickerEntriesStream(): Flow<PagingData<LiveTickerEntry>> {
return Pager(
config = PagingConfig(pageSize = NETWORK_PAGE_SIZE, initialLoadSize = NETWORK_PAGE_SIZE, enablePlaceholders = false),
pagingSourceFactory = { LiveTickerModulePagingSource(service) }
).flow
}
companion object {
private const val NETWORK_PAGE_SIZE = 3
}
}
And lastly Fragment:
class LiveTickerModuleFragment : Fragment() {
private lateinit var binding: FragmentLiveTickerModuleBinding
private lateinit var viewModel: LiveTickerModuleViewModel
private var liveTickerJob: Job? = null
private val adapter by lazy { LiveTickerModuleAdapter() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this, Injection.provideViewModelFactory())
.get(LiveTickerModuleViewModel::class.java)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentLiveTickerModuleBinding.inflate(layoutInflater)
initAdapter()
getLiveTickerEntries()
return binding.root
}
private fun getLiveTickerEntries() {
liveTickerJob?.cancel()
liveTickerJob = lifecycleScope.launch {
viewModel.getEntries().collectLatest {
adapter.submitData(it)
}
}
}
private fun initAdapter() {
binding.list.adapter = adapter
adapter.addLoadStateListener { loadState ->
val sourceAppend = loadState.source.append
val sourcePrepend = loadState.source.prepend
val errorState = sourceAppend as? LoadState.Error
?: sourcePrepend as? LoadState.Error
val endOfPaginationReached = errorState?.endOfPaginationReached
}
}
}
I was using this codelab as my reference, which is based on infinite scrolling. I'm thankful for any kind of explanation. It doesn't necessarily have to be code.
This is supported, but you may find Paging adds more complexity than necessary if you don't need incremental loading.
For a static list you can simply use:
PagingDataAdapter.submitData(PagingData.from(myList)
Otherwise, if you're loading from a resource, you can implement a PagingSource which returns a LoadResult.Page with nextKey and prevKey set to null. You'll need to construct a Pager, then observe the PagingData stream to submit to PagingDataAdapter with adapter.submitData(PagingData)
Related
I have an API which give me the list of doctors. On it's last page only 1 item is there and other items are null like this:
After this i have used paging library for pagination
my pagingSource code: `
class DocPagingSource(val docRepository: DocRepository): PagingSource<Int, Data>() {
override fun getRefreshKey(state: PagingState<Int, Data>): Int? {
return state.anchorPosition?.let {
state.closestPageToPosition(it)?.prevKey?.plus(1)
?: state.closestPageToPosition(it)?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Data> {
return try {
val currentPage = params.key?: 1
val city: String = ""
val response = docRepository.getDoctors(city, currentPage)
val page = Math.ceil(response.body()!!.total.toDouble()/5).toInt()
val data = response.body()!!.data
val responseData = mutableListOf<Data>()
responseData.addAll(data)
LoadResult.Page(
data = responseData,
prevKey = if(currentPage==1) null else -1,
nextKey = if (currentPage== page) null else currentPage.plus(1)
)
}catch (e: HttpException){
LoadResult.Error(e)
}catch (e: Exception){
LoadResult.Error(e)
}
}
`
My paging Adapter Code:
class DocAdapter(val context: Context): PagingDataAdapter<Data, DocAdapter.DocViewHolder>(DiffUtil()) {
private lateinit var binding: ItemDoctorsBinding
inner class DocViewHolder : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Data?) {
binding.apply {
txtDocCity.text = item?.city
txtDocName.text = item?.docName
txtDocFees.text = item?.docConsultationFee
txtDocYOE.text = item?.docYoE
txtDocSpecialisation.text = item?.docSpecialisation
Glide.with(context)
.load(item?.docProfileImgUrl)
.fitCenter()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(docPhoto)
}
}
}
override fun onBindViewHolder(holder: DocViewHolder, position: Int) {
val item = getItem(position)
if (item!=null){
holder.bind(getItem(position)!!)
holder.setIsRecyclable(false)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DocViewHolder {
val inflater = LayoutInflater.from(context)
binding = ItemDoctorsBinding.inflate(inflater, parent, false)
return DocViewHolder()
}
class DiffUtil: androidx.recyclerview.widget.DiffUtil.ItemCallback<Data>(){
override fun areItemsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem.docId == newItem.docId
}
override fun areContentsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem==newItem
}
}}
what I am getting after reaching my 16th item in doctor list on last page it should show entry till 16th item but after that it also shows again like this:
Also if i dont use holder.setIsRecyclable(false) in pagingAdapter then this android icon not shown but then list is populated with previous doctors:
on the top DR. Sixteen is shown like this:
and in between it again shows like this:
My doctorViewModel Class:
class DocViewModel(val repository: DocRepository): ViewModel() {
val loading = MutableLiveData<Boolean>()
val docList = Pager(PagingConfig(5, maxSize = 100)){
DocPagingSource(repository)
}.flow.cachedIn(viewModelScope)}
My main Activity:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var docViewModel: DocViewModel
private lateinit var docAdapter: DocAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val docListRepository = DocRepository()
val docFactory = DocViewModelFactory(docListRepository)
docViewModel = ViewModelProvider(this, docFactory).get(DocViewModel::class.java)
docAdapter = DocAdapter(this)
lifecycleScope.launchWhenCreated {
docViewModel.docList.collect{
docAdapter.submitData(it)
}
}
binding.docRecyclerView.apply {
layoutManager = LinearLayoutManager(this#MainActivity)
adapter = docAdapter
setHasFixedSize(true)
}
}}
I have solved this error by applying a condition in my paging source code
As a developer one needs to adapt to change, I read somewhere it says:
If you don’t choose the right architecture for your Android project, you will have a hard time maintaining it as your codebase grows and your team expands.
I wanted to implement Clean Architecture with MVVM
My app data flow will look like this:
Model class
data class Note(
val title: String? = null,
val timestamp: String? = null
)
Dtos
data class NoteRequest(
val title: String? = null,
val timestamp: String? = null
)
and
data class NoteResponse(
val id: String? = null,
val title: String? = null,
val timestamp: String? = null
)
Repository layer is
interface INoteRepository {
fun getNoteListSuccessListener(success: (List<NoteResponse>) -> Unit)
fun deleteNoteSuccessListener(success: (List<NoteResponse>) -> Unit)
fun getNoteList()
fun deleteNoteById(noteId: String)
}
NoteRepositoryImpl is:
class NoteRepositoryImpl: INoteRepository {
private val mFirebaseFirestore = Firebase.firestore
private val mNotesCollectionReference = mFirebaseFirestore.collection(COLLECTION_NOTES)
private val noteList = mutableListOf<NoteResponse>()
private var getNoteListSuccessListener: ((List<NoteResponse>) -> Unit)? = null
private var deleteNoteSuccessListener: ((List<NoteResponse>) -> Unit)? = null
override fun getNoteListSuccessListener(success: (List<NoteResponse>) -> Unit) {
getNoteListSuccessListener = success
}
override fun deleteNoteSuccessListener(success: (List<NoteResponse>) -> Unit) {
deleteNoteSuccessListener = success
}
override fun getNoteList() {
mNotesCollectionReference
.addSnapshotListener { value, _ ->
noteList.clear()
if (value != null) {
for (item in value) {
noteList
.add(item.toNoteResponse())
}
getNoteListSuccessListener?.invoke(noteList)
}
Log.e("NOTE_REPO", "$noteList")
}
}
override fun deleteNoteById(noteId: String) {
mNotesCollectionReference.document(noteId)
.delete()
.addOnSuccessListener {
deleteNoteSuccessListener?.invoke(noteList)
}
}
}
ViewModel layer is:
interface INoteViewModel {
val noteListStateFlow: StateFlow<List<NoteResponse>>
val noteDeletedStateFlow: StateFlow<List<NoteResponse>>
fun getNoteList()
fun deleteNoteById(noteId: String)
}
NoteViewModelImpl is:
class NoteViewModelImpl: ViewModel(), INoteViewModel {
private val mNoteRepository: INoteRepository = NoteRepositoryImpl()
private val _noteListStateFlow = MutableStateFlow<List<NoteResponse>>(mutableListOf())
override val noteListStateFlow: StateFlow<List<NoteResponse>>
get() = _noteListStateFlow.asStateFlow()
private val _noteDeletedStateFlow = MutableStateFlow<List<NoteResponse>>(mutableListOf())
override val noteDeletedStateFlow: StateFlow<List<NoteResponse>>
get() = _noteDeletedStateFlow.asStateFlow()
init {
// getNoteListSuccessListener
mNoteRepository
.getNoteListSuccessListener {
viewModelScope
.launch {
_noteListStateFlow.emit(it)
Log.e("NOTE_G_VM", "$it")
}
}
// deleteNoteSuccessListener
mNoteRepository
.deleteNoteSuccessListener {
viewModelScope
.launch {
_noteDeletedStateFlow.emit(it)
Log.e("NOTE_D_VM", "$it")
}
}
}
override fun getNoteList() {
// Get all notes
mNoteRepository.getNoteList()
}
override fun deleteNoteById(noteId: String) {
mNoteRepository.deleteNoteById(noteId = noteId)
}
}
and last but not least Fragment is:
class HomeFragment : Fragment() {
private lateinit var binding: FragmentHomeBinding
private val viewModel: INoteViewModel by viewModels<NoteViewModelImpl>()
private lateinit var adapter: NoteAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val recyclerView = binding.recyclerViewNotes
recyclerView.addOnScrollListener(
ExFABScrollListener(binding.fab)
)
adapter = NoteAdapter{itemView, noteId ->
if (noteId != null) {
showMenu(itemView, noteId)
}
}
recyclerView.adapter = adapter
// initView()
fetchFirestoreData()
binding.fab.setOnClickListener {
val action = HomeFragmentDirections.actionFirstFragmentToSecondFragment()
findNavController().navigate(action)
}
}
private fun fetchFirestoreData() {
// Get note list
viewModel
.getNoteList()
// Create list object
val noteList:MutableList<NoteResponse> = mutableListOf()
// Impose StateFlow
viewModel
.noteListStateFlow
.onEach { data ->
data.forEach {noteResponse ->
noteList.add(noteResponse)
adapter.submitList(noteList)
Log.e("NOTE_H_FRAG", "$noteResponse")
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
}
//In the showMenu function from the previous example:
#SuppressLint("RestrictedApi")
private fun showMenu(v: View, noteId: String) {
val menuBuilder = MenuBuilder(requireContext())
SupportMenuInflater(requireContext()).inflate(R.menu.menu_note_options, menuBuilder)
menuBuilder.setCallback(object : MenuBuilder.Callback {
override fun onMenuItemSelected(menu: MenuBuilder, item: MenuItem): Boolean {
return when(item.itemId){
R.id.option_edit -> {
val action = HomeFragmentDirections.actionFirstFragmentToSecondFragment(noteId = noteId)
findNavController().navigate(action)
true
}
R.id.option_delete -> {
viewModel
.deleteNoteById(noteId = noteId)
// Create list object
val noteList:MutableList<NoteResponse> = mutableListOf()
viewModel
.noteDeletedStateFlow
.onEach {data ->
data.forEach {noteResponse ->
noteList.add(noteResponse)
adapter.submitList(noteList)
Log.e("NOTE_H_FRAG", "$noteResponse")
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
true
} else -> false
}
}
override fun onMenuModeChange(menu: MenuBuilder) {}
})
val menuHelper = MenuPopupHelper(requireContext(), menuBuilder, v)
menuHelper.setForceShowIcon(true) // show icons!!!!!!!!
menuHelper.show()
}
}
With all the above logic I'm facing TWO issues
issue - 1
As mentioned here, I have added SnapshotListener on collection as:
override fun getNoteList() {
mNotesCollectionReference
.addSnapshotListener { value, _ ->
noteList.clear()
if (value != null) {
for (item in value) {
noteList
.add(item.toNoteResponse())
}
getNoteListSuccessListener?.invoke(noteList)
}
Log.e("NOTE_REPO", "$noteList")
}
}
with it if I change values of a document from Firebase Console, I get updated values in Repository and ViewModel, but list of notes is not being updated which is passed to adapter, so all the items are same.
issue - 2
If I delete any item from list/recyclerview using:
R.id.option_delete -> {
viewModel
.deleteNoteById(noteId = noteId)
// Create list object
val noteList:MutableList<NoteResponse> = mutableListOf()
viewModel
.noteDeletedStateFlow
.onEach {data ->
data.forEach {noteResponse ->
noteList.add(noteResponse)
adapter.submitList(noteList)
Log.e("NOTE_H_FRAG", "$noteResponse")
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
still I get updated list(i.e new list of notes excluding deleted note) in Repository and ViewModel, but list of notes is not being updated which is passed to adapter, so all the items are same, no and exclusion of deleted item.
Question Where exactly I'm making mistake to initialize/update adapter? because ViewModel and Repository are working fine.
Make following changes:
In init{} block of NoteViewModelImpl :
// getNoteListSuccessListener
mNoteRepository
.getNoteListSuccessListener{noteResponseList ->
viewModelScope.launch{
_noteListStateFlow.emit(it.toList())
}
}
you must add .toList() if you want to emit list in StateFlow to get notified about updates, and in HomeFragment
private fun fetchFirestoreData() {
// Get note list
viewModel
.getNoteList()
// Impose StateFlow
lifecycleScope.launch {
viewModel.noteListStateFlow.collect { list ->
adapter.submitList(list.toMutableList())
}
}
}
That's it, I hope it works fine.
Try to remove additional lists of items in the fetchFirestoreData() and showMenu() (for item R.id.option_delete) methods of the HomeFragment fragment and see if it works:
// remove `val noteList:MutableList<NoteResponse>` in `fetchFirestoreData()` method
private fun fetchFirestoreData() {
...
// remove this line
val noteList:MutableList<NoteResponse> = mutableListOf()
// Impose StateFlow
viewModel
.noteListStateFlow
.onEach { data ->
adapter.submitList(data)
}.launchIn(viewLifecycleOwner.lifecycleScope)
}
And the same for the delete menu item (R.id.option_delete).
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)
}
}
}
}
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()
}
I'm using the Android Paging Library and I am having trouble getting results back from my DataSource.
I am basically getting an empty LiveData from my LivePagedListBuilder. Not sure what I am doing wrong. My loadInitial is also never called.
Here is my ViewModel:
class WorkPackagesViewModel: ViewModel() {
companion object {
const val PAGING_LIMIT_DEFAULT = 10
}
var workPackages: LiveData<PagedList<WorkPackagesQuery.WorkPackage>>? = null
fun fetchWorkPackages(isOnline: Boolean, workWeeK: Configurations.AppWeek, pagingStart: Int?, pagingLimit: Int?) {
val myConfig = PagedList.Config.Builder()
.setInitialLoadSizeHint(pagingLimit ?: PAGING_LIMIT_DEFAULT)
.setPageSize(pagingLimit ?: PAGING_LIMIT_DEFAULT )
.build()
val workPackageDataFactory = WorkPackageDataFactory(workWeeK)
workPackageDataFactory.create()
workPackages = LivePagedListBuilder(workPackageDataFactory, myConfig)
.setInitialLoadKey(pagingStart)
.setFetchExecutor(Executors.newFixedThreadPool(5))
.build()
}
}
}
Here is my DataSource.Factory:
class WorkPackageDataFactory(
private val workWeek : Configurations.AppWeek
) : DataSource.Factory<Int, WorkPackagesQuery.WorkPackage>() {
private var mutableLiveData: MutableLiveData<WorkPackageDataSource>? = MutableLiveData()
override fun create(): DataSource<Int, WorkPackagesQuery.WorkPackage> {
val workPackageDataSource = WorkPackageDataSource(workWeek)
mutableLiveData?.postValue(workPackageDataSource)
return workPackageDataSource
}
}
Here is my datasource file:
class WorkPackageDataSource(
private val workWeek : Configurations.AppWeek
) : PageKeyedDataSource<Int, WorkPackagesQuery.WorkPackage>(), KoinComponent
{
val client : PipefighterApi by inject()
private var parentJob = Job()
private val coroutineContext: CoroutineContext get() = parentJob + Dispatchers.Main
private val scope = CoroutineScope(coroutineContext)
override fun loadInitial( //DOES NOT GET CALLED
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, WorkPackagesQuery.WorkPackage>
) {
scope.launch {
val workPackages = getWorkPackages(1,params.requestedLoadSize)?.workPackages() as MutableList<WorkPackagesQuery.WorkPackage>
callback.onResult(workPackages, null, params.requestedLoadSize + 1 )
}
}
override fun loadAfter(
params: LoadParams<Int>,
callback: LoadCallback<Int, WorkPackagesQuery.WorkPackage>
) {
scope.launch {
val workPackages = getWorkPackages(params.key,params.requestedLoadSize)?.workPackages() as MutableList<WorkPackagesQuery.WorkPackage>
var nextKey : Int? = null
if (params.key.plus(0) != workPackages.size) {
nextKey = params.key.plus(1)
}
callback.onResult(workPackages, nextKey )
}
}
override fun loadBefore(
params: LoadParams<Int>,
callback: LoadCallback<Int, WorkPackagesQuery.WorkPackage>
) {
}
suspend fun getWorkPackages(
initialPage : Int,
requestedLoadSize : Int
) : WorkPackagesQuery.Result? {
return withContext(Dispatchers.IO) {
async { client.workPackages(
workWeek.currentWeekStart(),
workWeek.currentWeekEnd(),
initialPage,
requestedLoadSize
) }.await().response
}
}
}
Here is my fragment
class WorkPackagesFragment : Fragment(), WorkPackagesRecyclerAdapter.OnClickWorkPackage {
companion object {
private const val VERTICAL_ITEM_SPACE = 30
const val PAGING_START = 1
const val PAGING_LIMIT = 10
}
private val workPackagesViewModel: WorkPackagesViewModel by viewModel()
private val mainViewModel : MainViewModel by sharedViewModel()
private val networkConnection: NetworkConnectionHelper by inject()
private lateinit var binding: FragmentWorkPackagesBinding
private lateinit var adapter: WorkPackagesRecyclerAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
com.bechtel.pf.R.layout.fragment_work_packages,
container,
false
)
mainViewModel.week.observe(this, Observer {
it ?: return#Observer
workPackagesViewModel.fetchWorkPackages(networkConnection.connected(), it, PAGING_START, PAGING_LIMIT)
})
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = workPackagesViewModel
workPackagesViewModel.workPackages?.observe(this, Observer {
it ?: return#Observer
adapter = WorkPackagesRecyclerAdapter(this)
adapter.submitList(it)
binding.workPackagesRecyclerView.adapter = adapter
adapter.notifyDataSetChanged()
})
return binding.root
}
}
I don't see where workPackages is observed, but I guess the problem is that you observe workPackages BEFORE you call fetchWorkPackages(). When fetchWorkPackages() is called, it creates a new LiveData and assigns the new one to workPackages. But you only observe to the old LiveData in workPackages before, so this new one doesn't have any active observers and therefore it never trigger loadInitial()
You can try to put fetchWorkPackages() in init {} in ViewModel, or set observe for workPackages AFTER calling fetchWorkPackages() in your Activity or Fragment