Mehedi Hassan Piash [Sr. Software Engineer]

February 11, 2022

Pagination with Paging3 in android

February 11, 2022 Posted by Piash , No comments

Pagination or Endless scrolling is one of the common pain points in recyclerView in android. But Paging3 library just solved it very swiftly. Let’s start with how to implement the paging3 library in real-life projects with API calls.

paging3 in movie app

Step1. build.gradle

dependencies {
implementation "androidx.paging:paging-runtime-ktx:3.1.0"
}

Step-2. ApiService.kt

interface ApiService {
@GET(ApiUrls.POPULAR_MOVIE_LIST) // your desired api end points
suspend fun popularMovieList(@Query("page") page: Int): BaseModel
}

Step-3. BaseModel.kt

data class BaseModel(
@SerializedName("page")
val page: Int,
@SerializedName("results")
val results: List<MovieItem>,
@SerializedName("total_pages")
val totalPages: Int,
@SerializedName("total_results")
val totalResults: Int
)

Step-4. MovieItem.kt

data class MovieItem(
@SerializedName("adult")
val adult: Boolean,
@SerializedName("backdrop_path")
val backdropPath: String,
@SerializedName("genre_ids")
val genreIds: List<Int>,
@SerializedName("id")
val id: Int,
@SerializedName("original_language")
val originalLanguage: String,
@SerializedName("original_title")
val originalTitle: String,
@SerializedName("overview")
val overview: String,
@SerializedName("popularity")
val popularity: Double,
@SerializedName("poster_path")
val posterPath: String,
@SerializedName("release_date")
val releaseDate: String,
@SerializedName("title")
val title: String,
@SerializedName("video")
val video: Boolean,
@SerializedName("vote_average")
val voteAverage: Double,
@SerializedName("vote_count")
val voteCount: Int
)

Step-5. PopularPagingDataSource.kt

class PopularPagingDataSource @Inject constructor(private val apiService: ApiService) :
PagingSource<Int, MovieItem>() {

override fun getRefreshKey(state: PagingState<Int, MovieItem>): Int? {
return state.anchorPosition
}

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MovieItem> {
return try {
val nextPage = params.key ?: 1
val movieList = apiService.popularMovieList(nextPage)
LoadResult.Page(
data = movieList.results,
prevKey = if (nextPage == 1) null else nextPage - 1,
nextKey = movieList.page + 1
)
} catch (exception: IOException) {
Timber.e("exception ${exception.message}")
return LoadResult.Error(exception)
} catch (httpException: HttpException) {
Timber.e("httpException ${httpException.message}")
return LoadResult.Error(httpException)
}
}
}

Step-6. Paging3ViewModel.kt

@HiltViewModel
class Paging3ViewModel @Inject constructor(private val repoPaging: PopularPagingDataSource) :
ViewModel() {
val flow = Pager(
// Configure how data is loaded by passing additional properties to
// PagingConfig, such as prefetchDistance.
PagingConfig(pageSize = 2)
) {
repoPaging
}.flow.cachedIn(viewModelScope)
}

Step-7. adapter_movie_item_paging_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:elevation="2dp"
android:orientation="vertical"
app:cardCornerRadius="16dp">

Step-8. MoviePagingAdapter.kt

class MoviePagingAdapter :
PagingDataAdapter<MovieItem, MoviePagingAdapter.MovieViewHolder>(DataDifferentiator) {
var onItemClick: ((MovieItem) -> Unit)? = null

inner class MovieViewHolder(val bind: AdapterMovieItemPagingLayoutBinding) :
RecyclerView.ViewHolder(bind.root) {
fun bind(item: MovieItem) {
itemView.setOnClickListener {
onItemClick?.invoke(item)
}
}

}

override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
holder.bind.image.loadImage(ApiUrls.IMAGE_URL.plus(getItem(position)?.posterPath))
getItem(position)?.let { holder.bind(it) }
}


override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val bind = AdapterMovieItemPagingLayoutBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return MovieViewHolder(bind)
}

object DataDifferentiator : DiffUtil.ItemCallback<MovieItem>() {

override fun areItemsTheSame(oldItem: MovieItem, newItem: MovieItem): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: MovieItem, newItem: MovieItem): Boolean {
return oldItem == newItem
}
}

}

Step-9. fragment_paging3.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.paging3.Paging3Fragment">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/paging_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="6dp"/>

<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />

</RelativeLayout>

Step-10. Paging3Fragment.kt

@AndroidEntryPoint
class Paging3Fragment : Fragment() {
private var _binding: FragmentPaging3Binding? = null
private val binding get() = requireNotNull(_binding)
private val viewModel: Paging3ViewModel by viewModels()
private val moviePagingAdapter: MoviePagingAdapter by lazy {
MoviePagingAdapter()
}

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentPaging3Binding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
}

private fun initView() = with(binding) {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.flow.catch {
progressBar.hide()
}.collectLatest {
moviePagingAdapter.submitData(it)
}
}
pagingRecycler.apply {
layoutManager = GridLayoutManager(requireContext(), 2)
adapter = moviePagingAdapter
}
moviePagingAdapter.addLoadStateListener {
when {
it.refresh == LoadState.Loading -> {
progressBar.show()
}
it.append == LoadState.Loading -> {
progressBar.show()
}
else -> {
progressBar.hide()
}
}
}
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}


Ref: https://piashcse.medium.com/pagination-with-paging3-in-android-ccaf7030fcc

Github: https://github.com/piashcse/blog_piashcse_code/tree/master/MVVM_Hilt/app/src/main/java/com/piashcse/experiment/mvvm_hilt/ui/paging3