How to change recyclerview item and sub items when clicked and also get the data in the position clicked from a fragment?

Viewed 183

I have 2 Recyclerviews in a fragment. Each item consists of 2 textview. When clicked, i want to change the color of item background and the 2 tvs and get the listofData(position) clicked also then send these s pieces of data to an activity.

The problem is 2 rvs each have its own adapter so i can't call the activity and check if data is selected from both adapters. And when i try it from the fragment i get the adapter position right but the view colors are not being changed correctly.

My RV element

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:paddingTop="2dp"
    android:paddingBottom="10dp"
    android:paddingStart="20dp"
    android:paddingEnd="20dp"
    android:background="@drawable/date_time_background"
    android:elevation="0dp"
    android:gravity="center"
    android:layout_marginEnd="5dp"
    android:orientation="vertical"
    android:id="@+id/item_linear_layout">

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/dateNameTV"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="Sat"
        android:textColor="@color/black_65"
        android:textSize="16sp" />

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/dateNumTV"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="23/5"
        android:textColor="@color/black_65"
        android:textSize="16sp" />

</LinearLayout>

My RV adapter

class TimesAdapter(private var availableTimes: List<String>?, private val onTimeListener: OnTimeListener) :
        RecyclerView.Adapter<TimesAdapter.TimeViewHolder>() {

    private var itemIndex = -1

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TimeViewHolder {
        val v = LayoutInflater.from(parent.context)
                .inflate(R.layout.date_time_element, parent, false)
        return TimeViewHolder(v, onTimeListener)

    }

    override fun getItemCount(): Int = availableTimes?.size!!

    @SuppressLint("ClickableViewAccessibility")
    override fun onBindViewHolder(holder: TimeViewHolder, position: Int) {

        val currentDate: String? = availableTimes?.get(position) // i.e Sun 23/5
        val parts = currentDate?.split(" ")

        try {
            val part1 = parts?.get(0)
            holder.dateNameTV.text = part1
            val part2 = parts?.get(1)
            holder.dateNumTV.text = part2
        } catch (e: IndexOutOfBoundsException) {
            e.printStackTrace()
        }

        holder.itemLinearLayout.setOnClickListener {
            itemIndex = position
            notifyItemChanged(position)
        }
        val ctx = holder.itemLinearLayout.context
        if (itemIndex == position) {
            holder.itemLinearLayout.background = (loadDrawable(ctx, R.drawable.date_time_background_selected))
            holder.dateNameTV.setTextColor(loadColor(ctx, android.R.color.white))
            holder.dateNumTV.setTextColor(loadColor(ctx, android.R.color.white))
        } else {
            holder.itemLinearLayout.background = (loadDrawable(ctx, R.drawable.date_time_background))
            holder.dateNameTV.setTextColor(loadColor(ctx, R.color.black_65))
            holder.dateNumTV.setTextColor(loadColor(ctx, R.color.black_65))
        }

    }

    class TimeViewHolder(itemView: View, onTimeListener: OnTimeListener) :
            RecyclerView.ViewHolder(itemView), View.OnClickListener{
        var dateNameTV: TextView = itemView.findViewById(R.id.dateNameTV)
        var dateNumTV: TextView = itemView.findViewById(R.id.dateNumTV)
        var itemLinearLayout: LinearLayout = itemView.findViewById(R.id.item_linear_layout)

        private var onTimeListener: OnTimeListener? = null

        init {
            itemView.setOnClickListener(this)
            this.onTimeListener = onTimeListener
        }

        override fun onClick(v: View?) {
            onTimeListener?.onTimeClick(adapterPosition, itemView)
        }
    }

    interface OnTimeListener{
        fun onTimeClick(position: Int, itemView: View)
    }

}

My fragment

// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"

/**
 * A simple [Fragment] subclass.
 * Use the [DoctorAppointmentsFragment.newInstance] factory method to
 * create an instance of this fragment.
 */
class DoctorAppointmentsFragment : Fragment(),
        DatesAdapter.OnDateListener, TimesAdapter.OnTimeListener {
    private lateinit var datesRV: RecyclerView
    private lateinit var timesRV: RecyclerView
    private lateinit var linearLayoutManager: LinearLayoutManager
    private lateinit var datesAdapter: DatesAdapter
    private lateinit var timesAdapter: TimesAdapter
    private lateinit var dateNameTV: TextView
    private lateinit var dateNumTV: TextView
    private lateinit var dateLinearLayout: LinearLayout

    private var dates: List<String>? = null
    private var times: List<String>? = null

    private var isDateSelected = false
    private var isTimeSelected = false
    private var selectedDate: String? = null
    private var selectedTime: String? = null

    // TODO: Rename and change types of parameters
    private var param1: String? = null
    private var param2: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            param1 = it.getString(ARG_PARAM1)
            param2 = it.getString(ARG_PARAM2)
        }
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        // Inflate the layout for this fragment
        val view = inflater.inflate(R.layout.fragment_doctor_appointments, container, false)

        datesRV = view.findViewById(R.id.datesRV)
        datesRV.setHasFixedSize(true)

        linearLayoutManager = LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false)
        linearLayoutManager.isAutoMeasureEnabled = false
        datesRV.layoutManager = linearLayoutManager

        timesRV = view.findViewById(R.id.timesRV)
        timesRV.setHasFixedSize(true)

        linearLayoutManager = LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false)
        linearLayoutManager.isAutoMeasureEnabled = false
        timesRV.layoutManager = linearLayoutManager

        val bookNowBT = view.findViewById<Button>(R.id.bookNowBT)
        bookNowBT.setOnClickListener {
            if (isDateSelected && isTimeSelected) {
                val i = Intent(activity, ConfirmPaymentActivity::class.java)
                i.putExtra(SELECTED_DATE, selectedDate)
                i.putExtra(SELECTED_TIME, selectedTime)
                startActivity(i)
            }
        }

        getDoctorAvailableAppointments("7ab63fd2461bfb0008b72f5d8c0033fs", "basic")

        return view
    }

    companion object {
        /**
         * Use this factory method to create a new instance of
         * this fragment using the provided parameters.
         *
         * @param param1 Parameter 1.
         * @param param2 Parameter 2.
         * @return A new instance of fragment DoctorAppointmentsFragment.
         */
        // TODO: Rename and change types and number of parameters
        @JvmStatic
        fun newInstance(param1: String, param2: String) =
                DoctorAppointmentsFragment().apply {
                    arguments = Bundle().apply {
                        putString(ARG_PARAM1, param1)
                        putString(ARG_PARAM2, param2)
                    }
                }
    }

    private fun getDoctorAvailableAppointments(doctorID: String, type: String) {
        setProgressDialog(this.requireActivity())
        if (!InternetConnection.isInternetAvailable(this.requireActivity())) {
            alertError(this.requireActivity(),
                    R.string.no_internet_connection,
                    R.string.check_internet_connection)
        } else {
            showProgressDialog()
            val builder = ServiceBuilder()
            val appointments = builder.getDoctorAvailableReservations()
            val call = appointments.getDoctorAvailableReservations(
                    RequestAvailableReservations(doctorID,type))
            call?.enqueue(object : Callback<ResponseAvailableReservations?> {
                override fun onResponse(call: Call<ResponseAvailableReservations?>, response: Response<ResponseAvailableReservations?>) {
                    dismissProgressDialog()
                    if (!response.isSuccessful) {
                        alertError(requireActivity(),
                                R.string.code_not_200,
                                R.string.try_later)
                        return
                    }
                    val body: ResponseAvailableReservations? = response.body()
                    if (body == null) {
                        alertError(requireActivity(),
                                R.string.null_body,
                                R.string.try_later)
                        return
                    }
                    val status = body.status
                    val message = body.message
                    val data = body.data

                    if (status == null || message == null || data == null) {
                        alertError(requireActivity(),
                                R.string.null_body,
                                R.string.try_later)
                        return
                    }
                    if (status == "error") {
                        alertError(requireActivity(), R.string.error, message)
                    } else if (status == "success") {
                        dates = data.availableDatesList
                        datesAdapter = DatesAdapter(dates, this@DoctorAppointmentsFragment)
                        datesRV.adapter = datesAdapter

                        times = data.availableTimesList
                        timesAdapter = TimesAdapter(times, this@DoctorAppointmentsFragment)
                        timesRV.adapter = timesAdapter

                    }
                }

                override fun onFailure(call: Call<ResponseAvailableReservations?>, t: Throwable) {
                    t.printStackTrace()
                    dismissProgressDialog()
                    alertError(requireActivity(),
                            R.string.fail,
                            R.string.login_fail)
                }
            })
        }
    }

    private var dateIndex = -1
    override fun onDateClick(position: Int) {
        selectedDate = dates?.get(position)
        isDateSelected = true
        Log.e("selectedDate", "selectedDate")

        dateIndex = position
        datesAdapter.notifyDataSetChanged()


    }

    private var timeIndex = -1
    override fun onTimeClick(position: Int, itemView: View) {
//        selectedTime = times?.get(position)
//        isTimeSelected = true
//        Log.e("isSelectedTime", "true")

//        timeIndex = position
//        timesAdapter.notifyItemChanged(position)
//        Log.e("timeIndex", timeIndex.toString())
//
//        val ctx = itemView.context
//        if (timeIndex == position) {
//            Log.e("timeIndex", "timeIndex == position")
//
//            itemView.background = loadDrawable(ctx, R.drawable.date_time_background_selected)
//            itemView.dateNameTV?.setTextColor(loadColor(ctx, android.R.color.white))
//            itemView.dateNumTV?.setTextColor(loadColor(ctx, android.R.color.white))
//        } else {
//            itemView.background = (loadDrawable(ctx, R.drawable.date_time_background))
//            itemView.dateNameTV.setTextColor(loadColor(ctx, R.color.black_65))
//            itemView.dateNumTV.setTextColor(loadColor(ctx, R.color.black_65))
//        }

    }
}        
3 Answers

Solved For anyone interested i ended up using a very simple idea. Instead of accessing the RecyclerView selected element from outside the adapter, i did all i wanted to do inside the adapter and used 4 static variables in the Fragment. 2 booleans to check if date and time are selected or not and 2 Strings having the date and time actually selected.

Inside the Fragment:

companion object {
        @JvmStatic var isTimeSelected = false
        @JvmStatic var selectedTime = ""

        @JvmStatic var isDateSelected = false
        @JvmStatic var selectedDate = ""
....
}

Inside the Adapter's onBindViewHolder()

holder.itemLinearLayout.setOnClickListener {
            selectedDate = dates?.get(position)!!
            isDateSelected = true
            listener?.onDateClick(position)
            selectedIndex = position
            notifyDataSetChanged()
        }

        val ctx = holder.itemLinearLayout.context
        if (selectedIndex == position) {
            holder.itemLinearLayout.background = (loadDrawable(ctx, R.drawable.date_time_background_selected))
            holder.dateNameTV.setTextColor(loadColor(ctx, android.R.color.white))
            holder.dateNumTV.setTextColor(loadColor(ctx, android.R.color.white))
        } else {
            holder.itemLinearLayout.background = (loadDrawable(ctx, R.drawable.date_time_background))
            holder.dateNameTV.setTextColor(loadColor(ctx, R.color.black_65))
            holder.dateNumTV.setTextColor(loadColor(ctx, R.color.black_65))
        }

And i made sure static variables don't have old values using this

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        isTimeSelected = false
        selectedTime = ""

        isDateSelected = false
        selectedDate = ""
    }

I know it's bad practice to put onClickListener inside onBindViewHolder but i don't konw if the static variables thing is good or bad practice. Either way it's working fine for now.

There is many ways to do this, And I recommend the first one.

  1. Using shared ViewModels for communication between fragments in the same activity. The idea is that you will tie up the ViewModel with activity and every fragment can access this ViewModel and in this ViewModel you will have a LiveData object that holds the data and observing it from both fragments and every change in the value of LiveData will affect each fragment. Shared ViewModel

  2. Using interfaces for communication between fragments and it's a hard thing to do these days. Basic communication between fragments

  3. Using EventBus it's easy but not recommended while you can use ViewModel at the first solution. EventBus

Try this

TimesAdapter

class TimesAdapter(
    availableTimes: List<String>
) : RecyclerView.Adapter<TimesAdapter.TimeViewHolder>() {
        var availableTimes:List<String> = availableTimes
        set(value) {
                field = value
                notifyDataSetChanged()
        }

        var listener:TimeSelectedListener?=null
        var selectedIndex:Int = -1

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TimeViewHolder {
        return TimeViewHolder(
            LayoutInflater.from(parent.context)
                .inflate(R.layout.date_time_element, parent, false)
        )
    }

    override fun getItemCount(): Int = availableTimes.count()

    override fun onBindViewHolder(holder: TimeViewHolder, position: Int) {

        val currentDate: String? = availableTimes[position] // i.e Sun 23/5
        val parts = currentDate?.split(" ")

        try {
            val part1 = parts?.get(0)
            val part2 = parts?.get(1)
            holder.dateNameTV.text = part2
            holder.dateNumTV.text = part1
        } catch (e: IndexOutOfBoundsException) {
            e.printStackTrace()
        }

        holder.itemLinearLayout.setOnClickListener {
                listener?.onTimeClick(position)
                selectedIndex = position
        }

        if (selectedIndex == position) {
            // Selection Code
        } else {
            // De selection code
        }

    }

    class TimeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        var dateNameTV: TextView = itemView.findViewById(R.id.dateNameTV)
        var dateNumTV: TextView = itemView.findViewById(R.id.dateNumTV)
        var itemLinearLayout: LinearLayout = itemView.findViewById(R.id.item_linear_layout)
    }

    interface TimeSelectedListener {
        fun onTimeClick(position: Int)
    }
}

Usage from fragment

times = data.availableTimesList
timesAdapter = TimesAdapter(times).apply {
    listener = object :TimesAdapter.TimeSelectedListener{
        override fun onTimeClick(position: Int) {
            // TODO here is where you should implement when an item is selected
        }
    }
}
timesRV.adapter = timesAdapter
Related