Before we start, you can check the Youtube Demo Video to understand what we’ll do in this article. We’ll be implementing RecyclerView with,
- State Management with multiple view types (Loading, Error, Pagination etc.)
- View Binding
- Pull to Refresh
- DiffUtil
- Pagination
- Shimmer Loading Animation
- Scroll to Top FAB
- Favorite Button
- Error Handling
- Popup Menu
- Delete/Update/Insert Item
I wanted to cover everything that you might need in your project. Hopefully this article will help you understand more about RecyclerView. It’s open to improvements and feedback. Please do let me know if you have any.
Table of Contents
Prerequisites
We won’t use anything fancy in this article but I’ll assume that you know basics of RecyclerView, View Holder, Live Data and how to implement it.
I’ll skip some parts of the code, so if you want to see the source code, you can find the link at the bottom of this article.
Getting Started
App level build.gradle
file,
android { //... buildFeatures { viewBinding true } } dependencies { //... implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' implementation "com.facebook.shimmer:shimmer:0.5.0" }
Response Wrapper,
sealed class NetworkResponse<out T> { data class Loading( val isPaginating: Boolean = false, ): NetworkResponse<Nothing>() data class Success<out T>( val data: T, val isPaginationData: Boolean = false, ): NetworkResponse<T>() data class Failure( val errorMessage: String, val isPaginationError: Boolean = false, ): NetworkResponse<Nothing>() }
You can learn more about it from this link, Handling success data and error callback responses from a network for Android projects using Sandwich | by Jaewoong Eum | ProAndroidDev
This enum class to help us with view types, you’ll understand it better when we actually use it. Usage is very simple.
enum class RecyclerViewEnum(val value: Int) { Empty(0), Loading(1), Error(2), View(3), PaginationLoading(4), PaginationExhaust(5), }
Operation
will help us with Insert, Delete and Update “operations”, it’ll make things easier for us to handle upcoming changes.
data class Operation<out T>( val data: T, val operationEnum: OperationEnum ) enum class OperationEnum { Insert, Delete, Update, }
Finally, RecyclerViewModel
data class,
data class RecyclerViewModel( var id: String, var content: String = "", var isLiked: Boolean = false, ) { val text: String get() = "ID: $id" override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is RecyclerViewModel) return false return other.id == id } override fun hashCode() = Objects.hash(id) }
DiffUtil
In the past, I used to use notifyDataSetChanged
and it was the easiest way to update RecyclerView but I’ve noticed that it created performance issues and caused bad user experience.
This event does not specify what about the data set has changed, forcing any observers to assume that all existing items and structure may no longer be valid. LayoutManagers will be forced to fully rebind and relayout all visible views.
If you are writing an adapter it will always be more efficient to use the more specific change events if you can. Rely on
notifyDataSetChanged()
as a last resort.
First, it updates all RecyclerView and causes performance issues, and it should be the last resort as Android documentation points out.
Second, it has no animation and all list blinks and causes bad user experience.
DiffUtil is a utility class that calculates the difference between two lists and outputs a list of update operations that converts the first list into the second one.
DiffUtil comes for the rescue. It calculates the changes in the list and updates only necessary items.
class RecyclerViewDiffUtilCallBack( private val oldList: List<RecyclerViewModel>, private val newList: List<RecyclerViewModel>, ): DiffUtil.Callback() { override fun getOldListSize() = oldList.size override fun getNewListSize() = newList.size override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return oldList[oldItemPosition].id == newList[newItemPosition].id } override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return when { oldList[oldItemPosition].id != newList[newItemPosition].id -> false oldList[oldItemPosition].content != newList[newItemPosition].content -> false oldList[oldItemPosition].isLiked != newList[newItemPosition].isLiked -> false else -> true } } }
areContentsTheSame is Called to check whether two items have the same data.
areItemsTheSame is called to check whether two objects represent the same item.
Adapters
Before we start, we’ll create two adapters. First is generic BaseAdapter<T>
which most of the implementation will be here and the second one is RecyclerViewAdapter
. We could use single adapter but having BaseAdapter
makes things a lot easier for future usages. If you have more than one RecyclerView with similar necessity, instead of repeating the same codes, we can create base adapter and extend from it.
@Suppress("UNCHECKED_CAST") @SuppressLint("NotifyDataSetChanged") abstract class BaseAdapter<T>(open val interaction: Interaction<T>): RecyclerView.Adapter<RecyclerView.ViewHolder>() { private var errorMessage: String? = null var isLoading = true var isPaginating = false var canPaginate = true protected var arrayList: ArrayList<T> = arrayListOf() protected abstract fun handleDiffUtil(newList: ArrayList<T>) override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when(getItemViewType(position)) { RecyclerViewEnum.View.value -> { (holder as ItemViewHolderBind<T>).bind(arrayList[position], position, interaction) } RecyclerViewEnum.Error.value -> { (holder as ErrorViewHolderBind<T>).bind(errorMessage, interaction) } RecyclerViewEnum.PaginationExhaust.value -> { (holder as PaginationExhaustViewHolderBind<T>).bind(interaction) } } } override fun getItemViewType(position: Int) : Int { return if (isLoading) RecyclerViewEnum.Loading.value else if (errorMessage != null) RecyclerViewEnum.Error.value else if (isPaginating && position == arrayList.size) RecyclerViewEnum.PaginationLoading.value else if (!canPaginate && position == arrayList.size) RecyclerViewEnum.PaginationExhaust.value else if (arrayList.isEmpty()) RecyclerViewEnum.Empty.value else RecyclerViewEnum.View.value } override fun getItemCount(): Int { return if (isLoading || errorMessage != null || arrayList.isEmpty()) 1 else { if (arrayList.isNotEmpty() && !isPaginating && canPaginate) //View Type arrayList.size else arrayList.size.plus(1) } } fun setError(errorMessage: String, isPaginationError: Boolean) { //... } fun setLoadingView(isPaginating: Boolean) { //... } fun handleOperation(operation: Operation<T>) { //... } fun setData(newList: ArrayList<T>, isPaginationData: Boolean = false) { //... } private fun setState(rvEnum: RecyclerViewEnum) { //... }
We’ll implement commented functions later. Let’s check one by one.
handleDiffUtil
will be implemented in each adapter with corresponding models. We’ll just keep it as abstract.
errorMessage
, isLoading
, isPaginating
and canPaginate
values will be used for view types.
- When
errorMessage
is not null, we’ll showError
view type. - When
isLoading
is true, we’ll showLoading
view type. - When
isPaginating
is true and position is equal to list size, we’ll showPaginationLoading
view type. - When
canPaginate
is false and position is equal to list size, we’ll showPaginationExhaust
view type.
getItemViewType
returns the view type of the item atposition
for the purposes of view recycling. Consider using id resources to uniquely identify item view types.
In getItemViewType
we are using RecyclerViewEnum
that we’ve created earlier. We could just pass numbers like 0, 1, 2 etc. but to make things easier to read we are using enum class.
Let’s start implementing commented functions.
fun setErrorView(errorMessage: String, isPaginationError: Boolean) { if (isPaginationError) { setState(RecyclerViewEnum.PaginationExhaust) notifyItemInserted(itemCount) } else { setState(RecyclerViewEnum.Error) this.errorMessage = errorMessage notifyDataSetChanged() } } fun setLoadingView(isPaginating: Boolean) { if (isPaginating) { setState(RecyclerViewEnum.PaginationLoading) notifyItemInserted(itemCount) } else { setState(RecyclerViewEnum.Loading) notifyDataSetChanged() } }
Both setErrorView
and setLoadingView
have similar implementations for different cases. If it’s pagination, we call notifyItemInserted
and append corresponding view to the end of the list. If it’s not pagination, we set the state and use notifyDataSetChanged
. We are minimizing usage of notifyDataSetChanged
but in this case it’s necessary.
fun handleOperation(operation: Operation<T>) { val newList = arrayList.toMutableList() when(operation.operationEnum) { OperationEnum.Insert -> { newList.add(operation.data) } OperationEnum.Delete -> { newList.remove(operation.data) } OperationEnum.Update -> { val index = newList.indexOfFirst { it == operation.data } newList[index] = operation.data } } handleDiffUtil(newList as ArrayList<T>) }
First, we copy the list with toMutableList
to prevent reference pass and take the necessary action according to the operation. After that we pass new list to handleDiffUtil
and DiffUtil does its “magic”.
fun setData(newList: ArrayList<T>, isPaginationData: Boolean = false) { setState(RecyclerViewEnum.View) if (!isPaginationData) { if (arrayList.isNotEmpty()) arrayList.clear() arrayList.addAll(newList) notifyDataSetChanged() } else { notifyItemRemoved(itemCount) newList.addAll(0, arrayList) handleDiffUtil(newList) } }
This function will be used to insert new data to the list. If we are not paginating, we clear the list and add all new items and finally refresh the list. If we are paginating, we notifyItemRemoved
to remove pagination view at the end of the list and add new items and notify DiffUtil.
private fun setState(rvEnum: RecyclerViewEnum) { when(rvEnum) { RecyclerViewEnum.Empty -> { isLoading = false isPaginating = false errorMessage = null } RecyclerViewEnum.Loading -> { isLoading = true isPaginating = false errorMessage = null canPaginate = true } RecyclerViewEnum.Error -> { isLoading = false isPaginating = false } RecyclerViewEnum.View -> { isLoading = false isPaginating = false errorMessage = null } RecyclerViewEnum.PaginationLoading -> { isLoading = false isPaginating = true errorMessage = null } RecyclerViewEnum.PaginationExhaust -> { isLoading = false isPaginating = false canPaginate = false } } }
Finally, this function is simply setting the values according to the state. Again, this function is to makes easier to understand.
That’s it for BaseAdapter
, let’s implement RecyclerViewAdapter
.
@Suppress("UNCHECKED_CAST") class RecyclerViewAdapter( override val interaction: Interaction<RecyclerViewModel>, private val extraInteraction: RecyclerViewInteraction, ): BaseAdapter<RecyclerViewModel>(interaction) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when(viewType) { RecyclerViewEnum.View.value -> ItemViewHolder(CellItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), extraInteraction) RecyclerViewEnum.Loading.value -> LoadingViewHolder(CellLoadingBinding.inflate(LayoutInflater.from(parent.context), parent, false)) RecyclerViewEnum.PaginationLoading.value -> PaginationLoadingViewHolder(CellPaginationLoadingBinding.inflate(LayoutInflater.from(parent.context), parent, false)) RecyclerViewEnum.PaginationExhaust.value -> PaginationExhaustViewHolder(CellPaginationExhaustBinding.inflate(LayoutInflater.from(parent.context), parent, false)) RecyclerViewEnum.Error.value -> ErrorItemViewHolder(CellErrorBinding.inflate(LayoutInflater.from(parent.context), parent, false)) else -> EmptyViewHolder(CellEmptyBinding.inflate(LayoutInflater.from(parent.context), parent, false)) } } override fun handleDiffUtil(newList: ArrayList<RecyclerViewModel>) { val diffUtil = RecyclerViewDiffUtilCallBack( arrayList, newList, ) val diffResults = DiffUtil.calculateDiff(diffUtil, true) arrayList = newList.toList() as ArrayList<RecyclerViewModel> diffResults.dispatchUpdatesTo(this) } }
Since we’ve made all the implementations in BaseAdapter
, it’s very easy to create adapter. We only need to pass view holders and implement handleDiffUtil
.
Small notes, interaction
and extraInteraction
are interfaces to handle actions. You can check them from the source code.
View Type Designs
View Holder
It’s already a long article, so I’ll skip the implementations of view holders except the ItemViewHolder. You can check other view holders from the source code. If you have any questions, feel free to ask.
Job Offers
class ItemViewHolder( private val binding: CellItemBinding, private val extraInteraction: RecyclerViewInteraction, ): RecyclerView.ViewHolder(binding.root), ItemViewHolderBind<RecyclerViewModel> { override fun bind(item: RecyclerViewModel, position: Int, interaction: Interaction<RecyclerViewModel>) { val text = "Position: $position ${item.text}" binding.contentTV.text = item.content.ifBlank { text } binding.idTV.text = item.id binding.favButton.setImageDrawable(ContextCompat.getDrawable(binding.root.context, if (item.isLiked) R.drawable.ic_heart else R.drawable.ic_empty_heart)) binding.moreButton.setOnClickListener { val popupMenu = PopupMenu(binding.root.context, binding.moreButton) popupMenu.inflate(R.menu.popup_menu) popupMenu.setOnMenuItemClickListener { when(it.itemId) { R.id.delete -> { try { extraInteraction.onDeletePressed(item) } catch (e: Exception) { Toast.makeText( binding.root.context, "Please wait before doing any operation.", Toast.LENGTH_SHORT ).show() } return@setOnMenuItemClickListener true } R.id.update -> { try { extraInteraction.onUpdatePressed(item) } catch (e: Exception) { Toast.makeText( binding.root.context, "Please wait before doing any operation.", Toast.LENGTH_SHORT ).show() } return@setOnMenuItemClickListener true } else -> { return@setOnMenuItemClickListener false } } } popupMenu.show() } binding.favButton.setOnClickListener { extraInteraction.onLikePressed(item) } binding.root.setOnClickListener { try { interaction.onItemSelected(item) } catch (e: Exception) { Toast.makeText( binding.root.context, "Please wait before doing any operation.", Toast.LENGTH_SHORT ).show() } } } }
In bind
function, we set the UI elements and click events. Again, there is nothing “magical” about this.
Repository & View Model
const val PAGE_SIZE = 50 class MainRepository { private val tempList = arrayListOf<RecyclerViewModel>().apply { for (i in 0..PAGE_SIZE) { add(RecyclerViewModel(UUID.randomUUID().toString(), "Content $i"),) } } fun fetchData(page: Int): Flow<NetworkResponse<ArrayList<RecyclerViewModel>>> = flow { emit(NetworkResponse.Loading(page != 1)) kotlinx.coroutines.delay(2000L) try { if (page == 1) emit(NetworkResponse.Success(tempList.toList() as ArrayList<RecyclerViewModel>)) else { val tempPaginationList = arrayListOf<RecyclerViewModel>().apply { for (i in 0..PAGE_SIZE) { add(RecyclerViewModel(UUID.randomUUID().toString(), "Content ${i * 2}"),) } } if (page < 4) { emit(NetworkResponse.Success( tempPaginationList, isPaginationData = true, )) } else { emit(NetworkResponse.Failure( "Pagination failed.", isPaginationError = true )) } } } catch (e: Exception) { emit(NetworkResponse.Failure( e.message ?: e.toString(), isPaginationError = page != 1 )) } }.flowOn(Dispatchers.IO) fun deleteData(item: RecyclerViewModel): Flow<NetworkResponse<Operation<RecyclerViewModel>>> = flow { kotlinx.coroutines.delay(1000L) try { emit(NetworkResponse.Success(Operation(item, OperationEnum.Delete))) } catch (e: Exception) { emit(NetworkResponse.Failure(e.message ?: e.toString())) } }.flowOn(Dispatchers.IO) fun updateData(item: RecyclerViewModel): Flow<NetworkResponse<Operation<RecyclerViewModel>>> = flow { kotlinx.coroutines.delay(1000L) try { item.content = "Updated Content ${(0..10).random()}" emit(NetworkResponse.Success(Operation(item, OperationEnum.Update))) } catch (e: Exception) { emit(NetworkResponse.Failure(e.message ?: e.toString())) } }.flowOn(Dispatchers.IO) fun toggleLikeData(item: RecyclerViewModel): Flow<NetworkResponse<Operation<RecyclerViewModel>>> = flow { kotlinx.coroutines.delay(1000L) try { item.isLiked = !item.isLiked emit(NetworkResponse.Success(Operation(item, OperationEnum.Update))) } catch (e: Exception) { emit(NetworkResponse.Failure(e.message ?: e.toString())) } }.flowOn(Dispatchers.IO) fun insertData(item: RecyclerViewModel): Flow<NetworkResponse<Operation<RecyclerViewModel>>> = flow { emit(NetworkResponse.Loading()) kotlinx.coroutines.delay(1000L) try { emit(NetworkResponse.Success(Operation(item, operationEnum = OperationEnum.Insert))) } catch (e: Exception) { emit(NetworkResponse.Failure(e.message ?: e.toString())) } }.flowOn(Dispatchers.IO) }
We’ll try to pretend like we are making a network request, waiting for it to finish and present the data. We’ll use flows to present NetworkResponse
.
For example, in fetchData
first we send loading state with NetworkResponse.Loading
and wait 2 seconds. After waiting, if page number is 1 which means we are either refreshing or it’s initial fetch, we send NetworkResponse.Success
with data. If page number is something other than 1, it means we are paginating and we send NetworkResponse.Success
with isPaginationData = true
.
Since we mimic the network request, if page number is 4, we exhaust the pagination and sent NetworkResponse.Failure
with isPaginationError = true
to show pagination exhaust view.
We have a similar logic for other functions too. Only difference is, in some cases we use NetworkResponse
with Operation
. These functions are used to mimic insert, update and delete.
class MainViewModel : ViewModel() { private val repository = MainRepository() private val _rvList = MutableLiveData<NetworkResponse<ArrayList<RecyclerViewModel>>>() val rvList: LiveData<NetworkResponse<ArrayList<RecyclerViewModel>>> = _rvList private val _rvOperation = MutableLiveData<NetworkResponse<Operation<RecyclerViewModel>>>() val rvOperation: LiveData<NetworkResponse<Operation<RecyclerViewModel>>> = _rvOperation private var page: Int = 1 init { fetchData() } fun refreshData() { page = 1 fetchData() } fun fetchData() = viewModelScope.launch(Dispatchers.IO) { repository.fetchData(page).collect { state -> withContext(Dispatchers.Main) { _rvList.value = state if (state is NetworkResponse.Success) { page += 1 } } } } fun deleteData(item: RecyclerViewModel) = viewModelScope.launch(Dispatchers.IO) { repository.deleteData(item).collect { state -> withContext(Dispatchers.Main) { _rvOperation.value = state } } } fun updateData(item: RecyclerViewModel) = viewModelScope.launch(Dispatchers.IO) { repository.updateData(item).collect { state -> withContext(Dispatchers.Main) { _rvOperation.value = state } } } fun toggleLikeData(item: RecyclerViewModel) = viewModelScope.launch(Dispatchers.IO) { repository.toggleLikeData(item).collect { state -> withContext(Dispatchers.Main) { _rvOperation.value = state } } } fun insertData(item: RecyclerViewModel) = viewModelScope.launch(Dispatchers.IO) { repository.insertData(item).collect { state -> withContext(Dispatchers.Main) { _rvOperation.value = state } } } fun throwError() = viewModelScope.launch(Dispatchers.Main) { _rvList.value = NetworkResponse.Failure("Error occured!") } fun exhaustPagination() = viewModelScope.launch(Dispatchers.Main) { _rvList.value = NetworkResponse.Failure( "Pagination Exhaust", true ) } }
View model is only here to present the data to UI. We’ll have two LiveData, rvList
and rvOperation
. rvList
will be used to listen changes on our list and rvOperation
will be used to listen operations e.g., new item gets inserted and we’ll listen to that operation and handle it in UI.
UI
class MainFragment : BaseFragment<FragmentMainBinding>() { private lateinit var viewModel: MainViewModel private var recyclerViewAdapter: RecyclerViewAdapter? = null private var loadingDialog: Dialog? = null //OnCreate and OnCreateView commented. override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setDialog(view.context) setListeners() setRecyclerView() setObservers() } private fun setDialog(context: Context) { loadingDialog = Dialog(context) loadingDialog?.setCancelable(false) loadingDialog?.setContentView(R.layout.dialog_loading) loadingDialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) } private fun setListeners() { binding.swipeRefreshLayout.setOnRefreshListener { viewModel.refreshData() binding.swipeRefreshLayout.isRefreshing = false } binding.errorButton.setOnClickListener { viewModel.throwError() } binding.appendButton.setOnClickListener { if (recyclerViewAdapter?.canPaginate == true && recyclerViewAdapter?.isPaginating == false) viewModel.fetchData() binding.mainRV.scrollToPosition(recyclerViewAdapter?.itemCount ?: 0) } binding.insertButton.setOnClickListener { viewModel.insertData(RecyclerViewModel(UUID.randomUUID().toString())) } binding.paginateErrorButton.setOnClickListener { viewModel.exhaustPagination() } binding.fab.setOnClickListener { viewLifecycleOwner.lifecycleScope.launch { binding.mainRV.quickScrollToTop() } } } private fun setObservers() { viewModel.rvList.observe(viewLifecycleOwner) { response -> binding.swipeRefreshLayout.isEnabled = when (response) { is NetworkResponse.Success -> { true } is NetworkResponse.Failure -> { response.isPaginationError } else -> false } when(response) { is NetworkResponse.Failure -> { recyclerViewAdapter?.setErrorView(response.errorMessage, response.isPaginationError) } is NetworkResponse.Loading -> { recyclerViewAdapter?.setLoadingView(response.isPaginating) } is NetworkResponse.Success -> { recyclerViewAdapter?.setData(response.data, response.isPaginationData) } } } viewModel.rvOperation.observe(viewLifecycleOwner) { response -> when(response) { is NetworkResponse.Failure -> { if (loadingDialog?.isShowing == true) loadingDialog?.dismiss() } is NetworkResponse.Loading -> { if (recyclerViewAdapter?.isLoading == false) loadingDialog?.show() } is NetworkResponse.Success -> { if (loadingDialog?.isShowing == true) loadingDialog?.dismiss() recyclerViewAdapter?.handleOperation(response.data) } } } } private fun setRecyclerView() { //... Later } }
We’ll implement setRecyclerView
function later,
setListeners
function is to set click or refresh listeners. Most buttons are for testing purposes and not really necessary. binding.fab is scroll to top button.
quickScrollToTop is custom function by
. You can check his article from this link.
In rvList.observe
,
- We set the
swipeRefreshLayout.isEnabled
since we don’t want user to refresh again when we are already loading the data. - In
when(response)
, we checkNetworkResponse
type and call necessary function withrecyclerViewAdapter
.
Same logic in rvOperation.observe
,
- We check the response in
when(response)
and call necessary function.
Only difference is, weshow
ordismiss
loading dialog.
Loading Dialog
private fun setRecyclerView() { binding.mainRV.apply { val linearLayoutManager = LinearLayoutManager(context) layoutManager = linearLayoutManager addItemDecoration(DividerItemDecoration(context, linearLayoutManager.orientation)) recyclerViewAdapter = RecyclerViewAdapter(object: Interaction<RecyclerViewModel> { override fun onItemSelected(item: RecyclerViewModel) { Toast.makeText(context, "Item ${item.content}", Toast.LENGTH_SHORT).show() } override fun onErrorRefreshPressed() { viewModel.refreshData() } override fun onExhaustButtonPressed() { viewLifecycleOwner.lifecycleScope.launch { quickScrollToTop() } } }, object: RecyclerViewInteraction { override fun onUpdatePressed(item: RecyclerViewModel) { viewModel.updateData(item.copy()) } override fun onDeletePressed(item: RecyclerViewModel) { viewModel.deleteData(item) } override fun onLikePressed(item: RecyclerViewModel) { viewModel.toggleLikeData(item.copy()) } }) adapter = recyclerViewAdapter var isScrolling = false addOnScrollListener(object: RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) isScrolling = newState != AbsListView.OnScrollListener.SCROLL_STATE_IDLE } override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) val itemCount = linearLayoutManager.itemCount val lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition() if (lastVisibleItemPosition > PAGE_SIZE.plus(PAGE_SIZE.div(2)) && dy <= -75) { binding.fab.show() } else if (lastVisibleItemPosition <= PAGE_SIZE.plus(PAGE_SIZE.div(2)) || dy >= 60) { binding.fab.hide() } recyclerViewAdapter?.let { if ( isScrolling && lastVisibleItemPosition >= itemCount.minus(5) && it.canPaginate && !it.isPaginating ) { viewModel.fetchData() } } } }) } }
Finally, setRecyclerView
function. We create and set recyclerViewAdapter
to binding.mainRV
.
We also implement addScrollListener
which will be used to show/hide fab
and trigger the pagination.
- For
fab.show
, we check if we’ve scrolled enough and certain number of items visible. If so, if we’ve also scrolled certain number in negative direction.fab.hide
is the reverse of that. You can try yourself and set numbers yourself. - For pagination, we check if we are scrolling & if we are certain number before the last visible item & if
canPaginate
& if we are not already paginating.
Shimmer
We’ll separate shimmer into two parts, first will be normal layout part and second will be ShimmerFrameLayout
,
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginVertical="6dp"> <TextView android:id="@+id/shimmerIdTV" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="12dp" android:layout_marginTop="8dp" android:layout_marginEnd="12dp" android:textColor="@color/black" android:background="@color/shimmer_color" app:layout_constraintEnd_toStartOf="@+id/shimmerFavButton" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/shimmerContentTV" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="12dp" android:layout_marginTop="8dp" android:layout_marginEnd="12dp" android:layout_marginBottom="8dp" android:background="@color/shimmer_color" android:textColor="@color/black" android:textSize="14sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/shimmerFavButton" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/shimmerIdTV" /> <ImageView android:id="@+id/shimmerFavButton" android:layout_width="32dp" android:layout_height="32dp" android:layout_marginEnd="8dp" android:background="@color/shimmer_color" app:layout_constraintBottom_toBottomOf="@+id/shimmerMoreButton" app:layout_constraintEnd_toStartOf="@+id/shimmerMoreButton" app:layout_constraintTop_toTopOf="@+id/shimmerMoreButton" app:layout_constraintVertical_bias="0.407" /> <ImageView android:id="@+id/shimmerMoreButton" android:layout_width="32dp" android:layout_height="32dp" android:layout_marginEnd="8dp" android:background="@color/shimmer_color" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent"/> </androidx.constraintlayout.widget.ConstraintLayout>
simmerColor code is #BFBDBD
We’ve simply copied and pasted RecyclerView item design and added background=”@color/shimmer_color”
to each one of them.
<?xml version="1.0" encoding="utf-8"?> <com.facebook.shimmer.ShimmerFrameLayout android:id="@+id/shimmerLoadingLayout" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" app:shimmer_auto_start="true" app:shimmer_duration="1300"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <include layout="@layout/cell_shimmer"/> <include layout="@layout/cell_shimmer"/> <include layout="@layout/cell_shimmer"/> <include layout="@layout/cell_shimmer"/> <include layout="@layout/cell_shimmer"/> <include layout="@layout/cell_shimmer"/> <include layout="@layout/cell_shimmer"/> <include layout="@layout/cell_shimmer"/> <include layout="@layout/cell_shimmer"/> <include layout="@layout/cell_shimmer"/> <include layout="@layout/cell_shimmer"/> <include layout="@layout/cell_shimmer"/> <include layout="@layout/cell_shimmer"/> <include layout="@layout/cell_shimmer"/> <include layout="@layout/cell_shimmer"/> <include layout="@layout/cell_shimmer"/> <include layout="@layout/cell_shimmer"/> </LinearLayout> </com.facebook.shimmer.ShimmerFrameLayout>
Inside of ShimmerFrameLayout
, we set auto_start="true"
to start animation automatically. duration
is how long it takes to finis the animation. You can see more about it here.
You can decrease or increase the number of included layouts. I’ve tried to add as low as I can to cover whole screen. Less is better for performance, I think 🙂
That’s it! I hope it was useful. 👋👋
Full Code
MrNtlu/RecyclerView-Guide (github.com)
Sources:
- Quickly scroll to the top of a list | by Patrick Elmquist | Flat Pack Tech | Medium
- android — How to use ViewBinding in a RecyclerView.Adapter? — Stack Overflow
- DiffUtil — Improve RecyclerView’s Performance | Android Studio Tutorial — YouTube
- android — DiffUtil.ItemCallback doesn’t update item position (after a deleting) — Stack Overflow
- android — How to call suspend function from Fragment or Activity? — Stack Overflow
- android — How to disable “pull to refresh” action and use only indicator? — Stack Overflow
- android — Padding not working on ImageButton — Stack Overflow
- equals — kotlin — how to override hashcode — Stack Overflow
- android — Create Options Menu for RecyclerView-Item — Stack Overflow
- shimmer effect in android studio | shimmer effect recycler view android | Tech Projects — YouTube
- Buttons: floating action button — Material Design
- How to make custom dialog with rounded corners in android — Stack Overflow
This article was originally published on proandroiddev.com on January 05, 2023