Android Paging 3 RemoteMediator does not go to the next page - android

I use RemoteMediator (Paging 3) to cache files. The problem is that when I scroll to the last item, does not go to the next page ... such problems with PagingSource did not have been
interface ImagesApi {
#GET(".?safesearch=true")
suspend fun searchImages(
#Query("q") query: String,
#Query("page") page: Int,
#Query("per_page") perPage: Int
): ImagesResponse
}
#Dao
interface ImageDao {
#Query("SELECT * FROM ${ImageEntity.TABLE_IMAGES}")
fun getImage(): PagingSource<Int,ImageEntity>
#Query("SELECT * FROM ${ImageEntity.TABLE_IMAGES}")
suspend fun getImageList(): List<ImageEntity>
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(images: List<ImageEntity>)
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertImage(image: ImageEntity)
#Delete
suspend fun deleteImage(image: ImageEntity)
#Query("DELETE FROM ${ImageEntity.TABLE_IMAGES}")
suspend fun clearAll()
}
#Dao
interface ImageRemoteKeysDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(remoteKey: List<ImageRemoteKeys>)
#Query("SELECT * FROM ${ImageRemoteKeys.IMAGE_REMOTE_KEY_TABLE} WHERE repoId = :repoId")
suspend fun remoteKeysById(repoId: Long): ImageRemoteKeys?
#Query("DELETE FROM ${ImageRemoteKeys.IMAGE_REMOTE_KEY_TABLE}")
suspend fun clearRemoteKeys()
}
#Entity(tableName = IMAGE_REMOTE_KEY_TABLE)
data class ImageRemoteKeys(
#PrimaryKey
val repoId: Long,
val prevKey: Int?,
val nextKey: Int?,
) {
companion object {
const val IMAGE_REMOTE_KEY_TABLE = "image_remote_key_table"
}
}
class ImageRemoteMediator(
private val service: ImagesApi,
private val query: String,
private val database: PixabayDb,
) : RemoteMediator<Int, ImageEntity>() {
private val imageDao = database.imageDao()
private val imageRemoteKeyDao = database.imageRemoteKeysDao()
override suspend fun initialize(): InitializeAction {
return InitializeAction.LAUNCH_INITIAL_REFRESH
}
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, ImageEntity>
): MediatorResult {
return try {
val page = when (loadType) {
LoadType.REFRESH -> {
val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
remoteKeys?.nextKey?.minus(1) ?: IMAGE_STARTING_PAGE_INDEX
}
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
}
}
val response = service.searchImages(query, page, state.config.pageSize)
val images = response.hits
val endPaginationReached = images.isEmpty()
database.withTransaction {
if (loadType == LoadType.REFRESH) {
imageRemoteKeyDao.clearRemoteKeys()
imageDao.clearAll()
}
val prevKey = if (page == IMAGE_STARTING_PAGE_INDEX) null else page - 1
val nextKey = if (endPaginationReached) null else page + 1
val keys = images.map {
ImageRemoteKeys(
repoId = it.id,
prevKey = prevKey,
nextKey = nextKey
)
}
Log.d(
"MEDIATOR",
"${images.size}, $prevKey, $nextKey ${state.config.pageSize}, ${keys.size}"
)
imageRemoteKeyDao.insertAll(keys)
imageDao.insertAll(images.map { it.toEntity() })
}
MediatorResult.Success(endOfPaginationReached = endPaginationReached)
} catch (e: IOException) {
MediatorResult.Error(e)
} catch (e: HttpException) {
MediatorResult.Error(e)
}
}
private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, ImageEntity>): ImageRemoteKeys? {
return state.pages.lastOrNull() { it.data.isNotEmpty() }?.data?.lastOrNull()
?.let { image ->
imageRemoteKeyDao.remoteKeysById(image.remoteId)
}
}
private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, ImageEntity>): ImageRemoteKeys? {
return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull()
?.let { image ->
imageRemoteKeyDao.remoteKeysById(image.remoteId)
}
}
private suspend fun getRemoteKeyClosestToCurrentPosition(state: PagingState<Int, ImageEntity>): ImageRemoteKeys? {
return state.anchorPosition?.let { position ->
state.closestItemToPosition(position)?.remoteId?.let { imageId ->
imageRemoteKeyDao.remoteKeysById(imageId)
}
}
}
}
class ImageRepository(
private val service: ImagesApi,
private val database: PixabayDb,
) {
#ExperimentalPagingApi
fun getSearchResult(query: String): Flow<PagingData<ImageEntity>> =
Pager(
config = PagingConfig(
pageSize = 200,
enablePlaceholders = false
),
remoteMediator = ImageRemoteMediator(
service,
query,
database
),
pagingSourceFactory = { database.imageDao().getImage() }
// pagingSourceFactory = { ImagePagingSource(service, query, database) }
).flow
}
class ImageViewModel(
private val repository: ImageRepository,
state: SavedStateHandle
) : ViewModel() {
private val currentQuery = state.getLiveData(LAST_SEARCH_QUERY, DEFAULT_QUERY)
var refreshInProgress = false
var pendingScrollToTopAfterRefresh = false
var newQueryInProgress = false
var pendingScrollToTopAfterNewQuery = false
#ExperimentalPagingApi
#ExperimentalCoroutinesApi
val images = currentQuery.asFlow().flatMapLatest { query ->
repository.getSearchResult(query)
}.cachedIn(viewModelScope)
fun searchImage(query: String) {
currentQuery.value = query
newQueryInProgress = true
pendingScrollToTopAfterNewQuery = true
}
private companion object {
const val LAST_SEARCH_QUERY = "current_query"
const val DEFAULT_QUERY = "dogs"
}
}
class ImagesFragment : BaseFragment(R.layout.fragment_images) {
private val binding by viewBinding(FragmentImagesBinding::bind)
private val viewModel by viewModel<ImageViewModel>()
override fun setRecyclerView(): RecyclerView = binding.recyclerViewImages
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val imageAdapter = ImagePagingAdapter(object : OnClickListener<ImageEntity> {
override fun click(item: ImageEntity) {
val direction =
ImagesFragmentDirections.actionPhotosFragmentToImageDetailFragment(item)
navController.navigate(direction)
}
})
binding.run {
searchInput.requestFocus()
searchInput.afterTextChanged(viewModel::searchImage)
imageAdapter.withLoadStateFooter(
LoadAdapter(imageAdapter::retry)
)
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
imageAdapter.loadStateFlow.collect { loadState ->
when (val refresh = loadState.mediator?.refresh) {
is LoadState.Loading -> {
textViewError.isVisible = false
buttonRetry.isVisible = false
textViewNoResults.isVisible = false
recyclerViewImages.showIfOrInvisible {
!viewModel.newQueryInProgress && imageAdapter.itemCount > 0
}
viewModel.refreshInProgress = true
viewModel.pendingScrollToTopAfterRefresh = true
}
is LoadState.NotLoading -> {
textViewError.isVisible = false
buttonRetry.isVisible = false
recyclerViewImages.isVisible = imageAdapter.itemCount > 0
val noResult =
imageAdapter.itemCount < 1 && loadState.append.endOfPaginationReached
&& loadState.source.append.endOfPaginationReached
textViewNoResults.isVisible = noResult
viewModel.refreshInProgress = false
viewModel.newQueryInProgress = false
}
is LoadState.Error -> {
textViewNoResults.isVisible = false
recyclerViewImages.isVisible = imageAdapter.itemCount > 0
val noCachedResults =
imageAdapter.itemCount < 1 && loadState.source.append.endOfPaginationReached
textViewError.isVisible = noCachedResults
buttonRetry.isVisible = noCachedResults
val errorMessage = getString(
R.string.could_not_load_search_results,
refresh.error.localizedMessage
?: getString(R.string.unknown_error_occurred)
)
textViewError.text = errorMessage
if (viewModel.refreshInProgress) {
showSnackbar(errorMessage)
}
viewModel.refreshInProgress = false
viewModel.newQueryInProgress = false
viewModel.pendingScrollToTopAfterRefresh = false
}
}
}
}
buttonRetry.setOnClickListener {
imageAdapter.retry()
}
}
setAdapter(imageAdapter)
lifecycleScope.launchWhenStarted {
viewModel.images.collectLatest { data ->
imageAdapter.submitData(data)
}
}
}
}

Related

Data From API Has Not Appeared Using Paging 3

I'm learning paging 3, but the data from the API doesn't appear. My code is like below:
interface PokeAPI {
#GET("pokemon")
fun getPokemonList() : Call<PokemonList>
#GET("pokemon")
fun getAllPokemon(
#Query("limit") limit: Int,
#Query("offset") offset: Int) : PokemonList
#GET("pokemon/{name}")
fun getPokemonInfo(
#Path("name") name: String
) : Call<Pokemon>
}
class PokePagingSource(private val apiService: PokeAPI): PagingSource<Int, Result>() {
private companion object {
const val INITIAL_PAGE_INDEX = 1
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Result> {
return try {
val position = params.key ?: INITIAL_PAGE_INDEX
val responseData = apiService.getAllPokemon(position, params.loadSize)
if (responseData.results.isEmpty()) {
Log.e("Response Succeed!", responseData.results.toString())
} else {
Log.e("Response Failed!", responseData.results.toString())
}
LoadResult.Page(
data = responseData.results,
prevKey = if (position == INITIAL_PAGE_INDEX) null else position - 1,
nextKey = if (responseData.results.isNullOrEmpty()) null else position + 1
)
} catch (exception: Exception) {
return LoadResult.Error(exception)
}
}
override fun getRefreshKey(state: PagingState<Int, Result>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}
class PokemonRepository(private val apiService: PokeAPI) {
fun getAllPokemon(): LiveData<PagingData<Result>>{
return Pager(
config = PagingConfig(
pageSize = 10
),
pagingSourceFactory = {
PokePagingSource(apiService)
}
).liveData
}
}
object Injection {
private val api by lazy { RetrofitClient().endpoint }
fun provideRepository(): PokemonRepository {
return PokemonRepository(api)
}
}
class PokemonViewModel(pokemonRepository: PokemonRepository) : ViewModel() {
val allPokemonList: LiveData<PagingData<Result>> =
pokemonRepository.getAllPokemon().cachedIn(viewModelScope)
}
class ViewModelFactory : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(PokemonViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return PokemonViewModel(Injection.provideRepository()) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
`class PokemonPagingAdapter(private val context: Context) :
PagingDataAdapter<Result, PokemonPagingAdapter.ViewHolder>(DIFF_CALLBACK) {
private var onItemClick: OnAdapterListener? = null
fun setOnItemClick(onItemClick: OnAdapterListener) {
this.onItemClick = onItemClick
}
class ViewHolder(val binding: AdapterPokemonBinding) : RecyclerView.ViewHolder(binding.root) {
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
AdapterPokemonBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val pokemonData = getItem(position)
if (pokemonData != null) {
holder.binding.apply {
val number = if (pokemonData.url.endsWith("/")) {
pokemonData.url.dropLast(1).takeLastWhile { it.isDigit() }
} else {
pokemonData.url.takeLastWhile { it.isDigit() }
}
val url = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${number}.png"
Glide.with(context)
.load(url)
.transition(DrawableTransitionOptions.withCrossFade())
.centerCrop()
.circleCrop()
.into(ivPokemon)
tvNamePokemon.text = pokemonData.name
btnDetail.setOnClickListener {
onItemClick?.onClick(pokemonData, pokemonData.name, url)
}
}
}
}
companion object {
val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Result>() {
override fun areItemsTheSame(
oldItem: Result,
newItem: Result
): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(
oldItem: Result,
newItem: Result
): Boolean {
return oldItem.name == newItem.name
}
}
}
interface OnAdapterListener {
fun onClick(data: Result, name: String, url: String)
}
}`
class FragmentPokemon: Fragment(R.layout.fragment_pokemon) {
private var _binding : FragmentPokemonBinding? = null
private val binding get() = _binding!!
private lateinit var dataPagingAdapter: PokemonPagingAdapter
private val viewModel: PokemonViewModel by viewModels {
ViewModelFactory()
}
private lateinit var comm: Communicator
override fun onStart() {
super.onStart()
getData()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentPokemonBinding.bind(view)
val toolBar = requireActivity().findViewById<View>(R.id.tool_bar)
toolBar.visibility = View.VISIBLE
val navBar = requireActivity().findViewById<BottomNavigationView>(R.id.bottom_navigation)
navBar.visibility = View.VISIBLE
comm = requireActivity() as Communicator
setupListPokemon()
}
private fun setupListPokemon(){
dataPagingAdapter = PokemonPagingAdapter(requireContext())
dataPagingAdapter.setOnItemClick(object: PokemonPagingAdapter.OnAdapterListener{
override fun onClick(data: Result, name: String, url: String) {
comm.passDataCom(name, url)
}
})
binding.apply {
rvPokemon.layoutManager = LinearLayoutManager(context)
rvPokemon.setHasFixedSize(true)
rvPokemon.adapter = dataPagingAdapter
}
}
private fun getData(){
viewModel.allPokemonList.observe(viewLifecycleOwner){
dataPagingAdapter.submitData(lifecycle, it)
binding.btnCoba.setOnClickListener { btn ->
if (it == null){
Log.e("ResponseFailed", it.toString())
} else Log.e("ResponseSucceed", it.toString())
}
}
}
}
What's the reason? I have followed the step by step implementation of paging 3 but the data still doesn't appear either.
I don't know the API you are using, but it seems that you are using it incorrectly. The getAllPokemon method has limit and offset parameters and you are calling it like apiService.getAllPokemon(position, params.loadSize), so you are using position as a limit and params.loadSize as an offset.
You should pass params.loadSize as a limit, rename INITIAL_PAGE_INDEX to INITIAL_OFFSET and set it to 0, since your API uses offsets instead of pages (at least it seems so from what you provided). The load function should then look something like this:
// get current offset
val offset = params.key ?: INITIAL_OFFSET
val responseData = apiService.getAllPokemon(limit = params.loadSize, offset = offset)
val prevKey = offset - params.loadSize
val nextKey = offset + params.loadSize

Remote meditor notworking when return MediatorResult.Success

I'm using paging 3 to load some data in recycler view. I fetch data from the server and then store them in my local database. so here is my DAO interface:
#Dao
interface BarberDAO {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(movies: List<Barber>)
#Query("SELECT * FROM barbershop ORDER BY id ASC")
fun selectAll(): PagingSource<Int, Barber>
#Query("DELETE FROM barbershop")
fun clearBarber()
}
I used a remote mediator like this
#ExperimentalPagingApi
class BarberRxRemoteMediator #Inject constructor(
private val main: MainApi,
private val database: BarberDatabase
) : RxRemoteMediator<Int, Barber>() {
override fun loadSingle(
loadType: LoadType,
state: PagingState<Int, Barber>
): Single<MediatorResult> {
return Single.just(loadType)
.subscribeOn(Schedulers.io())
.map {
when (it) {
LoadType.REFRESH -> {
val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
remoteKeys?.nextKey?.minus(1) ?: 1
}
LoadType.PREPEND -> {
Log.d("RemoteMediator:", "PREPEND")
val remoteKeys = getRemoteKeyForFistItem(state)
?: throw InvalidObjectException("Result is empty")
remoteKeys.prevKey ?: INVALID_PAGE
}
LoadType.APPEND -> {
Log.d("RemoteMediator:", "APPEND")
val remoteKeys = getRemoteKeyForLastItem(state)
?: throw InvalidObjectException("Result is empty")
remoteKeys.nextKey ?: INVALID_PAGE
}
}
}
.flatMap { page ->
if (page == INVALID_PAGE) {
Single.just(MediatorResult.Success(endOfPaginationReached = true))
} else {
main.barberShops(page)
.map { insertToDB(page, loadType, it) }
.map<MediatorResult> {
Log.d("RemoteMediator:", "MediatorResult.Success:"+ it.isEmpty())
MediatorResult.Success(endOfPaginationReached = it.isEmpty())
}
.onErrorReturn {
Log.d("RemoteMediator:", " MediatorResult.Error:$it")
MediatorResult.Error(it)
}
}
}
.onErrorReturn {
Log.d("RemoteMediator:", " MediatorResult.Error:$it")
MediatorResult.Error(it)
}
}
private fun insertToDB(
page: Int,
loadType: LoadType,
data: List<Barber>
): List<Barber> {
database.runInTransaction {
if (loadType == LoadType.REFRESH) {
database.remoteKeyDAO().clearRemoteKeys()
database.barberDao().clearBarber()
}
val prevKey = if (page == 1) null else page - 1
val nextKey = if (data.isEmpty()) null else page + 1
val keys = data.map {
RemoteKey(
id = it.id,
prevKey = prevKey,
nextKey = nextKey
)
}
database.remoteKeyDAO().insertAll(keys)
database.barberDao().insertAll(data)
Log.d("RemoteMediator:", "Request api và lưu dữ liệu vào SQLite. Data :$keys")
}
return data
}
private fun getRemoteKeyForFistItem(state: PagingState<Int, Barber>): RemoteKey? {
return state.pages.firstOrNull { it.data.isNotEmpty() }
?.data?.firstOrNull()
?.let { repo ->
database.remoteKeyDAO().remoteKeysByMovieId(repo.id)
}
}
private fun getRemoteKeyForLastItem(state: PagingState<Int, Barber>): RemoteKey? {
return state.pages.lastOrNull { it.data.isNotEmpty() }
?.data?.lastOrNull()
?.let { repo ->
database.remoteKeyDAO().remoteKeysByMovieId(repo.id)
}
}
private fun getRemoteKeyClosestToCurrentPosition(state: PagingState<Int, Barber>): RemoteKey? {
return state.anchorPosition?.let { position ->
state.closestItemToPosition(position)?.id?.let { id ->
database.remoteKeyDAO().remoteKeysByMovieId(id)
}
}
}
companion object {
const val INVALID_PAGE = -1
}
}
and use the data using my view model class like this:
#ExperimentalPagingApi
fun getBarberTest(): Observable<PagingData<Barber>> {
return barberRepository.getBarber().cachedIn(viewModelScope)
}
and I've got this chunk of code in my fragment:
mDisposable.add(viewModel.getBarberTest().subscribe {
adapter.submitData(lifecycle, it)
})
Update:
D/loadState: CombinedLoadStates(refresh=NotLoading(endOfPaginationReached=false), prepend=NotLoading(endOfPaginationReached=true), append=NotLoading(endOfPaginationReached=false), source=LoadStates(refresh=NotLoading(endOfPaginationReached=false), prepend=NotLoading(endOfPaginationReached=true), append=NotLoading(endOfPaginationReached=true)), mediator=LoadStates(refresh=NotLoading(endOfPaginationReached=false), prepend=NotLoading(endOfPaginationReached=true), append=NotLoading(endOfPaginationReached=false)))
When I debug and see that when the first paging data loads successfully and returns MediatorResult.Success(endOfPaginationReached = false) . paging doesn't refresh the data hence causing LoadType.APPEND error. I really don't know what I did wrong. hope all help.Sorry for my bad English

Why my PagingSource doesn't give me any data?

The Elements of the project that don't work
And I check if data is no null and do default submitList in the fragment.
Btw here is the link to the documentation
SearchPagingSource
These logs aren't even shown
class SearchPagingSource(
private val api: Api,
private val query: String
) : PagingSource<Int, Image>
() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Image> {
val position = params.key ?: 0
Log.d("SUPERTAG", "response")
return try {
val response =
api.search(query, position, params.loadSize, Locale.getDefault().language)
val list = ArrayList<Image>()
response.gif.forEach {
list.add(it.image)
}
Log.d("SUPERTAG", "response: $list")
LoadResult.Page(
data = list,
prevKey = null,
nextKey = if (list.isEmpty()) null else position + 15
)
} catch (e: IOException) {
// no connection
Log.d("SUPERTAG", "IOException: ${e.message}")
LoadResult.Error(e)
} catch (e: HttpException) {
// error loading
Log.d("SUPERTAG", "HttpException: ${e.message}")
LoadResult.Error(e)
}
}
}
ViewModel
Null because of the null that returned by the repository.
class SearchViewModel : ViewModel() {
private val searchRepository = SearchRepository.getInstance()
private val _query = MutableLiveData<String>()
private val _results = _query.map { data ->
searchRepository.search(data).value
}
val results = _results
private val _error = MutableLiveData<String>()
val error: LiveData<String> = _error
#SuppressLint("StaticFieldLeak")
private lateinit var progressBar: ProgressBar
fun initProgress(progress: ProgressBar) {
progressBar = progress
}
fun search(query: String, errorLoading: String) {
viewModelScope.launch {
try {
progressBar.visibility = View.VISIBLE
_query.value = query
Log.d("SUPERTAG", "result2: ${searchRepository.search(_query.value!!).value}")
progressBar.visibility = View.GONE
} catch (e: Exception) {
_error.value = e.message
}
}
}
}
Repository
Exactly this part of the code returns null, I checked It by logs. I guess I do smth wrong with parameters or in general.
object SearchRepository {
private lateinit var instance: SearchRepository
private val app: App by lazy {
App().getInstance()
}
fun getInstance(): SearchRepository {
instance = this
app.initRetrofit()
return instance
}
fun search(query: String) = Pager(
config = PagingConfig(
15,
maxSize = 50,
enablePlaceholders = false
),
pagingSourceFactory = {
SearchPagingSource(app.api, query)
}
).liveData
}
If I do like this, I get at least snackbar and an error. Usually it shows nothing and even no progressBar.
So, If I add jumpThreshold = 0, I get a snackbar with an error that I don't have usually.
fun search(query: String) = Pager(
config = PagingConfig(
pageSize = 15,
jumpThreshold = 0
),
pagingSourceFactory = {
SearchPagingSource(app.api, query)
}
).liveData
Edit
So, I did it with flow and it works a bit, but Im still not getting a list in my recycler.
Repository
fun getListData(query: String): Flow<PagingData<Image>> {
return Pager(
config = PagingConfig(
pageSize = 15,
maxSize = 50,
enablePlaceholders = true
), pagingSourceFactory = {
SearchPagingSource(query = query, api = app.api)
}).flow
}
ViewModel
fun search(query: String){
viewModelScope.launch {
try {
searchRepository.getListData(query).collectLatest {
it.map {
Log.d("SUPERTAG", "image $it")
}
Log.d("SUPERTAG", it.toString())
_results.value = it
}
} catch (e: Exception){
_error.value = e.message
}
}
}
The answer was in the adapter!
class SearchAdapter(
diffCallback: DiffUtil.ItemCallback<Image>,
private val clickListener: (url: String) -> Unit
) : PagingDataAdapter<Image, SearchAdapterViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchAdapterViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = inflater.inflate(R.layout.list_item, parent, false)
return SearchAdapterViewHolder(view)
}
override fun onBindViewHolder(holder: SearchAdapterViewHolder, position: Int) {
val item = getItem(position)
if(item != null){
holder.imageView.setOnClickListener {
clickListener(item.original.url)
}
holder.bind(item)
}
}
}

How to reset the scroll position after process death when using Paging 3 with RemoteMediator

I have set up Paging 3 with offline caching using a RemoteMediator. After process death, the RecyclerView immediately restores the correct scrolling position. However, since we need to send the search query again it triggers a LoadType.REFRESH which clears the current search results from the cache and replaces them with new values. This brings us back to the start of the list.
My RemoteMediator:
private const val NEWS_STARTING_PAGE_INDEX = 1
class SearchNewsRemoteMediator(
private val searchQuery: String,
private val newsDb: NewsArticleDatabase,
private val newsApi: NewsApi
) : RemoteMediator<Int, NewsArticle>() {
private val newsArticleDao = newsDb.newsArticleDao()
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, NewsArticle>
): MediatorResult {
val page = when (loadType) {
LoadType.REFRESH -> {
val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
remoteKeys?.nextKey?.minus(1) ?: NEWS_STARTING_PAGE_INDEX
}
LoadType.PREPEND -> {
val remoteKeys = getRemoteKeyForFirstItem(state)
?: throw InvalidObjectException("Remote key should not be null for $loadType")
val prevKey = remoteKeys.prevKey
if (prevKey == null) {
return MediatorResult.Success(endOfPaginationReached = true)
}
remoteKeys.prevKey
}
LoadType.APPEND -> {
val remoteKeys = getRemoteKeyForLastItem(state)
if (remoteKeys == null || remoteKeys.nextKey == null) {
throw InvalidObjectException("Remote key should not be null for $loadType")
}
remoteKeys.nextKey
}
}
return try {
delay(2000)
val apiResponse = newsApi.searchNews(searchQuery, page, state.config.pageSize)
val serverSearchResults = apiResponse.articles
val endOfPaginationReached = serverSearchResults.isEmpty()
val bookmarkedArticles = newsArticleDao.getAllBookmarkedArticles().first()
val cachedBreakingNewsArticles = newsArticleDao.getCachedBreakingNews().first()
val searchResults = serverSearchResults.map { serverSearchResultArticle ->
val bookmarked = bookmarkedArticles.any { bookmarkedArticle ->
bookmarkedArticle.url == serverSearchResultArticle.url
}
val inBreakingNewsCache =
cachedBreakingNewsArticles.any { breakingNewsArticle ->
breakingNewsArticle.url == serverSearchResultArticle.url
}
NewsArticle(
title = serverSearchResultArticle.title,
url = serverSearchResultArticle.url,
urlToImage = serverSearchResultArticle.urlToImage,
isBreakingNews = inBreakingNewsCache,
isBookmarked = bookmarked,
isSearchResult = true
)
}
newsDb.withTransaction {
if (loadType == LoadType.REFRESH) {
newsDb.searchRemoteKeyDao().clearRemoteKeys()
newsArticleDao.resetSearchResults()
newsArticleDao.deleteAllObsoleteArticles()
}
val prevKey = if (page == NEWS_STARTING_PAGE_INDEX) null else page - 1
val nextKey = if (endOfPaginationReached) null else page + 1
val remoteKeys = serverSearchResults.map { article ->
SearchRemoteKeys(article.url, prevKey, nextKey)
}
newsDb.searchRemoteKeyDao().insertAll(remoteKeys)
newsDb.newsArticleDao().insertAll(searchResults)
}
MediatorResult.Success(endOfPaginationReached)
} catch (exception: IOException) {
MediatorResult.Error(exception)
} catch (exception: HttpException) {
MediatorResult.Error(exception)
}
}
private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, NewsArticle>): SearchRemoteKeys? =
state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull()
?.let { article ->
newsDb.searchRemoteKeyDao().getRemoteKeyFromArticleUrl(article.url)
}
private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, NewsArticle>): SearchRemoteKeys? =
state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull()
?.let { article ->
newsDb.searchRemoteKeyDao().getRemoteKeyFromArticleUrl(article.url)
}
private suspend fun getRemoteKeyClosestToCurrentPosition(
state: PagingState<Int, NewsArticle>
): SearchRemoteKeys? =
state.anchorPosition?.let { position ->
state.closestItemToPosition(position)?.url?.let { articleUrl ->
newsDb.searchRemoteKeyDao().getRemoteKeyFromArticleUrl(articleUrl)
}
}
}
The repository method that instantiates it:
fun getSearchResults(query: String): Flow<PagingData<NewsArticle>> =
Pager(
config = PagingConfig(pageSize = 20, enablePlaceholders = false),
remoteMediator = SearchNewsRemoteMediator(query, newsArticleDatabase, newsApi),
pagingSourceFactory = { newsArticleDatabase.newsArticleDao().getSearchResultsPaged() }
).flow
The ViewModel that triggers the query. currentQuery is restored after process death and therefore calls getSearchResults immediately with the old query.
class SearchNewsViewModel #ViewModelInject constructor(
private val repository: NewsRepository,
#Assisted state: SavedStateHandle
) : ViewModel() {
private val currentQuery = state.getLiveData<String?>("currentQuery")
val newsArticles = currentQuery.switchMap { query ->
repository.getSearchResults(query).asLiveData().cachedIn(viewModelScope)
}
fun searchArticles(query: String) {
currentQuery.value = query
}
}

Android paging from local data (without room)

I'm trying to work with pagination library, to get list from server, and use it with local data, but i don't want to use room for it (don't have db in my app, and don't want to add it just for it),
so i have mediator, and i'm trying to implement PagingSource. the list should be flowable, so when i delete an item, it will update automatically.
mediator
class EventMediator(
private val id: String,
private val remoteDataSource: EventRemote,
private val eventLocalData: EvrntsLocal
) : RemoteMediator<Int, EventItem>() {
var hasNextKey = true
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, EventItem>
): MediatorResult {
try {
val loadKey = when (loadType) {
LoadType.REFRESH -> STARTING_MEAL_INDEX
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
if (!eventLocalData.hasNextKey) {
return MediatorResult.Success(endOfPaginationReached = true)
}
eventLocalData.getNumOfMeals()
}
}
val response = remoteDataSource.getEvents(loadKey)
return if (response is Result.Success) {
hasNextKey = !response.data.lastPage
if (loadType == LoadType.REFRESH) {
eventLocalData.clearMeals()
}
eventLocalData.saveMeals(response.data.items)
MediatorResult.Success(endOfPaginationReached = !hasNextKey)
} else {
MediatorResult.Error(IOException("Failed to get Events"))
}
} catch (e: IOException) {
return MediatorResult.Error(e)
} catch (e: HttpException) {
return MediatorResult.Error(e)
}
}
}
EventSource:
class EventSource(
private val eventLocalData: EvrntsLocal
) : PagingSource<Int, EventItem>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, EventItem> {
val offset = (params.key ?: STARTING_MEAL_INDEX)
return try {
val response = eventLocalData.getMeals()
LoadResult.Page(
data = response,
prevKey = if (offset - NUM_OF_EVENTS <= STARTING_MEAL_INDEX) null else offset - NUM_OF_EVENTS,
nextKey = if (offset + NUM_OF_EVENTS >= response.size) null else offset + NUM_OF_EVENTS
)
} catch (exception: IOException) {
return LoadResult.Error(exception)
} catch (exception: HttpException) {
return LoadResult.Error(exception)
}
}
}
repository
fun getEvents(folderId: String): Flow<PagingData<EventItem>> {
return Pager(
config = PagingConfig(50),
remoteMediator = EventMediator(folderId, remoteDataSource, localDataSource),
pagingSourceFactory = { EventSource(localDataSource) }
) .flow
}
my local data:
class EvrntsLocal #Inject constructor(
) {
private val _eventChannel = ConflatedBroadcastChannel<List<EventItem>>(emptyList())
var hasNextKey: Boolean = true
fun observeMeals(): Flow<List<EventItem>> {
return _eventChannel.asFlow()
}
fun getMeals(): List<EventItem> {
return _eventChannel.value
}
fun saveMeals(list: List<EventItem>) {
_eventChannel.offer(_eventChannel.value.plus(list))
}
fun getNumOfMeals(): Int {
return _eventChannel.value.size
}
fun clearMeals() {
_eventChannel.offer(emptyList())
}
}

Categories

Resources