5
\$\begingroup\$

Brief Background: I just started learning Android Development recently. I have some experience with programming and understand the basics of OOP but am not confident that I am using principles of OOP properly.

In the App that I am working on, I am using a Nested RecyclerView. Since posting the code from the Actual App where this Nested RecyclerView is actually used would introduce a lot of irrelevant code, I created a sample App that addresses all the concepts that I wanted this Code Review to address without introducing other unnecessary things into the picture. I originally posted this as a question/request for recommendations on Stackoverflow but after 5 days, it has only gotten 21 view and no answers/comments, so I thought it might be more appropriate for this site.

What I hoped to Accomplish/The Problem

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 sources with my ideas 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.

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.

Screenshot of the Example I made

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. I also created an abstract class called ClickReporter that is instantiated as an object expression in the Activity needing this ClickReporter and ClickReporters abstract function is provided an implementation here which basically just Logs the position clicked in Parent RecyclerView, position in child RecyclerView and the specific View in the layout that was clicked. This instance of ClickReporter is passed as a parameter to ParentRecyclerAdapter and ChildRecyclerAdapter. The parent and child RecyclerAdapter class implement the View.OnClickListener interface whose function onClick then calls a function in the instance of the ClickReporter that was passed to the parent/child RecyclerAdapter. If the View that was clicked is a View from the Parent RecyclerView adapter, then the ClickReporter class has enough information about what view was clicked, but if the View that was clicked is a View in the Child RecyclerView, then the ClickReporter class doesn't have enough information as it still needs 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 ClickReporter is able to know the index in the Parent RecyclerView from the onItemClicked() function call, 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.

Once ClickReporter has the information, it calls its abstract function onResult, passing in the parameters for parentPosition, childPosition, and specific view clicked. This function is implemented in the Activity/fragment that instantiated ClickReporter.

I am not sure if creating ClickReporter as an abstract class was the best solution. Would it have been better to maybe create an interface to accomplish what I was trying to do. I wasn't able to think of a way of accomplishing what I was trying to do using an interface. I would appreciate your insight as what would be the best way to accomplish what I was trying to do and any other input you may have.

The output of the relevant Log statements is below. I/NOTE:MA#onItemClick stands for onItemClick() that is called in MainActivity. I/NOTE:PRA#onClick stands for the onClick function that is called in
ParentRecyclerAdapter. I/NOTE:CRA#onClick stands for onClick function that is called in ChildRecyclerAdapter. I/MainActivity is when ClickReporters onResult function is called giving the result I was seeking.

The first three log statements are Logged when I clicked on "Seller1" in the screenshot of the App shown above. And last 3 Log statements are Logged when I click on "Oranges" which are being sold by Seller1.

I/NOTE:MA#onItemClick: In onItemClick w/ section pos=0, view=androidx.appcompat.widget.LinearLayoutCompat{...}
I/NOTE:PRA#onClick: In PRA's VH#onClick w/ pos=0, clickedView=androidx.appcompat.widget.AppCompatTextView{...app:id/section_row_seller_name}
I/MainActivity: Parent Position: 0, Child Position: -1, viewClicked: androidx.appcompat.widget.AppCompatTextView{...app:id/section_row_seller_name}

I/NOTE:MA#onItemClick: In onItemClick w/ section pos=0, view=androidx.appcompat.widget.LinearLayoutCompat{...}
I/NOTE:CRA:onClick: in CRA's VH#onClick w/ pos=1,     
clickedView=androidx.appcompat.widget.AppCompatTextView{...app:id/item_row_product_name}
I/MainActivity: Parent Position: 0, Child Position: 1, viewClicked:    
    androidx.appcompat.widget.AppCompatTextView{...app:id/item_row_product_name}

I will post the code next into the body of this CodeReview request and will also post a link so you can download the project instead of having to copy and paste the code given below: code on iCloud

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>
    lateinit var clickReporter: ClickReporter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Setup Click Reporter
        clickReporter = object: ClickReporter() {
            override fun onResult(parentPos: Int, childPos: Int, viewClicked: View?) {
                Log.i("MainActivity","Parent Position: $parentPos, Child Position: $childPos, viewClicked: $viewClicked")
            }
        }
        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")
                        clickReporter.onNestedRvItemClick(position)
                    }
                }
            )
        )

        // 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,clickReporter)
        // 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)
    }
}
import android.view.View

abstract class ClickReporter {
    var childPosition:Int = -1
    var parentPosition: Int = -1
    var view: View? = null

    // The user calls this function if any view is Clicked(in parent or child RecyclerView).
    // If it is called  from the Parent RecyclerView adapter, then
    fun onNestedRvClick(typeOfAdapter: String, position: Int, clickedView: View?) {
        if (typeOfAdapter == "Parent") {
            view = clickedView
            parentPosition = position
            onResult(parentPosition, childPosition, clickedView)
            childPosition = -1
            parentPosition = -1
            view = null
        } else if (typeOfAdapter == "Child") {
            view = clickedView
            childPosition = position
            onResult(parentPosition, childPosition, clickedView)
            childPosition = -1
            parentPosition = -1
            view = null
        }
    }
    // This function is called from the onItemClick() function in the Activity/Fragment that uses
    // an instance of this class(ClickReporter). It gets called irrespective if the user clicked on
    // a view in Parent RecyclerView or Child RecyclerView.
    // But we only need the information from the call to this function if the
    // user clicked on a View in the Child RecyclerView as it tells us which position in the Parent
    // RecyclerView was clicked.
    fun onNestedRvItemClick(position: Int) {
        parentPosition = position
    }

    // This is the function the user has to over ride in their Activity/Fragment where they will
    // be using ClickReporter. It lets them know which position in the Parent RecyclerView was clicked,
    // position in Child RecyclerView and specific view in the layout that was clicked.
    // If the clicked View is only present in the Parent RecyclerView then childPos will be -1.
    abstract fun onResult(parentPos: Int, childPos: Int, viewClicked: View?)



}
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>, val clickReporter: ClickReporter):
    RecyclerView.Adapter<ChildRecyclerAdapter.ViewHolder>() {


    inner 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")
            clickReporter.onNestedRvClick("Child", adapterPosition, 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 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>, var clickReporter: ClickReporter): RecyclerView.Adapter<ParentRecyclerAdapter.ViewHolder>() {

    inner 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")
            clickReporter.onNestedRvClick("Parent", adapterPosition, 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, clickReporter)

        // 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>

I look forward to your suggestions/recommendations regarding my use of OOP principles, any design patterns that I could have implemented, best practices or overall anything you thought of.

\$\endgroup\$

0

Browse other questions tagged or ask your own question.