Using Android Paging Library + Clean Architecture

Using Android Paging Library + Clean Architecture

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

You can also visit my personal blog at Ahmed Gadein

Cheers! ✨