Home > OS >  How to add OnClickListener with databinding in recycleView buttons?
How to add OnClickListener with databinding in recycleView buttons?

Time:07-11

I am fetching the categories from an API and I would like to fire up a new fragment by passing the category name to retrofit on the button click. I am thinking of doing it in my UI with getText() on the button clicked and firing up the API call which will display the result on a new fragment. The API I am using is https://fakestoreapi.com/

To my understanding, I have to handle the click events in the UI part (Fragment's Code).

The code below is in my FakeApiService.kt

@GET("categories")
suspend fun getCategories(): List<String>

@GET("categories/{product}")
suspend fun getProduct(@Path(value = "product", encoded = true) productType: String): List<Product>

However, I am having trouble adding a clickListenerto the the buttons.

Here is the code for recycleView adapter

class MyCategoryRecyclerViewAdapter(
    private val values: ArrayList<String>
    ) : RecyclerView.Adapter<MyCategoryRecyclerViewAdapter.MyCategoryRecyclerViewHolder>() {

    inner class MyCategoryRecyclerViewHolder :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(category: String) {
            binding.category = category
            binding.btnCategoryName.setOnClickListener {
                Log.d("Debug On", "Button clicked ${binding.btnCategoryName.text}")
            }
        }
    }

    private lateinit var binding: CategoryItemBinding

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): MyCategoryRecyclerViewHolder {
        binding = CategoryItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return MyCategoryRecyclerViewHolder()
    }

    override fun onBindViewHolder(holder: MyCategoryRecyclerViewHolder, position: Int) {
        holder.bind(values[position])
    }


    fun updateData(values: ArrayList<String>) {
        this.values.clear()
        this.values.addAll(values)
    }

    override fun getItemCount() = values.size
}

My ViewModel for the categories fragment.

enum class FakeApiStatus { LOADING, ERROR, DONE }

class CategoryViewModel : ViewModel() {
    // The internal MutableLiveData that stores the status of the most recent request
    private val _status = MutableLiveData<FakeApiStatus>()
    val status: LiveData<FakeApiStatus> = _status
    val categories: ArrayList<String> = ArrayList()

    /**
     * Call getCategories() on init so we can display status immediately.
     */
    init {
        Log.d("Debug", "view model created")
        getCategories()
    }

    private fun getCategories() {
        viewModelScope.launch(Dispatchers.IO) {
            _status.postValue(FakeApiStatus.LOADING)
            try {
                categories.clear()
                categories.addAll(FakeApi.retrofitService.getCategories())
                _status.postValue(FakeApiStatus.DONE)
            } catch (e: Exception) {
                _status.postValue(FakeApiStatus.ERROR)
            }
        }
    }
}

Code for the fragment.

class CategoryFragment : Fragment() {
    private lateinit var binding: FragmentCategoryBinding
    private val adapter = MyCategoryRecyclerViewAdapter(ArrayList())
    private val viewModel: CategoryViewModel by lazy {
        ViewModelProvider(this)[CategoryViewModel::class.java]
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = FragmentCategoryBinding.inflate(inflater)
        binding.rvCategoryList.adapter = adapter
        viewModel.status.observe(viewLifecycleOwner) {
            when (it) {
                FakeApiStatus.DONE -> binding.animationRotation.visibility = View.GONE
                FakeApiStatus.ERROR -> Toast.makeText(
                    context,
                    "No Internet!",
                    Toast.LENGTH_LONG
                ).show()
                else -> {}
            }
            observeCategories()
        }
        return binding.root
    }

    private fun observeCategories(){
        adapter.updateData(viewModel.categories)
        adapter.notifyDataSetChanged()
    }
}

This is the layout for my item

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="category"
            type="String" />
    </data>

    <Button
        android:id="@ id/btn_category_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@{category.toUpperCase()}"
        tools:text="Test"
        android:onClick="TODO"
        android:layout_margin="@dimen/cardview_default_elevation"
        android:textAppearance="?attr/textAppearanceListItem" />
</layout>

And the layout for Fragment(RecycleView)

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

    <data>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@ id/rv_category_list"
            android:name="com.example.task.categories.CategoryFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layoutManager="LinearLayoutManager"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:context=".categories.CategoryFragment"
            tools:listitem="@layout/category_item" />

        <com.google.android.material.progressindicator.CircularProgressIndicator
            android:id="@ id/animation_rotation"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:indeterminate="true"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

CodePudding user response:

You can pass a call back to the adapter class. In your fragment / activity, below the class create a new class to handle the click

 /**
 * Click listener for YourDomainObject. By giving the block a name it helps a reader understand what it does.
 */
class YourCustomClickClick(val block:(YourDomainObject)-> Unit){
    fun onClick(yourDomainObject:YourDomainObject)= block(yourDomainObject)
}

When you initialize the viewModelAdapter in your fragment/activity you can also return the call back

viewModelAdapter = YourAdapter(YourCustomClick{item->
            var value = item
            Log.i(logTag,"The item that was clicked: ${value}")
//do any other operations you would like on click here
            return@YourCustomClickCallback

        }

Your adapter class:

class MyCategoryRecyclerViewAdapter(
private val values: ArrayList<String>
)

change to: pass a callback

class MyCategoryRecyclerViewAdapter(
private val values: ArrayList<String>,
val callback:YourCustomClick // the call back created in your fragment/activity
)

in your view holder you can get the positions of the click like so

   override fun onBindViewHolder(holder: YourViewHolder, position: Int) {
        holder.viewDataBinding.also {
                    it.item = item[position] // here we can tell what item is clicked based on the position in the list
                    it.YourCustomClickCallback= callback // assign the call back for each item in the list

        }
    }

finally, in your view you can gain access to the call back and domain object by creating data variables. inside your layout tags, add data tags. Inside the data tags add a variable with a name and path to the object. (if you have a domain object created yet).

 <layout> 
    <data>
        <variable
            name="item"
            type="com.example.yourproject.domain.models.Item" />
        <variable
            name="yourCustomCallback"
            type="com..example.yourproject.main.ui.fragments.YourCustomClick" />
    </data>
    </layout>

Tip, this can be done in the xml layout that holds the cards or views to your recycler view. The view can be assigned from the view holder

class YourViewHolder(val viewDataBinding: CardViewYourItemBinding):
        RecyclerView.ViewHolder(viewDataBinding.root){

        companion object{
            @LayoutRes
            val LAYOUT = R.layout.card_view_your_recycler_item
        }

    }

You can assign the call back in your xml with data binding

<ImageView
                
            android:clickable="true"
            android:onClick="@{()->yourCustomCallback.onClick(item)}"
<!-- item is passed to your custom on click -->

By domain object I meant domain model. It is a simple class that you can instance to hold some data.

Here is an example of a simple data class for a user.

data class User(
val userID: String,
val fireBaseId:String,
val userName: String,
val firstName: String,
val lastName: String,
val score: Int,
val age: Int,
val emailAddress: String,
val photoUrl: String,
val sex: Int,
val emailVerified:Boolean) 
  • Related