I am trying to implement the Remote Mediator to have an infinite list. It was working properly when I was using the Paging Source but from the moment I started to implement the Remote Mediator, I only get the first page of the list or a constant flickering where the data are deleted and added to the database every second.
API SERVICE
@GET("companies/{id}/timeline")
suspend fun getTimeline(
@Path("id") id: String,
@Query("page") page: String?
): Response<List<TimelineResponse>>
DATABASE
@Database(
entities = [
DatabaseMessage::class,
DatabaseRemoteKey::class
],
version = 5,
exportSchema = false
)
@TypeConverters(AttachmentConverter::class)
abstract class TimelineDatabase : RoomDatabase() {
abstract fun getTimelineDao(): TimelineDao
abstract fun getKeysDao(): RemoteKeyDao
}
DAO
@Dao
interface TimelineDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTimeline(list: List<DatabaseMessage>)
@Query("SELECT * FROM timeline ORDER BY score ASC")
fun pagingSource(): PagingSource<Int, DatabaseMessage>
@Query("DELETE FROM timeline")
suspend fun deleteTimeline()
}
REMOTE MEDIATOR
@OptIn(ExperimentalPagingApi::class)
class TimelineRemoteMediator(
private val api: ContentService,
private val db: TimelineDatabase
) : RemoteMediator<Int, DatabaseMessage>() {
private val timelineDao = db.getTimelineDao()
private lateinit var timeline: List<DatabaseMessage>
override suspend fun initialize(): InitializeAction {
return InitializeAction.LAUNCH_INITIAL_REFRESH
}
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, DatabaseMessage>
): MediatorResult {
return try {
val loadKey = when(loadType) {
LoadType.REFRESH -> null
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
val lastItem = state.lastItemOrNull()
if (lastItem == null) {
return MediatorResult.Success(endOfPaginationReached = true)
}
lastItem.id
}
}
val response = api.getTimeline(
id = COMMUNITY_ID,
page = loadKey
).let { response ->
response.body()?.map { it.asDatabaseModel() }
}
db.withTransaction {
if (loadType == LoadType.REFRESH) {
timelineDao.deleteTimeline()
}
if (response != null) {
timeline = response
timelineDao.insertTimeline(timeline)
}
}
MediatorResult.Success(
endOfPaginationReached = timeline.isEmpty()
)
} catch (e: IOException) {
MediatorResult.Error(e)
} catch (e: HttpException) {
MediatorResult.Error(e)
}
}
}
REPOSITORY
class ContentRepository @Inject constructor(
private val contentRemoteDataSource: ContentRemoteDataSource,
private val contentService: ContentService,
private val database: TimelineDatabase
) {
// Here we can use the Mediator inside the repository
@OptIn(ExperimentalPagingApi::class)
fun getTimeline(): Flow<PagingData<DatabaseMessage>> {
val pagingSourceFactory = { database.getTimelineDao().pagingSource() }
return Pager(
config = PagingConfig(
pageSize = DEFAULT_PAGE_LIMIT,
enablePlaceholders = false
),
remoteMediator = TimelineRemoteMediator(
api = contentService,
db = database
),
pagingSourceFactory = pagingSourceFactory
).flow
}
suspend fun addToInterest(id: String) = flow {
emit(Resource.loading(null))
val response = contentRemoteDataSource.addToInterest(id)
when (response.status) {
SUCCESS -> {
emit(Resource.success(response.data))
}
ERROR -> {
emit(Resource.error(response.errorResponse, null))
}
LOADING -> {
emit(Resource.loading(null))
}
}
}
}
ADAPTER
class CommunityMessageAdapter(private val onClickListener: OnClickListener) :
PagingDataAdapter<Message, CommunityMessageAdapter.MessageViewHolder>(DiffCallback) {
class MessageViewHolder(private var binding: ItemViewMessageBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(message: Message) {
binding.message = message
binding.executePendingBindings()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {
return MessageViewHolder(
ItemViewMessageBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
getItem(position)?.let { message ->
holder.itemView.setOnClickListener {
onClickListener.onClick(message)
}
holder.bind(message)
}
}
companion object DiffCallback : DiffUtil.ItemCallback<Message>() {
override fun areItemsTheSame(oldItem: Message, newItem: Message): Boolean {
return oldItem.id === newItem.id
}
override fun areContentsTheSame(oldItem: Message, newItem: Message): Boolean {
return oldItem == newItem
}
}
class OnClickListener(val clickListener: (message: Message) -> Unit) {
fun onClick(message: Message) = clickListener(message)
}
}
VIEW MODEL
@OptIn(ExperimentalPagingApi::class)
@HiltViewModel
class HomeViewModel @Inject constructor(
contentRepository: ContentRepository
) : ViewModel() {
private val _errorMessage = MutableLiveData<String?>()
val errorMessage: LiveData<String?> = _errorMessage
private val _isLoading = MutableLiveData<Boolean?>()
val isLoading: LiveData<Boolean?> = _isLoading
private val _networkError = MutableLiveData<Boolean?>()
val networkError: LiveData<Boolean?> = _networkError
private val _navigateToMessageDetails = MutableLiveData<Message?>()
val navigateToMessageDetails: LiveData<Message?> = _navigateToMessageDetails
val getTimeline = contentRepository.getTimeline().map { pagingData ->
pagingData.map { it.asUiModel() }
}
fun openMessageDetails(message: Message) {
_navigateToMessageDetails.postValue(message)
}
fun openMessageDetailsComplete() {
_navigateToMessageDetails.postValue(null)
}
}
FRAGMENT
@AndroidEntryPoint
class HomeFragment : Fragment() {
private lateinit var binding: FragmentHomeBinding
private val adapter by lazy {
CommunityMessageAdapter(CommunityMessageAdapter.OnClickListener { message ->
viewModel.openMessageDetails(message)
})
}
private val viewModel: HomeViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentHomeBinding.inflate(layoutInflater, container, false)
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
@OptIn(ExperimentalPagingApi::class)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setAdapters()
setObservers()
}
private fun setObservers() {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.getTimeline.collectLatest { timeline ->
adapter.submitData(timeline)
}
}
viewModel.isLoading.observe(viewLifecycleOwner) { loading ->
if (loading == true) {
binding.ivLoadingTimeline.setImageResource(R.drawable.loading_animation)
binding.ivLoadingTimeline.visibility = View.VISIBLE
} else {
binding.ivLoadingTimeline.visibility = View.GONE
}
}
viewModel.networkError.observe(viewLifecycleOwner) { error ->
if (error == true) {
binding.ivErrorNetworkTimeline.setImageResource(R.drawable.ic_network_error)
binding.ivErrorNetworkTimeline.visibility = View.VISIBLE
} else {
binding.ivErrorNetworkTimeline.visibility = View.GONE
}
}
viewModel.errorMessage.observe(viewLifecycleOwner) {
binding.layoutHomeFragment.showSnackBar(R.string.error_message)
}
viewModel.navigateToMessageDetails.observe(viewLifecycleOwner) {
if (null != it) {
this.findNavController()
.navigate(HomeFragmentDirections.actionHomeFragmentToMessageDetailsFragment(it))
}
viewModel.openMessageDetailsComplete()
}
}
private fun setAdapters() {
binding.rvListMessages.adapter = adapter
}
}
Thanks a lot in advance, I tried different ways, also with Remote Keys but I still have the constant flickering.