Blog Infos
Author
Published
Topics
Author
Published

 

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.

RecyclerView Demo Video

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.

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

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

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

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

 

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

You can contact me on,

This article was originally published on proandroiddev.com on January 05, 2023

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
This tutorial teaches us how to display the most popular YouTube video content in…
READ MORE
blog
RecyclerView is a really cool and powerful tool to display list(s) of content on Android.…
READ MORE
blog
Hi everyone! We (Kaspresso Team and AvitoTech) are back with more about automated Android testing. Previously…
READ MORE
blog
Ever wondered how to implement a synchronizer between Android’s RecyclerView and TabLayout? What are…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu