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())
}
}
Related
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)
}
}
}
}
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
I am creating an app in which i am using RxJava and paging 3 library.
I am using RxPagingSource and Retrofit to paginate response coming from Server
but i have to fetch some data from Firestore and have to paginate
override fun loadSingle(params: LoadParams<QuerySnapshot>): Single<LoadResult<QuerySnapshot, Notification>> {
var currentPage : QuerySnapshot
if(params.key != null){
currentPage = params.key!!
}else{
reference
.limit(10)
.get()
.addOnCompleteListener(OnCompleteListener {
if(it.isSuccessful){
currentPage = it.result
val lastDocumentSnapshot = it.result.documents[it.result.size() - 1]
reference
.limit(10)
.startAfter(lastDocumentSnapshot)
.get()
.addOnCompleteListener(OnCompleteListener {
val nextPage: QuerySnapshot
if(it.isSuccessful){
nextPage = it.result
return Single.just(
LoadResult.Page(
data = currentPage.toObjects(Notification::class.java),
prevKey = null,
nextKey = nextPage
)
)
//return
}
})
}
})
}
This is code i tried but its not working for me, there is many mistake in this code
How can i paginate Firestore data using RxPagingSource provided by Paging 3 library
PagingDataSource.kt
class NotificationPagingDataSource(val service: AppRepository) :
RxPagingSource<Int, Notification>() {
override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, Notification>> {
val page = params.key ?: 1
return service.getFireNotification(page)
.subscribeOn(Schedulers.io())
.map {
toLoadResult(it, page)
}
.onErrorReturn {
LoadResult.Error(it)
}
}
private fun toLoadResult(
data: QuerySnapshot,
page: Int
): LoadResult<Int, Notification> {
Log.i("TAG"," mappingstarted::: 3")
return LoadResult.Page(
data = data.toObjects(Notification::class.java),
prevKey = if (page <= 1) null else page - 1,
nextKey = if (data.size() == 0) null else page + 1
)
}
override fun getRefreshKey(state: PagingState<Int, Notification>): Int? {
return state.anchorPosition
}
}
AppRepository.kt
var timestamp : String? = null
fun getFireNotification(i: Int): Single<QuerySnapshot> {
return Single.create<QuerySnapshot>(SingleOnSubscribe { emitter ->
if (i == 1) {
FirebaseFirestore.getInstance()
.collection(Constant.NOTIFICATION_NODE).document(FirebaseAuth.getInstance().currentUser.uid)
.collection(Constant.USER_NOTIFICATION_NODE)
.limit(15)
.get()
.addOnCompleteListener {
if (it.isSuccessful) {
emitter.onSuccess(it.result)
timestamp = it.result.documents.last().data?.get("timestamp").toString()
}
}
.addOnFailureListener {
Log.i("TAG"," addOnFailureListener:: $it")
}
} else {
FirebaseFirestore.getInstance()
.collection(Constant.NOTIFICATION_NODE).document(FirebaseAuth.getInstance().currentUser.uid)
.collection(Constant.USER_NOTIFICATION_NODE)
.orderBy("timestamp",Query.Direction.DESCENDING)
.startAfter(timestamp)
.limit(15)
.get()
.addOnCompleteListener {
if (it.isSuccessful) {
if(it.result.documents.isNotEmpty()) {
emitter.onSuccess(it.result)
timestamp = it.result.documents.last().data?.get("timestamp").toString()
}
}
}
.addOnFailureListener {
}
}
})
}
FirebaseViewModel.kt
private val notificationPagingdata: MediatorLiveData<PagingData<NotificationModal>> =
MediatorLiveData()
fun getNotification(){
disposable.add(Pager(
config = PagingConfig(
pageSize = 15,
enablePlaceholders = false,
prefetchDistance = 1,
),
pagingSourceFactory = {
NotificationPagingDataSource(repository)
}
).flowable.subscribe({
notificationPagingdata.value = it
}
)
)
}
fun observeNotificationPagingDataSource():LiveData<PagingData<NotificationModal>>{
return notificationPagingdata
}
YourFragment or Activity.kt
private fun setObserver() {
fireViewModel.observeNotificationPagingDataSource().observe(viewLifecycleOwner, Observer {
adapter.submitData(lifecycle,it)
})
}
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)
}
}
}
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
}
}