1

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)!!) }
    }
}
t3nsa
  • 680
  • 6
  • 21

0 Answers0