Leveraging Retrofit Dynamic Url with Paging 3 to create PokemonApp

Today I will explain how to use Retrofit Dynamic Url with Paging 3 to create PokemonApp based on the data from PokeApi.

Retrofit Dynamic Url

To get detailed Pokemon data on PokeApi, we must know the Pokemon Id or Name

PokeApi

To find out the Pokemon Id or Name we can see the data at https://pokeapi.co/api/v2/pokemon

PokeApi

In the results data, there is a list of objects that contain name & url which we can use to get detailed Pokemon data.

because the Pokemon id data is in the url, instead of having to do the mapping again on the data, I will just use the url to get Pokemon data using Retrofit Dynamic Url.

How to use it is quite easy, which is enough to provide the @Url annotation in a function like this and we no longer need to specify the address in the @GET annotation.

After getting a list of Pokemon url data from PokemonApi, we can use Map function on that list of data to get list of detailed Pokemon data

This method is not a best practice, but what I want to show you is how to use Retrofit Dynamic Url & for the demo, we can see it in the video from my previous writing.

Pokemon Demo

Paging 3

Why use Paging 3? One of the main reasons I use it is because Paging 1 & 2 doesn’t yet support using Kotlin coroutines and Flow, and Paging 3 is currently Stable.

To see Paging 3 in detail, you can see the Paging library overview page and how to use it, we can see it in the Android Paging Codelab.

To do the pagination process on PokeApi we can use an offset to get the next data, in my case I will give an offset in a multiple of 20

The implementation using the Retrofit will be like this :

To use Paging 3, some of the main components that we will use are :

  • PagingData - a container for paginated data. Each refresh of data will have a separate corresponding PagingData.
  • PagingSource - a PagingSource is the base class for loading snapshots of data into a stream of PagingData.

PagingSource

To get Pokemon data we will use PagingSource and what we have to pay attention to is :

  • The type of the paging key — in our case, the PokeAPI uses 1-based index numbers for pages, so the type is Int.
  • The type of data loaded — in our case, we’re loading PokemonPaging items.
return object : PagingSource<Int, PokemonPaging>(){
override fun getRefreshKey(state: PagingState<Int, PokemonPaging>): Int? {
TODO("Not yet implemented")
}

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, PokemonPaging> {
TODO("Not yet implemented")
}
}

The load() function will be called by the Paging library to asynchronously fetch more data to be displayed as the user scrolls around. The LoadParams object keeps information related to the load operation, including the following :

  • Offset of the page to be loaded. When the first time load is called, LoadParams.key will be null. So we have to define the initial page key which is POKEMON_STARTING_OFFSET with zero value to get the first pagination data. and after that we will use next params.key value to multiply by 20

The load()function returns a LoadResult& can take one of the following types:

  • LoadResult.Page, if the result was successful.
  • LoadResult.Error, in case of error.

Next we need to implement getRefreshKey(). The refresh key is used for subsequent refresh calls to PagingSource.load() (the first call is initial load which uses initialKey provided by Pager). A refresh happens whenever the Paging library wants to load new data to replace the current list, e.g., on swipe to refresh or on invalidation due to database updates, config changes, process death, etc.

PagingData

To construct the PagingData, we first need to decide what API we want to use to pass the PagingData to other layers of our app:

  • Kotlin Flow - use Pager.flow.
  • LiveData - use Pager.liveData.
  • RxJava Flowable - use Pager.flowable.
  • RxJava Observable - use Pager.observable.

As we’re already using Flow in our app, we'll continue with this approach, we'll use Flow<PagingData<PokemonPaging>>

No matter which PagingData builder we use, we have to pass the following parameters:

  • PagingConfig. This class sets options regarding how to load content from a PagingSource such as how far ahead to load, the size request for the initial load, and others. The only mandatory parameter you have to define is the page size—how many items should be loaded in each page. By default, Paging will keep in memory all the pages you load.
  • A function that defines how to create the PagingSource. In our case, we'll just use getPagingPokemon() from remoteDataSource for each new query.

This is how we use all of our Paging setup in ViewModel

Flow<PagingData> has a handy cachedIn() method that allows us to cache the content of a Flow<PagingData> in a CoroutineScope. Since we're in a ViewModel, we will use the androidx.lifecycle.viewModelScope.

To bind a PagingData to a RecyclerView, use a PagingDataAdapter. The PagingDataAdapter gets notified whenever the PagingData content is loaded and then it signals the RecyclerView to update

We can observe PagingDataAdapter state and use it to show our custom dialog

Demo

Pokemon Demo

if you want to see an example of the code you can see it here, hopefully it’s useful

Thanks

References

Always curious and always want to continue learning in many ways