I try to use paging3 in android with offline support and for that I use Remotemediator but i'm not able to store data properly. it's store data only once when first time Api calls after that new item not shown in recyclerview scrolling
MovieDao
@Dao
interface MovieDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun addMovies(movies: List<Result>)
@Query("SELECT * FROM movies")
fun getMovies(): PagingSource<Int, Result>
@Query("DELETE FROM movies")
suspend fun deleteMovies()
}
RemoteKeyDao
@Dao
interface RemoteKeysDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun addAllRemoteKeys(remoteKeys: List<MovieRomoteKeys>)
@Query("SELECT * FROM movieRomoteKeys WHERE id =:id")
suspend fun getRemoteKeys(id: Int): MovieRomoteKeys
@Query("DELETE FROM movieRomoteKeys")
suspend fun deleteAllRemoteKeys()
}
MovieDatabase
@Database(entities = [Result::class, MovieRomoteKeys::class], version = 1)
abstract class MovieDatabase : RoomDatabase() {
abstract fun movieDao(): MovieDao
abstract fun remoteKeysDao(): RemoteKeysDao
}
DatabaseModule
@InstallIn(SingletonComponent::class)
@Module
class DataBaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext appContext: Context): MovieDatabase {
return Room.databaseBuilder(appContext, MovieDatabase::class.java,
"movieDB").build()
}
}
MovieRemoteMeditor
@ExperimentalPagingApi
class MovieRemoteMediator(
private val movieApi: MovieApi,
private val movieDatabase: MovieDatabase
) : RemoteMediator<Int, Result>() {
val movieDao = movieDatabase.movieDao()
val movieRomoteKeysDao = movieDatabase.remoteKeysDao()
override suspend fun load(loadType: LoadType, state: PagingState<Int, Result>): MediatorResult {
return try {
val currentPage = when (loadType) {
LoadType.REFRESH -> {
val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
remoteKeys?.nextPage?.minus(1) ?: 1
}
LoadType.PREPEND -> {
val remoteKeys = getRemoteKeyForFirstItem(state)
val prevPage = remoteKeys?.prevPage ?: return MediatorResult.Success(
endOfPaginationReached = remoteKeys != null
)
prevPage
}
LoadType.APPEND -> {
val remoteKeys = getRemoteKeyForLastItem(state)
val nextPage = remoteKeys?.nextPage ?: return MediatorResult.Success(
endOfPaginationReached = remoteKeys != null
)
nextPage
}
}
val response = movieApi.getMovies(currentPage)
val endOfPaginationReached = response.total_pages == currentPage
val prevPage = if (currentPage == 1) null else currentPage - 1
val nextPage = if (endOfPaginationReached) null else currentPage + 1
movieDatabase.withTransaction {
if (loadType == LoadType.REFRESH) {
movieDao.deleteMovies()
movieRomoteKeysDao.deleteAllRemoteKeys()
}
movieDao.addMovies(response.results!!)
val keys = response.results.map { movie ->
MovieRomoteKeys(
id = movie.id!!,
prevPage = prevPage,
nextPage = nextPage
)
}
movieRomoteKeysDao.addAllRemoteKeys(keys)
}
MediatorResult.Success(endOfPaginationReached)
} catch (e: Exception) {
MediatorResult.Error(e)
}
}
private suspend fun getRemoteKeyClosestToCurrentPosition(state: PagingState<Int, Result>): MovieRomoteKeys? {
return state.anchorPosition?.let { position ->
state.closestItemToPosition(position)?.id?.let { id ->
movieRomoteKeysDao.getRemoteKeys(id = id)
}
}
}
private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, Result>): MovieRomoteKeys? {
return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull()?.let { movie ->
movieRomoteKeysDao.getRemoteKeys(id = movie.id!!)
}
}
private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, Result>): MovieRomoteKeys? {
return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull()
?.let { movie -> movieRomoteKeysDao.getRemoteKeys(id = movie.id!!) }
}
}
MovieRepository
@ExperimentalPagingApi
class MovieRepository @Inject constructor(
private val movieApi: MovieApi,
private val movieDatabase: MovieDatabase
) {
fun getMovies() = Pager(
config = PagingConfig(pageSize = 20, maxSize = 100),
remoteMediator = MovieRemoteMediator(movieApi, movieDatabase),
pagingSourceFactory = { movieDatabase.movieDao().getMovies() }
).liveData
}
MovieViewModel
@ExperimentalPagingApi
@HiltViewModel
class MovieViewModel @Inject constructor(private val movieRepository: MovieRepository)
: ViewModel() {
val list = movieRepository.getMovies().cachedIn(viewModelScope)
}
MovieListFragment
@ExperimentalPagingApi
@AndroidEntryPoint
class MovieListFragment : Fragment() {
private var _binding: FragmentMovieListBinding? = null
private val binding get() = _binding!!
private val movieViewModel by viewModels<MovieViewModel>()
@Inject
lateinit var moviePagingAdapter: MoviePagingAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentMovieListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initialize()
}
private fun initialize() {
(activity as AppCompatActivity?)!!.supportActionBar!!.title = "Movie App"
binding.rvMovies.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
setHasFixedSize(true)
adapter = moviePagingAdapter.withLoadStateHeaderAndFooter(
header = LoaderAdapter { moviePagingAdapter.retry() },
footer = LoaderAdapter { moviePagingAdapter.retry() }
)
}
movieViewModel.list.observe(viewLifecycleOwner, Observer {
moviePagingAdapter.submitData(lifecycle, it)
})
viewLifecycleOwner.lifecycleScope.launch {
moviePagingAdapter.loadStateFlow.collectLatest { loadStates ->
binding.shimmerViewContainer.apply {
startShimmerAnimation()
isVisible = loadStates.refresh is LoadState.Loading
}
binding.layoutError.apply {
root.isVisible = loadStates.refresh is LoadState.Error
btnRetry.setOnClickListener {
moviePagingAdapter.retry()
}
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}