A very complete recipe that implements a RecyclerView that pulls in paginated data, using Retrofit, LiveData. It includes example models, adapter, viewholders, and a viewholder for displaying the network state in case of an error.
Using Jetpackās pagination library (at the time of this post v.2.1.0).
Usage
Gradle dependency:
implementation "androidx.paging:paging-runtime-ktx:$pagination_version"
Api call:
@GET("/myPath")
fun getMyModel(): Call<MyModelResponse>
Example of MyModelResponse:
data class MyModelResponse(
val data: List<MyModel>,
val totalCount: Int,
val lastEvaluatedKey: MyModelLastKey?
)
/**
* Data class that is necessary for a UI to show a listing and interact w/ the rest of the system
*/
data class PagingListing<T>(
// the LiveData of paged lists for the UI to observe
val pagedList: LiveData<PagedList<T>>,
// represents the network request status to show to the user
val networkState: LiveData<PagingNetworkState>,
// represents the refresh status to show to the user. Separate from networkState, this
// value is importantly only when refresh is requested.
val refreshState: LiveData<PagingNetworkState>,
// refreshes the whole data and fetches it from scratch.
val refresh: () -> Unit,
// retries any failed requests.
val retry: () -> Unit
)
// A class to hold the network state
@Suppress("DataClassPrivateConstructor")
data class PagingNetworkState private constructor(
val status: Status,
val msg: String? = null
) {
companion object {
val LOADED = PagingNetworkState(Status.SUCCESS)
val LOADING = PagingNetworkState(Status.LOADING)
fun error(msg: String?) = PagingNetworkState(Status.ERROR, msg)
}
}
class MyModelPageKeyedDataSource(private val retryExecutor: Executor) :
PageKeyedDataSource<MyModelLastKey, MyModel>() {
// keep a function reference for the retry event
private var retry: (() -> Any)? = null
/**
* There is no sync on the state because paging will always call loadInitial first then wait
* for it to return some success value before calling loadAfter.
*/
val networkState = MutableLiveData<PagingNetworkState>()
val initialLoad = MutableLiveData<PagingNetworkState>()
fun retryAllFailed() {
val prevRetry = retry
retry = null
prevRetry?.let {
retryExecutor.execute { it.invoke() }
}
}
override fun loadBefore(
params: LoadParams<MyModelLastKey>,
callback: LoadCallback<MyModelLastKey, MyModel>
) {
// no-op
}
override fun loadInitial(
params: LoadInitialParams<MyModelLastKey>,
callback: LoadInitialCallback<MyModelLastKey, MyModel>
) {
val request = Api.service.getMyModel()
networkState.postValue(PagingNetworkState.LOADING)
initialLoad.postValue(PagingNetworkState.LOADING)
// triggered by a refresh, we better execute sync
try {
val response = request.execute()
val originalData = response.body()?.data
retry = null
networkState.postValue(PagingNetworkState.LOADED)
initialLoad.postValue(PagingNetworkState.LOADED)
callback.onResult(originalData.orEmpty(),
null, response.body()?.lastEvaluatedKey)
} catch (exception: IOException) {
retry = { loadInitial(params, callback) }
val error = PagingNetworkState.error(exception.message ?: "unknown error")
networkState.postValue(error)
initialLoad.postValue(error)
}
}
override fun loadAfter(
params: LoadParams<MyModelLastKey>,
callback: LoadCallback<MyModelLastKey, MyModel>
) {
networkState.postValue(PagingNetworkState.LOADING)
Api.service.getMainContent().enqueue(object : Callback<MyModelResponse> {
override fun onFailure(call: Call<MyModelResponse>, t: Throwable) {
retry = { loadAfter(params, callback) }
networkState.postValue(PagingNetworkState.error(t.message ?: "unknown error"))
}
override fun onResponse(
call: Call<MyModelResponse>,
response: Response<MyModelResponse>
) {
if (response.isSuccessful) {
val data = response.body()?.data
retry = null
callback.onResult(data.orEmpty(), response.body()?.lastEvaluatedKey)
networkState.postValue(PagingNetworkState.LOADED)
} else {
retry = { loadAfter(params, callback) }
networkState.postValue(PagingNetworkState.error("error code: ${response.code()} : ${response.errorBody()}"))
}
}
})
}
}
class MyModelDataSourceFactory (private val retryExecutor: Executor) : DataSource.Factory<MyModelLastKey, MyModel>() {
val sourceLiveData = MutableLiveData<MyModelPageKeyedDataSource>()
override fun create(): DataSource<MyModelLastKey, MyModel> {
val source = MyModelPageKeyedDataSource(retryExecutor)
sourceLiveData.postValue(source)
return source
}
}
Repository:
fun getMyModel(): PagingListing<MyModel> {
val sourceFactory = MyModelDataSourceFactory(executors.networkExecutorService)
val livePagedList = sourceFactory.toLiveData(
pageSize = REQUEST_LIMIT,
fetchExecutor = executors.networkExecutorService
)
val refreshState = Transformations.switchMap(sourceFactory.sourceLiveData) {
it.initialLoad
}
return PagingListing(
pagedList = livePagedList,
networkState = Transformations.switchMap(sourceFactory.sourceLiveData) {
it.networkState
},
refreshState = refreshState,
refresh = {
sourceFactory.sourceLiveData.value?.invalidate()
},
retry = {
sourceFactory.sourceLiveData.value?.retryAllFailed()
}
)
}
ViewModel:
private val _myModelList : MutableLiveData<PagingListing<MyModel>> = MutableLiveData(repo.getMyModel())
val myModelList = Transformations.switchMap(_myModelList) { it.pagedList }
val networkState = Transformations.switchMap(_myModelList) { it.networkState }
fun refresh() {
_myModelList.value?.refresh?.invoke()
}
fun retry() {
_myModelList.value?.retry?.invoke()
}
Fragment:
viewModel.myModelList.observe(viewLifecycleOwner, Observer {
adapter.submitList(it)
})
Adapter:
class MyModelAdapter(
private val onMyModelItemClickListener: ((myModel: MyModel) -> Unit),
private val retryCallback: () -> Unit
) :
PagedListAdapter<MyModel, RecyclerView.ViewHolder>(
COMPARATOR_NOTIFICATIONS
) {
companion object {
private val COMPARATOR_MY_MODEL = object : DiffUtil.ItemCallback<MyModel>() {
override fun areContentsTheSame(oldItem: MyModel, newItem: MyModel): Boolean =
oldItem == newItem
override fun areItemsTheSame(oldItem: MyModel, newItem: MyModel): Boolean =
oldItem.id == newItem.id
}
}
private var networkState: PagingNetworkState? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
R.layout.li_my_model -> MyModelViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.li_my_model, parent, false)
)
R.layout.li_paging_network_state -> NetworkStateItemViewHolder.create(
parent,
retryCallback
)
else -> throw IllegalArgumentException("unknown view type $viewType")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (getItemViewType(position)) {
R.layout.li_my_model -> (holder as MyModelViewHolder).bind(
getItem(position),
onMyModelItemClickListener
)
R.layout.li_paging_network_state -> (holder as NetworkStateItemViewHolder).bindTo(
networkState
)
}
}
private fun hasExtraRow() = networkState != null && networkState != PagingNetworkState.LOADED
override fun getItemViewType(position: Int): Int {
return if (hasExtraRow() && position == itemCount - 1) {
R.layout.li_paging_network_state
} else {
R.layout.li_my_model
}
}
override fun getItemCount(): Int {
return super.getItemCount() + if (hasExtraRow()) 1 else 0
}
fun setNetworkState(newNetworkState: PagingNetworkState?) {
val previousState = this.networkState
val hadExtraRow = hasExtraRow()
this.networkState = newNetworkState
val hasExtraRow = hasExtraRow()
if (hadExtraRow != hasExtraRow) {
if (hadExtraRow) {
notifyItemRemoved(super.getItemCount())
} else {
notifyItemInserted(super.getItemCount())
}
} else if (hasExtraRow && previousState != newNetworkState) {
notifyItemChanged(itemCount - 1)
}
}
class MyModelViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val title: TextView = itemView.findViewById(R.id.tv_title)
...
fun bind(
myModel: MyModel?,
onMyModelItemClickListener: (myModel: MyModel) -> Unit
) {
myModel?.let {
title.text = myModel.title
...
itemView.setOnClickListener {
onMyModelItemClickListener.invoke(myModel)
}
}
}
}
class NetworkStateItemViewHolder(view: View, private val retryCallback: () -> Unit) :
RecyclerView.ViewHolder(view) {
private val progressBar = view.findViewById<ProgressBar>(R.id.progress_bar)
private val retry = view.findViewById<Button>(R.id.retry_button)
private val errorMsg = view.findViewById<TextView>(R.id.error_msg)
init {
retry.setOnClickListener {
retryCallback()
}
}
fun bindTo(networkState: PagingNetworkState?) {
progressBar.visibility = toVisibility(networkState?.status == Status.LOADING)
retry.visibility = toVisibility(networkState?.status == Status.ERROR)
errorMsg.visibility = toVisibility(networkState?.msg != null)
errorMsg.text = networkState?.msg
}
companion object {
fun create(parent: ViewGroup, retryCallback: () -> Unit): NetworkStateItemViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.li_paging_network_state, parent, false)
return NetworkStateItemViewHolder(view, retryCallback)
}
fun toVisibility(constraint: Boolean): Int {
return if (constraint) {
View.VISIBLE
} else {
View.GONE
}
}
}
}
}
li_paging_network_state.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/error_msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />
<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<Button
android:id="@+id/retry_button"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/retry_message" />
</LinearLayout>