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
}
}
}
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?