I just started learning Android Development about two months ago and am still trying to fully figure things out. I needed to implement a Nested RecyclerView with onClick functionality. After reading through a lot of Stackoverflow posts with questions similar to what I was trying to do and trying out their suggestions and trying ideas from various tutorials that I found online, I was not able to find a solution that addressed all the points I needed addressed but was able to combine the ideas from different solutions to attain the functionality I needed but feel like, even though, my solution does what I need it to do, it probably is not the best solution. It feels more like a work around than something which is likely to be a best practice. I decided to take the ideas that I implemented for my App and create a basic example of those ideas in a sample project, which I will post later in this post in hopes that they might help someone who finds themselves stuck like I was and if others can improve on what I created so that it accomplishes the same goals in a more efficient way, then that would be even better.
** What I hoped to Accomplish: **
The image below shows a Parent RecyclerView(Blue) which displays a list of objects. Each Object(Pink) in this list has the following properties: Seller Name, A List of Products sold by this Seller, and Subtotal(sum of (cost of product X quantity of product) for all products sold by this seller). The Child RecyclerView(Yellow) shows the list of Product objects. Each Product object(Aqua Blue) has properties: Name, Price, and Quantity.
I wanted to know when a user clicks on any View used in the Nested RecyclerView. For example, if the user clicks on the text "Seller 1", I want to know that they clicked on the TextView representing Seller Name in the 0th index of the List of Objects used for the Parent RecyclerView. Or if they clicked on "Oranges" by "Seller 2", then I want to know that they Clicked on the 1st Index of the List of Objects used for the Parent RecyclerView and 0th Index of the List of Products used for Child Recycler View and the View that they clicked was the TextView which is used to display the Products name("Oranges").
The part I was having difficulty with was, for example, when a User clicked on say Oranges by Seller 1. I would know that they clicked on the Second Item in a Child RecyclerView list, but I wouldn't know which Parent RecyclerView item this Child corresponds to. I need to know both things, the index in the Parent RecyclerView list and index in Child RecyclerView list(if the clicked View is part of the Child RecyclerView) and which View specifically was clicked.
** The solution I came up with but am hoping you guys can help refine: **
I implemented the View.OnClickListener interface in my ViewHolder class for both the ParentRecyclerViewAdapter and ChildRecyclerViewAdapter. When onClick is called, I Log a statement stating which position and view was clicked. If the View that was clicked is a View from the Parent RecyclerView adapter, then I have enough information, but if the View that was clicked is a View in the Child RecyclerView, then I don't have enough information as I still need to know which item index from the Parent RecyclerView this Child belongs to. For this, I found help from a post by someone who created a class called RecyclerItemClickListener that calls a function onItemClicked() that reports the index of the Parent RecyclerView in which the Child item was clicked. This way, I now know the index in the Parent RecyclerView from using this persons class, and know the the index of which item was clicked in the Child RecyclerView and which View specifically was clicked by the onClick() function from the interface View.OnClickListener implemented by the ViewHolder class in the ChildRecyclerAdapter.
Unfortunately, since I was just trying out different ideas to find something that would work, I wasn't keeping track of the URLs for the posts/tutorials these ideas came from, but it shouldn't be hard to find the original sources using Google. Some of the code below is verbatim from the sources I consulted and some is stuff I put together.
** The Code **
class Order(
val sellerName: String = "",
val productsInOrder: List<Product> = listOf(),
val subTotal: Int = 0
) {
override fun toString(): String {
return "Seller:$sellerName, Products:$productsInOrder, SubTotal:$subTotal"
}
}
class Product(val titleOfProduct: String = "", val priceOfProduct: Int = 0, val quantityOfProduct: Int = 0) {
override fun toString(): String {
return "Product Name:$titleOfProduct, Price:$priceOfProduct, Quantity:$quantityOfProduct"
}
}
class MainActivity : AppCompatActivity() {
lateinit var listOfOrders: List<Order>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Set Touch Listeners for each Section in Parent RecyclerView:
binding.rvParentInMainActivityLayout.addOnItemTouchListener(
RecyclerItemClickListener(
this,
object : RecyclerItemClickListener.OnItemClickListener {
override fun onItemClick(v: View?, position: Int) {
Log.i("NOTE:MA#onItemClick","In onItemClick w/ section pos=$position, view=$v")
}
}
)
)
// Generate listOfOrders to use for testing RecyclerView
listOfOrders = generateDataforRecyclerViewTest()
// Get main Recycler View
val parentRecyclerView = binding.rvParentInMainActivityLayout
// Create instance on ParentRecyclerAdapter
val parentRecyclerAdapter: ParentRecyclerAdapter = ParentRecyclerAdapter(listOfOrders)
// Set the adapter to mainRecyclerView
parentRecyclerView.adapter = parentRecyclerAdapter
// Last Step, we need to attach a LayoutManager. Done in layout file.
// Add Grand Total
val grandTotalTextView: TextView = binding.tvGrandTotalInMainActivityLayout
val grandTotal: Int = listOfOrders.map { it.subTotal }.sum()
grandTotalTextView.setText("Grand Total: $${grandTotal}")
}
// Generate a List of Order objects for use in testing Nested RecyclerView
fun generateDataforRecyclerViewTest(): List<Order> {
Log.i("NOTE:MA#gdfrvt","In generateDataforRecyclerViewTest")
// Create 3 instances of Product object to use in Order object
val apples = Product("Apples", 3, 2)
val oranges = Product("Oranges", 4, 3)
val pears = Product("Pears", 4, 5)
// Generate 2 Object instances using these Product instances.
val applesSubtotal = apples.priceOfProduct * apples.quantityOfProduct
val orangesSubtotal = oranges.priceOfProduct * oranges.quantityOfProduct
val pearsSubtotal = pears.priceOfProduct * pears.quantityOfProduct
val order1 = Order("Seller #1", listOf(apples, oranges), applesSubtotal + orangesSubtotal)
val order2 = Order("Seller #2", listOf(oranges, pears), orangesSubtotal + pearsSubtotal)
return listOf(order1,order2)
}
}
class RecyclerItemClickListener(context: Context?, private val mListener: OnItemClickListener?) :
OnItemTouchListener {
interface OnItemClickListener {
fun onItemClick(view: View?, position: Int)
}
var mGestureDetector: GestureDetector
override fun onInterceptTouchEvent(view: RecyclerView, e: MotionEvent): Boolean {
val childView = view.findChildViewUnder(e.x, e.y)
if (childView != null && mListener != null && mGestureDetector.onTouchEvent(e)) {
mListener.onItemClick(childView, view.getChildAdapterPosition(childView))
}
return false
}
override fun onTouchEvent(view: RecyclerView, motionEvent: MotionEvent) {}
override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {}
init {
mGestureDetector = GestureDetector(context, object : SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean {
return true
}
})
}
}
class ChildRecyclerAdapter(val products: List<Product>):
RecyclerView.Adapter<ChildRecyclerAdapter.ViewHolder>() {
class ViewHolder(val itemView: View): RecyclerView.ViewHolder(itemView), View.OnClickListener {
val singleItemProductNameTextView: TextView = itemView.findViewById(R.id.item_row_product_name)
val singleItemProductPriceTextView: TextView = itemView.findViewById(R.id.item_row_product_price)
val singleItemProductQuantityTextView: TextView = itemView.findViewById(R.id.item_row_product_quantity)
val singleItemUpdateQuantityButton: Button = itemView.findViewById(R.id.btn_update_quantity)
init {
singleItemUpdateQuantityButton.setOnClickListener { view ->
onClick(view)
}
singleItemProductNameTextView.setOnClickListener { view ->
onClick(view)
}
singleItemProductPriceTextView.setOnClickListener { view ->
onClick(view)
}
singleItemProductQuantityTextView.setOnClickListener { view ->
onClick(view)
}
itemView.setOnClickListener(this)
}
override fun onClick(clickedView: View?) {
Log.i("NOTE:CRA:onClick","in CRA's VH#onClick w/ pos=$adapterPosition, clickedView=$clickedView")
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
// To create view holder, we need to inflate our item_row
val layoutInflater: LayoutInflater = LayoutInflater.from(parent.context)
val view: View = layoutInflater.inflate(R.layout.item_row, parent, false)
val updateBtn: Button = view.findViewById(R.id.btn_update_quantity)
val quantityEditTextView: TextView = view.findViewById(R.id.item_row_product_quantity)
return ChildRecyclerAdapter.ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// First get the CartListItem at position "position
val productItem: Product = products[position]
// Second, set all the appropriate Views in item_row
holder.singleItemProductNameTextView.text = productItem.titleOfProduct
holder.singleItemProductPriceTextView.text = productItem.priceOfProduct.toString()
holder.singleItemProductQuantityTextView.text = productItem.quantityOfProduct.toString()
}
override fun getItemCount(): Int {
return products.size
}
}
class ParentRecyclerAdapter(var orderList: List<Order>): RecyclerView.Adapter<ParentRecyclerAdapter.ViewHolder>() {
class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView), View.OnClickListener {
val sectionSellerNameTextView: TextView = itemView.findViewById(R.id.section_row_seller_name)
val sectionSubtotal: TextView = itemView.findViewById(R.id.section_row_subtotal)
val childRecyclerViewContainer: RecyclerView = itemView.findViewById(R.id.rv_child_item_row)
init {
childRecyclerViewContainer.setOnClickListener(this)
sectionSellerNameTextView.setOnClickListener { view ->
onClick(view)
}
sectionSubtotal.setOnClickListener { view ->
onClick(view)
}
itemView.setOnClickListener(this)
}
override fun onClick(p0: View?) {
Log.i("NOTE:PRA#onClick","In PRA's VH#onClick w/ pos=${adapterPosition}, clickedView=$p0")
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
// This function should inflate the section_row layout
// Get layout inflater and inflate the section_row layout
val layoutInflater: LayoutInflater = LayoutInflater.from(parent.context)
val view: View = layoutInflater.inflate(R.layout.section_row, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// We want to get a section from the sectionList based on position
val order: Order = orderList[position]
val sectionRowSellerName: String = order.sellerName
val products: List<Product> = order.productsInOrder
val sectionRowSubtotal = order.subTotal
// Next set text in the Holder for orderSellerName and orderSubtotal
holder.sectionSellerNameTextView.text = sectionRowSellerName
holder.sectionSubtotal.text = sectionRowSubtotal.toString()
// Create a ChildRecyclerAdapter
val childRecyclerAdapter: ChildRecyclerAdapter = ChildRecyclerAdapter(products)
// Set adapter:
holder.childRecyclerViewContainer.adapter = childRecyclerAdapter
}
override fun getItemCount(): Int {
return orderList.size
}
}
** Layout Files: **
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_parent_in_main_activity_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="7dp"
android:background="#03A9F4"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:divider="@android:color/darker_gray"
android:dividerHeight="1px"
android:layout_marginBottom="15dp"
android:visibility="visible"/>
<TextView
android:id="@+id/tv_grand_total_in_main_activity_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Grand Total $Original"/>
</LinearLayout>
</ScrollView>
</layout>
item_row.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:orientation="vertical"
android:background="@color/teal_200">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="35dp"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
android:text="Product Name: "/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/item_row_product_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
tools:text="Apples"/>
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="35dp"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
android:text="Product Price: "/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/item_row_product_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
tools:text="5"/>
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="35dp"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="24sp"
android:text="Product Quantity: "/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/item_row_product_quantity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="24sp"
tools:text="1"/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_update_quantity"
android:layout_width="wrap_content"
android:layout_height="34dp"
android:layout_gravity="center_vertical"
android:text="Update"/>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
section_row.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:background="@color/purple_200"
android:orientation="vertical">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
android:text="Seller Name: "/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/section_row_seller_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
tools:text="John Doe"/>
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_child_item_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="7dp"
android:background="#FFEB3B"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:divider="@android:color/darker_gray"
android:dividerHeight="1px"/>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Subtotal: $ "
android:textSize="24sp"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/section_row_subtotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
tools:text="100"/>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
** The Output of Log Statements on Sample Clicks, I removed unnecessary info from the Log statements to make them easier to read **
When clicking "Seller 2" in the screen shot I posted at the start of this post. In this case, I only need information from the PRA#onClick(aka ParentRecyclerAdapter's ViewHolders onClick() function call):
I/NOTE:MA#onItemClick: In onItemClick w/ section pos=1, view=LinearLayoutCompat
I/NOTE:PRA#onClick: In PRA's VH#onClick w/ pos=1, clickedView=AppCompatTextView{...app:id/section_row_seller_name}
When clicking "Oranges" by "Seller 1" in the screen shot I posted at the start of this post. In this case, I need information from both functions being called on click. From MA#onItemClick(aka, the function onItemClick from MainActivity) tells me that the index(pos) is 0 for the ParentRecyclerAdapters list and CRA#onClick(onClick() from ChildRecyclerViewAdapters ViewHolder class) tells me that the index(pos) is 1 and the View Clicked was the AppCompatTextView with id="item_row_product_name"
I/NOTE:MA#onItemClick: In onItemClick w/ section pos=0, view=LinearLayoutCompat{...}
I/NOTE:CRA:onClick: in CRA's VH#onClick w/ pos=1, clickedView=AppCompatTextView{...app:id/item_row_product_name}
This works fine but I figure there probably is a more efficient way to accomplish the same thing and look forward to your suggestions.