I'm trying to make an app that gets data from TMDB API with retrofit.
I use Paging Source and Flow to get the movies from the api.
MoviesViewModel.kt
class MoviesViewModel: ViewModel() {
val isLoading: MutableLiveData<Boolean> = MutableLiveData()
val hasError: MutableLiveData<Boolean> = MutableLiveData()
fun getData(): Flow<PagingData<MoviesModel>> {
return MoviesRepository.getData()
}
}
MoviesRepository.kt
object MoviesRepository {
fun getData(): Flow<PagingData<MoviesModel>> {
return Pager(
config = PagingConfig(
pageSize = Constants.QUERY_PAGE_SIZE,
enablePlaceholders = false
),
pagingSourceFactory = { MoviesPagingSource(RetrofitInstance.api) }
).flow
}
}
MoviesPagingSource.kt
class MoviesPagingSource(
private val service: ApiService
) : PagingSource<Int, MoviesModel>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MoviesModel> {
val position = params.key ?: FIRST_PAGE
return try {
val response = service.getMovies(page = position)
val movies = response.body()?.results
val nextKey = if (movies!!.isEmpty()) {
null
} else {
position + (params.loadSize / QUERY_PAGE_SIZE)
}
LoadResult.Page(
data = movies,
prevKey = if (position == FIRST_PAGE) null else position,
nextKey = nextKey
)
} catch (e: IOException) {
LoadResult.Error(e)
} catch (e: HttpException) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, MoviesModel>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
}
MoviesPage.kt
private lateinit var pagerAdapter: MoviesAdapter
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
pagerAdapter.loadStateFlow.collectLatest { loadStates ->
viewModel.isLoading.value = loadStates.refresh is LoadState.Loading // set the visibility of progressBar
viewModel.hasError.value = loadStates.refresh is LoadState.Error // show the error message
Log.d("ERROR", viewModel.hasError.value.toString()) // returns true
}
}
lifecycleScope.launch {
viewModel.getData().collectLatest {
pagerAdapter.submitData(it) }
}
}
private fun setupRecyclerView() { // called in onCreateView
pagerAdapter = MoviesAdapter()
binding.movieRecycler.apply {
adapter = pagerAdapter
addItemDecoration(MarginDecoration(context))
}
}
Everything works well till this point but I want to add an error handler in case there is no network to be displayed instead of the RecyclerView
. The progressBar
is displayed when loading the movies then it's gone after loading the data but it's not the same case for the error layout. It's not showing at all.
In my xml files I have this for progressBar visibility:
android:visibility="@{viewModel.isLoading? View.VISIBLE: View.GONE}"
I've added the same logic for error layout:
<Button
android:id="@+id/retry"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="Error"
android:visibility="@{viewModel.hasError? View.VISIBLE: View.GONE}"
/>
MoviesAdapter.kt
class MoviesAdapter : PagingDataAdapter<MoviesModel, MoviesAdapter.MovieItemViewHolder>(
differCallback
) {
var onItemClick: ((MoviesModel) -> Unit)? = null
inner class MovieItemViewHolder(private var binding: ItemMovieBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(movie: MoviesModel?) {
binding.movieItem = movie
binding.executePendingBindings()
}
}
companion object {
private val differCallback = object : DiffUtil.ItemCallback<MoviesModel>() {
override fun areItemsTheSame(oldItem: MoviesModel, newItem: MoviesModel): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: MoviesModel, newItem: MoviesModel): Boolean {
return oldItem == newItem
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieItemViewHolder {
return MovieItemViewHolder(
ItemMovieBinding.inflate(LayoutInflater.from(parent.context))
)
}
override fun onBindViewHolder(holder: MovieItemViewHolder, position: Int) {
holder.bind(getItem(position))
holder.itemView.setOnClickListener { onItemClick?.invoke(getItem(position)!!) }
}
}