Expandable Recycler View in Android
Hi guys !! This is my 4th article on medium after a long time. I would like to give you a heads up about my previous posts since I am not doing open source work in form of libraries you will not be notified by Gradle if any significant improvement was made to the code or a new feature was added. So I would suggest to watch the repositories by clicking on watch and select watching option for all sorts of notifications or check my github often. The reason I don’t create a library is to spare you any problem when you obfuscate,shrink or minify your code and to prevent presence of dead code in your app .This is a short and helpful article If you frequently need to develop an expandable listing feature through out your app. I came up with great way which involved developing an abstract dynamic adapter implementation which can be extended to use the expandable listing feature for any number of your required listings. Also I only did function proto-typing here because whole code base is completely documented.
Alright let’s dive in.
Pre-Requisites
RecyclerView, Kotlin (Co-routines,Experimental Features(Layout Container)) Generics, Espresso (UI testing).
Summary
The whole idea which creates,iterates over the dynamic expandable list module is to enforce code re-usability and prevent code duplication.
Basically, Its an abstract adapter which can be directly extended to avail above functionality as many times needed.
Although left open to new feature addition, basic features of the functionality are listed below:
- One expandable list adapter for every usage.
- O(n) Asynchronous Expansion & Collapse using Kotlin Co-routines.
- Synchronous Expansion & Collapse for single item.
- Accommodates every list type which extends ExpandableGroup class.
- Add/remove group.
- Click Listeners.
- Single Expansion.
- Vertical/Horizontal expansion.
- Its not a fixed dependency to be included in your project to increase redundancy.
- Its flexible to be converted in any library/SDK or modular form as per your requirement.
- Modifications/Enhancements can be made as required.
- Highly decoupled,optimised and clean code.
- No Obfuscation Required (Proguard/Dexguard).
- Complete Documentation.
- UI tested (Espresso).
- It would be a part of your project while not implying any 3rd-party involvement.
As mentioned above, The whole code base is thoroughly documented but I will provide a brief overview of each related method and class in abstract adapter class below.
private fun initializeChildRecyclerView(childRecyclerView: RecyclerView?)
The expandable list adapter I cooked works with 2 recycler views. The above method initializes the inner recycler view used by the expanded listing. The layout used by main recycler view contains the inner recycler view. If multiple recycler views are found the first one to be declared in XML will be used. If no inner recycler view is found an error is logged only no exception is thrown. Exception should be thrown when you incorporate this in a real project for code clarity and a good design.
private fun onCreateParentView(parent: ViewGroup, viewType: Int):PVH
Above method creates the view holder that your extended adapter provides for the main recycler view and adds click listener to the entire row. The click listener corresponds to expand and collapse events.
private fun collapseAllGroups()
Above method is self-explanatory. It collapses all expanded groups.
private fun reverseExpandableState(expandableGroup: ExpandableType)
Above method just alters the state of expandable group. If its expanded its collapsed. If its collapsed its expanded.
private fun collapseAllExcept(position: Int)
Above method collapses all groups except the group at provided position.
private fun handleSingleExpansion(position: Int)
Above method provides and handles the single expansion feature only if its enabled.
private fun handleExpansion(expandableGroup: ExpandableType, position: Int)
Above method handles expansion for provided group at position and notifies the adapter of the change in the state to update UI to show a group was expanded or collapsed.
private fun setupChildRecyclerView(holder: PVH, position: Int)
Above method is called in binding (OnBindViewHolder) of main recycler view’s adapter which initializes the inner recycler view (attaches adapter and adds click listener)
fun setExpanded(expanded: Boolean)
Above public method is useful to set the entire list as expanded or collapsed and notifying the adapter if its attached to main recycler view. The method can be intensive so it relies on Kotlin co-routines. The co-routine consists of two jobs. The part of list iteration to change state asynchronously is 1st and then notifying the adapter is 2nd also both jobs execute synchronously.
fun addGroup(expandableGroup: ExpandableType, expanded: Boolean = false, position: Int = -1)
Above method adds a new group at the specified position or at the end if not provided then it notifies the adapter if required. For now this always runs on UI thread so I will not suggest looping on it.
fun removeGroup(position: Int)
Above method removes a group from specified position and notifies adapter if required. Always runs on UI thread same as its counter addGroup looping on it is not advised.
Why aren’t the above 2 methods Async ?
Because they add/remove only one group at a time. But you can modify the behaviour yourself to run async as well or even add more methods to add/remove more groups.Only the adapter notifying methods require to be called on the UI thread.
private fun View.getRecyclerView(): RecyclerView?
Above is a utility method used to find the 1st occurence of inner recycler view in main recycler view’s row layout XML file. This will return null and log an error if an inner recycler view is not found.
private inner class ChildListAdapter(private val expandableGroup: ExpandableType, private val parentViewHolder: PVH, private val position: Int, private val onChildRowCreated: (ViewGroup, Int) -> CVH ) : RecyclerView.Adapter<CVH>()
Above class is used as the adapter for the expanded listing or inner recycler view. It’s not meant to be used outside its defined scope.
abstract class ExpandableViewHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView), LayoutContainer
Above class is to be used when you are writing your own view holder for parent listing layout you must extend this class instead of default one.
abstract class ExpandedViewHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView),LayoutContainer
Above class is to be used when you are writing your own view holder for child listing layout you must extend this class instead of default one.
abstract fun onCreateParentViewHolder(parent: ViewGroup, viewType: Int): PVH
Above method is required to be implemented in your own adapter which extends ExpandableRecyclerViewAdapter for retrieving your parent list view holder.
abstract fun onBindParentViewHolder(parentViewHolder: PVH, expandableType: ExpandableType, position: Int )
Above method is required to be implemented in your own adapter which extends ExpandableRecyclerViewAdapter to set your binding logic for parent list.
abstract fun onCreateChildViewHolder(child: ViewGroup, viewType: Int): CVH
Above method is required to be implemented in your own adapter which extends ExpandableRecyclerViewAdapter for retrieving your child list view holder.
abstract fun onBindChildViewHolder(childViewHolder: CVH, expandedType: ExpandedType, expandableType: ExpandableType, position: Int )
Above method is required to be implemented in your own adapter which extends ExpandableRecyclerViewAdapter to set your binding logic for child list.
abstract fun onExpandableClick(expandableViewHolder: PVH, expandableType: ExpandableType)
Above method is always triggered when parent list item is expanded or collapsed it will be implemented by your adapter to handle your click event.
abstract fun onExpandedClick(expandableViewHolder: PVH, expandedViewHolder: CVH, expandedType: ExpandedType, expandableType: ExpandableType )
Above method is always triggered when the child list item is clicked.
protected open fun isSingleExpanded() = false
Last but not least,above method can be overridden in your adapter if you want single expansion to occur only by default its disabled.
Finally, the object that you want to use as an expandable group must extend the below class.
abstract class ExpandableGroup<out E> { /** * returns a list of provided type to be used for expansion. */ abstract fun getExpandingItems(): List<E>
/** * Specifies if you want to show the UI in expanded form. */
var isExpanded = false }
When extending the class, you must also provide the object type for the child listing as well. Then when you provide your implementation for getExpandingItems method it will be used as the child listing for the parent object created.
That’s all on the code side. See a simple example here.
Why to use this ?
Many 3rd party libraries and medium blogs I came across relied only on single recycler view but in return are taxing. Because they are not recycling views for child listings but rather creating new views and using extra data structures to carefully contain the state of items which is not memory efficient.
Why use 2 recycler views ? Given other solutions use one only.
The most ideal and elegant example demonstrating 2 recycler views representing the proposed structure above is the Google PlayStore app currently in your Android phone.
How to run ?
You can find the project on Github. It contains a Kotlin and Java Activity and an example adapter which displays an expandable group listing using recycler view declared in each activity’s XML layout which extends the Dynamic Expandable listing Adapter class also note not to attach any layout manager to recycler view as its already attached. You can clone the project in Android Studio to run it.
Last Step
Many thanks to everyone who reads this and even more to those who used this. My all articles can be found in my profile here. You can follow by watching my repositories on Github and also reach me at LinkedIn or Facebook. You can greatly help by starring and forking this project on github.