1

Everything You Need to Know About RecyclerView

 1 year ago
source link: https://proandroiddev.com/everything-you-need-to-know-about-recyclerview-577bf9248298
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Everything You Need to Know About RecyclerView

1*jWInruQouJjn6Dt-L57jxw.png

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 show Error view type.
  • When isLoading is true, we’ll show Loading view type.
  • When isPaginating is true and position is equal to list size, we’ll show PaginationLoading view type.
  • When canPaginate is false and position is equal to list size, we’ll show PaginationExhaust view type.

getItemViewType returns the view type of the item at position 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.

1*bvE4rC5o5KZc0Tn_Nj1E6A.png

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.

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.

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 check NetworkResponse type and call necessary function with recyclerViewAdapter.

Same logic in rvOperation.observe,

  • We check the response in when(response) and call necessary function.
    Only difference is, we show or dismiss loading dialog.
1*RP4OjGsWZjwLsRMm_lvpLg.png

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. 👋👋

0*lyAh0Rio9OEMN_1t

Full Code

MrNtlu/RecyclerView-Guide (github.com)

Sources:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK