Infinite List using Kotlin
This story describes about infinite list functionality using Kotlin.
Use case: For instance we are making an webservice call from mobile application to fetch data to be displayed in a RecyclerView. The back end webservice sends 300 or more rows at a time and pagination functionality is not implemented in backend.
Problems with the above use case: The problem with the above use case is we have to populate recyclerview with 300 rows using Adapter which is bad practice as all the elements are not visible at a time on Adapter .It’s not a good practice to populate adapter with 300 rows when user at a time can view maximum of 10 to 20 rows based on available screen height.
Solution for above use case: The above problem can be solved by implementing “Pagination” functionality with in the app .
Steps for Pagination implementation:
(1) Fetch all the rows from backend server using retrofit.
(2) Prepare data from the received data to display only 10 elements at a time on Recycler View.
(3) Implement ViewType functionality in recyclerview adapter : one view type displays 10rows and 11th row is displayed as “Loadmore”
(4) When clicked on “Loadmore” next set of elements are displayed which means 11 to 20 rows with 21st row as “Loadmore”.
(5) The above process is continued till we reach end of all the elements from the data received from webservice .
Sample Screen shots for the above implementation:
Implementation details:
Step 1: Create a data class that represents JSON response from backend. For demo purpose , the data is read from .json file of assets folder that contains 204 records of data.
class InfiniteModel : ArrayList<InfiniteModel.InfiniteModelItem>(){
data class InfiniteModelItem(
val completed: Boolean, // false
val id: Int, // 200
val title: String, // ipsam aperiam voluptates qui
val userId: Int // 10
)
}
Step 2: Prepare another model class with ViewTypes to be display data in with RecyclerView Adapter.
class InfiniteAdapterModel {
companion object{
const val VIEW_TYPE_DATA = 0
const val VIEW_TYPE_PROGRESS =1
}
var infiniteModelItem: InfiniteModel.InfiniteModelItem
var type : Int
constructor( type : Int, infiniteModelItem: InfiniteModel.InfiniteModelItem){
this.type = type
this.infiniteModelItem = infiniteModelItem
}
}
Step 3: Prepare data for adapter
fun prepareDataForAdapter(infiniteModel: InfiniteModel,startPosition:Int,size:Int): MutableList<InfiniteAdapterModel>{
var adapterModel = mutableListOf<InfiniteAdapterModel>()
for (item in startPosition until size) {
adapterModel.add(InfiniteAdapterModel(InfiniteAdapterModel.VIEW_TYPE_DATA,infiniteModel[item]))
}
adapterModel.add(InfiniteAdapterModel(InfiniteAdapterModel.VIEW_TYPE_PROGRESS,InfiniteModel.InfiniteModelItem(false,0,"",0)))
return adapterModel
}Note: startPositon is 0 and size is 10 to display initial list of 10 items
Step4: Create an interface to be implemented in fragment to fetch subsequent list items when clicked on “Loadmore” button in Adapter.
interface LoadMore {
fun loadMore(startPosition:Int,endPosition:Int,previousList:MutableList<InfiniteAdapterModel>, originalModel: InfiniteModel)
}
Step 5: ViewModel: Contains methods to fetch data from backend (in the sample using .json file located under assets folder ) and prepare data to be displayed in Adapter
class InfiniteListViewModel(private val dispatcher: CoroutineDispatcher, private val infiniteRepository: InfiniteRepository) : ViewModel(),
LifecycleObserver {
private val LOG_TAG = "InfiniteListViewModel"
var loading: MutableLiveData<Boolean> = MutableLiveData()
private val _obtainInfiniteListResponse= MutableLiveData<ResultOf<InfiniteModel>>()
val obtainInfiniteListResponse: LiveData<ResultOf<InfiniteModel>> = _obtainInfiniteListResponse
fun obtainInfiniteList(){
loading.postValue(true)
viewModelScope.launch(dispatcher){
try{
val infiniteListResponse = infiniteRepository.fetchInfiniteList()
if(infiniteListResponse.isSuccessful){
loading.postValue(false)
val response = infiniteListResponse.body()
response?.let {
_obtainInfiniteListResponse.postValue(ResultOf.Success(it))
}
}else{
loading.postValue(false)
_obtainInfiniteListResponse.postValue(ResultOf.Failure("Failed with Exception ", null))
}
}catch (e:Exception) {
e.printStackTrace()
_obtainInfiniteListResponse.postValue(ResultOf.Failure("Failed with Exception ${e.message} ", e))
}
}
}
fun prepareDataForAdapter(infiniteModel: InfiniteModel,startPosition:Int,size:Int): MutableList<InfiniteAdapterModel>{
var adapterModel = mutableListOf<InfiniteAdapterModel>()
for (item in startPosition until size) {
adapterModel.add(InfiniteAdapterModel(InfiniteAdapterModel.VIEW_TYPE_DATA,infiniteModel[item]))
}
adapterModel.add(InfiniteAdapterModel(InfiniteAdapterModel.VIEW_TYPE_PROGRESS,InfiniteModel.InfiniteModelItem(false,0,"",0)))
return adapterModel
}
Step 6: Adapter -> contains implementation to display data based on view type .
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType)
{
VIEW_TYPE_DATA ->
{//inflates row layout
val view = LayoutInflater.from(parent.context).inflate(R.layout.infinite_list_item,parent,false)
InfiniteListViewHolder(view)
}
VIEW_TYPE_PROGRESS ->
{//inflates progressbar layout
val view = LayoutInflater.from(parent.context).inflate(R.layout.infinite_loading,parent,false)
InfiniteLoadingViewHolder(view)
}
else -> throw IllegalArgumentException("Different View type")
}
}
override fun getItemCount(): Int = infiniteList.size
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val row = infiniteList[position]
when(row.type){
VIEW_TYPE_DATA ->{
if (holder is InfiniteListViewHolder)
{
holder.bind(infiniteList[position].infiniteModelItem)
}
}
VIEW_TYPE_PROGRESS -> {
if (holder is InfiniteLoadingViewHolder){
holder.loadingItem.setOnClickListener {
originalModel?.let { it1 ->
loadMore.loadMore(
startPosition as Int,
endPosition as Int,infiniteList, it1
)
}
}
}
}
}
}
override fun getItemViewType(position: Int): Int {
var viewtype = infiniteList[position].type
return when(viewtype)
{
1 -> VIEW_TYPE_PROGRESS
else -> VIEW_TYPE_DATA
}
}Note: VIEW_TYPE_PROGRESS -> Display row with "Loadmore" button
VIEW_TYPE_DATA -> Display 10 row data
Step 7: Logic to handle upper limit and lower limit of original data list to display 10 rows at a time.
previousList.removeAt(previousList.size -1) // Remove "Loadmore" button from last position
val scrollPosition = previousList.size
infiniteAdapter?.let {
it.notifyItemRemoved(scrollPosition)
var currentSize = scrollPosition
var nextLimit = currentSize+10
if(nextLimit<=originalModel.size) {
val preparedData = infiniteListViewModel.prepareDataForAdapter(
originalModel, currentSize,
nextLimit
// it.setFiniteList(startPosition,endPosition,infiniteAdapterModelList,originalModel,this)
)
previousList.addAll(preparedData)
it.setFiniteList(currentSize,nextLimit,previousList,originalModel,this)
it.notifyDataSetChanged()
}
else if(currentSize<originalModel.size){
nextLimit = originalModel.size
val preparedData = infiniteListViewModel.prepareDataForAdapter(
originalModel, currentSize,
nextLimit
// it.setFiniteList(startPosition,endPosition,infiniteAdapterModelList,originalModel,this)
)
previousList.addAll(preparedData)
currentSize = originalModel.size
it.setFiniteList(currentSize,nextLimit,previousList,originalModel,this)
it.notifyDataSetChanged()
}Note : else if condition handles the situation to handle remaining data from original data list .
Source code for the above functionality over GitHub: