0

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.

0 Answers0