androidandroid-roomandroid-pagingandroid-paging-3remote-mediator

Paging 3 Remote Mediator data is not shown?


I am developing simple News app, and i also implement Remote Mediator functionality in Paging 3 while Developing it i create a News Entity table , NewsRemoteKey table, and DAO for those when i implement and run that the app will not Show any data, i Log the response, But Nothing Happend i doesn't know what wrong i my code!!

So, Please help Me to identify what problem in my approach/code

NewsDao.kt

@Dao
interface NewsDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertNews(news: List<NewsDto>)

    @Query("SELECT * FROM news")
    fun getNews(): PagingSource<Int, NewsDto>

    @Query("DELETE FROM news")
    suspend fun clearAllNews()

}
NewsRemoteKeyDao.kt

@Dao
interface NewsRemoteKeyDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertRemoteKey(remoteKey: List<NewsRemoteKey>)

    @Query("SELECT * FROM newsRemoteKey WHERE id =:id")
    suspend fun getAllRemoteKey(id:String):NewsRemoteKey

    @Query("DELETE FROM newsRemoteKey")
    suspend fun clearAllNewsRemoteKey()
}
NewsDto.kt (Model class for Retrofit and for Room Entity)

@Entity(tableName = "news")
data class NewsDto(
    val author: String,
    val content: String,
    val publishedAt: String,
    val title: String,
    @PrimaryKey(autoGenerate = false)
    val url: String,
    val urlToImage: String
)
@Entity(tableName = "news")
data class NewsDto(
    val author: String,
    val content: String,
    val publishedAt: String,
    val title: String,
    @PrimaryKey(autoGenerate = false)
    val url: String,
    val urlToImage: String
)
NewsRemoteKey.kt (Model class for RemoteKeyTable(Entity))

@Entity(tableName = "newsRemoteKey")
data class NewsRemoteKey (
    @PrimaryKey(autoGenerate = false)
    val id:String,
    val prevPage:Int?,
    val nextPage:Int?,
)
NewsDatabase.kt 

@Database(
    entities = [NewsDto::class,NewsRemoteKey::class],
    version = 1
)
abstract class NewsDatabase:RoomDatabase() {

    abstract fun getNewsRemoteKeysDao():NewsRemoteKeyDao
    abstract fun getNewsDao():NewsDao
}
NetworkModule.kt (for hilt di)

@InstallIn(SingletonComponent::class)
@Module
class NetworkModule {

    @Singleton
    @Provides
    fun provideDatabase(@ApplicationContext context: Context): NewsDatabase {
        return Room.databaseBuilder(
           context= context,
            NewsDatabase::class.java,
            "newsDB"
        ).build()
    }


    @Singleton
    @Provides
    fun provideRetrofitInstance():Retrofit{
        return Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(httpClient())
            .build()
    }

    private fun httpClient():OkHttpClient{
        val log = HttpLoggingInterceptor()
        log.level = HttpLoggingInterceptor.Level.BODY
        return OkHttpClient.Builder()
            .addInterceptor(log)
            .build()

    }

    @Singleton
    @Provides
    fun provideApiService(retrofit: Retrofit): NewsApi{
        return retrofit.create(NewsApi::class.java)
    }

}
NewsRemoteMediator.kt (RemoteMediator class)

@ExperimentalPagingApi
class NewsRemoteMediator @Inject constructor(
    private val newsApi: NewsApi,
    private val newsDatabase: NewsDatabase
) : RemoteMediator<Int, NewsDto>() {

    private val getNewsDao = newsDatabase.getNewsDao()
    private val getRemoteKeyDao = newsDatabase.getNewsRemoteKeysDao()

    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, NewsDto>
    ): MediatorResult {
        return try {

            val currentPage = when (loadType) {
                LoadType.REFRESH -> {
                    val current = getRemoteKeyClosesToPosition(state)
                    current?.nextPage?.minus(1) ?:1
                }

                LoadType.PREPEND -> {
                    val firstItem = getRemoteKeyForFirstItem(state)
                    val nextPage = firstItem?.prevPage ?: return MediatorResult.Success(
                        endOfPaginationReached =true
                    )
                    nextPage
                }

                LoadType.APPEND -> {
                    val lastItem = getRemoteKeyForLastItem(state)
                    val nextPage = lastItem?.nextPage ?: return MediatorResult.Success(
                        endOfPaginationReached = true
                    ) 
                    nextPage
                }
            }

            val response = newsApi.getBreakingNews(currentPage, "in")

            val endOfPagination = currentPage == response.totalResults

            val prevPage = if (currentPage == 1) null else currentPage-1
            val nextPage = if (endOfPagination) null else currentPage+1

            newsDatabase.withTransaction {

                if (loadType == LoadType.REFRESH) {
                    getNewsDao.clearAllNews()
                    getRemoteKeyDao.clearAllNewsRemoteKey()
                }

                val api = response.articles

                val key = api.map {
                    NewsRemoteKey(
                        it.url,
                        prevPage = prevPage,
                        nextPage = nextPage
                    )
                }
                getNewsDao.insertNews(response.articles)
                getRemoteKeyDao.insertRemoteKey(key)

            }

            MediatorResult.Success(endOfPagination)

        } catch (e: Exception) {
            MediatorResult.Error(e)
        }
    }

    private suspend fun getRemoteKeyClosesToPosition(state: PagingState<Int, NewsDto>): NewsRemoteKey? {
        return state.anchorPosition?.let { url ->
            state.closestItemToPosition(url)?.url?.let {
                getRemoteKeyDao.getAllRemoteKey(it)
            }
        }

    }

    private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, NewsDto>): NewsRemoteKey? {
        return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull()?.let {
            getRemoteKeyDao.getAllRemoteKey(it.url)
        }

    }

    private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, NewsDto>): NewsRemoteKey? {
        return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull()?.let {
            getRemoteKeyDao.getAllRemoteKey(it.url)
        }
    }
}
NewsRepository.kt

class NewsRepository @Inject constructor(
      newsApi: NewsApi,
    private val newsDatabase: NewsDatabase
) {

    @ExperimentalPagingApi
    val newsPagingData = Pager(
    config = PagingConfig(
    pageSize = 20,
    maxSize = 100,
    ), pagingSourceFactory =  {newsDatabase.getNewsDao().getNews()},
        remoteMediator = NewsRemoteMediator(newsApi,newsDatabase)
    ).liveData


}
MainViewModel.kt

@HiltViewModel
class MainViewModel @Inject constructor(private val newsRepository: NewsRepository):ViewModel() {

    @ExperimentalPagingApi
    val page = newsRepository.newsPagingData.cachedIn(viewModelScope)
}
NewsPagingAdapter.kt (PagingStateAdapter for RecyclerView Adapter)


class NewsPagingAdapter @Inject constructor():PagingDataAdapter<NewsDto,NewsPagingAdapter.NewsViewHolder>(COMPARATOR) {

    class NewsViewHolder(binding: NewsItemBinding): RecyclerView.ViewHolder(binding.root){
        val tvTitle = binding.tvNewsTitle
        val tvAuthor = binding.tvAuthor
        val tvPublishedTime = binding.tvPublishedAt
        val newsImage = binding.imgNewsPic
    }


    companion object{
        val COMPARATOR = object : DiffUtil.ItemCallback<NewsDto>(){
            override fun areItemsTheSame(oldItem: NewsDto, newItem: NewsDto)=
                oldItem == newItem

            override fun areContentsTheSame(oldItem: NewsDto, newItem: NewsDto)=
                oldItem.url == newItem.url

        }
    }

    override fun onBindViewHolder(holder: NewsViewHolder, position: Int) {
        val list = getItem(position)
        holder.apply {
            tvTitle.text = list?.title
            tvAuthor.text = list?.author
            tvPublishedTime.text = list?.publishedAt
            newsImage.load(list?.urlToImage){
                transformations(RoundedCornersTransformation(radius = 60f))
                placeholder(R.drawable.ic_launcher_background)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder {
        return NewsViewHolder(
            NewsItemBinding.inflate(LayoutInflater.from(parent.context),parent,false)
        )
    }
}
MainActivity.kt

@ExperimentalPagingApi
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var adapter: NewsPagingAdapter

    private lateinit var newsRecyclerView:RecyclerView
    private lateinit var mainViewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        newsRecyclerView = findViewById(R.id.rv_news_list)

        newsRecyclerView.layoutManager = LinearLayoutManager(this)
        newsRecyclerView.setHasFixedSize(true)


        mainViewModel = ViewModelProvider(this)[MainViewModel::class.java]

        mainViewModel.page.observe(this){
            adapter.submitData(lifecycle,it)
            newsRecyclerView.adapter = adapter
        }
    }
}

Solution

  • That problem doesn't on implementation, that actual problem is on the api (NewsApi), the api sometimes return null properties for some fields like urlToImage and content in our NewsDto for some news, we will expect a non-nullable type for that property be sure to use nullable in the fields something like that

    val author: String,
    val content: String?,
    val publishedAt: String,
    val title: String,
    @PrimaryKey(autoGenerate = false)
    val url: String,
    val urlToImage: String?