Mobile devices run on limited power and network capacities, therefore it's our job as developers to optimize the use of these resources for a more efficient and smooth performance. While building android apps we frequently encounter the situation where we need to pull data from a data source or API and to display them as items in a scroll-able list. The problem arises when the number of these items is very huge or even worse: it's always increasing.
To solve this issue android provides the Paging Library as part of Android Jetpack, which is essentially a library that follows the lazy loading software design pattern, allowing us to defer the display of items until needed on the list of scroll-able items. Paging Library manages the network calls, caching of data, and reloading/refresh of list data. Pretty awesome, right? Now, let's see some code.
Models
First things first, we need to define our data container which will be an abstraction for our business logic. In this case we'll use a Person
class that holds the name and age of the person. Next, we'll define a network response class that hold the remote data source response, which we will call PeopleResponse
data class Person(val name:String, val age: int)
class PeopleResponse{
@SerializedName("next_page")
val nextPage: Int?
@SerializedName("previous_page")
val perviousPage: Int?
@SerializedName("people")
val people: List<Person>
}
Remote Data Source
The remote data source is the service responsible for the network calls to fetch data in order to load the items to display in the list. In our case well create a Retrofit interface with a single getPeople
function that takes the page number as a parameter.
interface PeopleService {
@GET("people/{page}")
fun getPeople(page:Int): PeopleResponse
companion object {
operator fun invoke(): PeopleService {
val builder = Retrofit.Builder().baseUrl(baseUrl)
// we need to add converter factory to
// convert JSON object to Java object
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(PeopleService::class.java)
}
}
}
Paging Source
Next we create our paging source by extending Jetpack's PagingSource
class. The paging source will be the class responsible for loading and synchronizing the network calls as well as caching and refreshing the data. We need to implement two functions:
load
Called when a new page is needed, and contains parameters specifying the load info such as the current page key.
getRefreshKey
Called when the current PagingSource
is invalidated returns an Int
the page's key to use in the newly created paging source. If null
is returned, the PagingSource
starts at the first page.
class PeoplePagingSource(
val service: PeopleService,
) : PagingSource<Int, Person>() {
override suspend fun load(
params: LoadParams<Int>
): LoadResult<Int, User> {
try {
// Should start at 1 if no key is found
val nextPage = params.key ?: 1
val response = service.getPeople(nextPage)
return LoadResult.Page(
data = response.users,
prevKey = response.previousPage,
nextKey = response.nextPage
)
} catch (e: Exception) {
// Handle errors e.g: Network Failure
}
}
override fun getRefreshKey(state: PagingState<Int, Person>): Int? {
// Try to get the closest page to the current anchorPosition
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.let { anchorPage ->
val pageIndex = pages.indexOf(anchorPage)
if (pageIndex == 0) {
null
} else {
pages[pageIndex - 1].nextKey
}
}
}
}
}
}
Exposing the PagingData
After we have successfully set our PagingSource
it's time to follow the MVVM best practices and expose the result as a Kotlin flow for our UI to consume. We set up this in the ViewModel as following:
class PeopleViewModel(val service: PeopleService) : ViewModel() {
val people = Pager(
// Configure how data should be loaded
PagingConfig(pageSize = 20)
) {
PeoplePagingSource(service)
}.flow.cachedIn(viewModelScope)
}
Update UI
Finally, the UI component, e.g: Fragment, should consume the paging data and update the List accordingly. This way whenever we run out of items when scrolling, the Paging library will automatically start network requests, cache them, and update the RecyclerView Adapter
accordingly.
class PeopleFragment : Fragment(){
val viewModel by viewModels<PeopleViewModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val pagingAdapter = PeopleAdapter(UserComparator)
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.adapter = pagingAdapter
viewLifecycleOwner.lifecycleScope..launch {
viewModel.flow.collectLatest { pagingData ->
pagingAdapter.submitData(pagingData)
}
}
return inflater.inflate(R.layout.fragment_people, container, false)
}
}
Final notes
Thank you very much for your precious time! I write about #AndroidDev & #FlutterDev
You can reach me at ahmed.g.obied@gmail.com
You can also visit my personal blog at Ahmed Gadein
Cheers! ✨