Mahmoud Reda
Mahmoud Reda

Reputation: 163

Databinding in recyclerView item doesn't work with the fragment viewModel

I have a recyclerView that shows a list of cart items, Every item is clickable and opens details fragment for that item, I'm Updating the item layout to have a delete button inside, the delete button suppose to call a method inside the fragment viewModel. I believe that making the viewModel a constructor in the adapter is not the best practice since separation of concern is important as I develop my skills.

I'm doing so with dataBinding and I've searched so much and couldn't find an answer.

CartListItem.xml

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

    <variable
        name="makeupItem"
        type="com.melfouly.makeupshop.model.MakeupItem" />

    <variable
        name="viewModel"
        type="com.melfouly.makeupshop.makeupcart.CartViewModel" />

</data>

<com.google.android.material.card.MaterialCardView
    android:id="@+id/cart_card_item"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:layout_margin="4dp"
    android:backgroundTint="@color/primary"
    app:cardCornerRadius="8dp"
    app:cardElevation="8dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:weightSum="6">

        <ImageView
            android:id="@+id/item_image"
            loadImage="@{makeupItem}"
            android:layout_gravity="center"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:scaleType="fitCenter"
            tools:src="@tools:sample/avatars" />

        <TextView
            android:id="@+id/item_name"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="2"
            android:fontFamily="@font/aclonica"
            android:gravity="center"
            android:text="@{makeupItem.name}"
            android:textColor="@color/black"
            android:textStyle="bold"
            tools:text="Item name" />


        <TextView
            android:id="@+id/item_price"
            loadPrice="@{makeupItem}"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="2"
            android:fontFamily="@font/aclonica"
            android:gravity="center"
            android:textColor="@color/black"
            tools:text="5$" />

        <Button
            android:id="@+id/delete_button"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:onClick="@{() -> viewModel.deleteItemFromCart(makeupItem)}"
            app:icon="@drawable/ic_baseline_delete_outline_24"
            app:iconGravity="end" />


    </LinearLayout>

</com.google.android.material.card.MaterialCardView>
</layout>

CartAdapter

class CartAdapter(private val clickListener: (MakeupItem) -> Unit) :
ListAdapter<MakeupItem, CartAdapter.CartViewHolder>(DiffCallback()) {

class CartViewHolder(private val binding: CartListItemBinding) :
    RecyclerView.ViewHolder(binding.root) {
    fun bind(makeupItem: MakeupItem) {
        binding.makeupItem = makeupItem
        binding.executePendingBindings()
    }
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CartViewHolder {
    val layoutInflater = LayoutInflater.from(parent.context)
    val binding = CartListItemBinding.inflate(layoutInflater, parent, false)
    return CartViewHolder(binding)
}

override fun onBindViewHolder(holder: CartViewHolder, position: Int) {
    val makeupItem = getItem(position)
    holder.itemView.setOnClickListener {
        clickListener(makeupItem)
    }
    holder.bind(makeupItem)
}

class DiffCallback : DiffUtil.ItemCallback<MakeupItem>() {
    override fun areItemsTheSame(oldItem: MakeupItem, newItem: MakeupItem): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: MakeupItem, newItem: MakeupItem): Boolean {
        return oldItem == newItem
    }
}

CartViewModel delete function

fun deleteItemFromCart(makeupItem: MakeupItem) {
    viewModelScope.launch {
        Log.d(TAG, "DeleteItemFromCart method in viewModel called")
        repository.deleteItemFromCart(makeupItem)
    }
}

Upvotes: 0

Views: 538

Answers (1)

Mahmoud Reda
Mahmoud Reda

Reputation: 163

I've came to an answer.

DataBinding of a viewModel with a recyclerView item won't work since we're not declaring the adapter in this viewModel, so you should make a callBack inside your adapter and receive it in your fragment then call your viewModel function.

Here is CartAdapter after modifying a callBack for your delete button onClick and use the same way for your cardItem

class CartAdapter(
private val cartItemClickListener: CartItemClickListener,
private val deleteItemClickListener: DeleteItemClickListener
) :
ListAdapter<MakeupItem, CartAdapter.CartViewHolder>(DiffCallback()) {

class CartViewHolder(private val binding: CartListItemBinding) :
    RecyclerView.ViewHolder(binding.root) {
    fun bind(
        makeupItem: MakeupItem,
        cartItemClickListener: CartItemClickListener,
        deleteItemClickListener: DeleteItemClickListener
    ) {
        binding.makeupItem = makeupItem
        binding.cartItemClickListener = cartItemClickListener
        binding.deleteItemClickListener = deleteItemClickListener
        binding.executePendingBindings()
    }
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CartViewHolder {
    val layoutInflater = LayoutInflater.from(parent.context)
    val binding = CartListItemBinding.inflate(layoutInflater, parent, false)
    return CartViewHolder(binding)
}

override fun onBindViewHolder(holder: CartViewHolder, position: Int) {
    val makeupItem = getItem(position)
    holder.bind(makeupItem, cartItemClickListener, deleteItemClickListener)
}

class DiffCallback : DiffUtil.ItemCallback<MakeupItem>() {
    override fun areItemsTheSame(oldItem: MakeupItem, newItem: MakeupItem): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: MakeupItem, newItem: MakeupItem): Boolean {
        return oldItem == newItem
    }
}

class CartItemClickListener(val clickListener: (makeupItem: MakeupItem) -> Unit) {
    fun onClick(makeupItem: MakeupItem) = clickListener(makeupItem)
}

class DeleteItemClickListener(val deleteItemClickListener: (makeupItem: MakeupItem) -> Unit) {
    fun onClick(makeupItem: MakeupItem) = deleteItemClickListener(makeupItem)
}

}

As for CartListItem add two dataBinding one itemClickListener and the other for deleteButtonClickListener and use android:onClick and a lambda expression inside it

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

    <variable
        name="makeupItem"
        type="com.melfouly.makeupshop.model.MakeupItem" />

    <variable
        name="cartItemClickListener"
        type="com.melfouly.makeupshop.makeupcart.CartAdapter.CartItemClickListener" />

    <variable
        name="deleteItemClickListener"
        type="com.melfouly.makeupshop.makeupcart.CartAdapter.DeleteItemClickListener" />

</data>

<com.google.android.material.card.MaterialCardView
    android:id="@+id/cart_card_item"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:layout_margin="4dp"
    android:backgroundTint="@color/primary"
    android:onClick="@{() -> cartItemClickListener.onClick(makeupItem)}"
    app:cardCornerRadius="8dp"
    app:cardElevation="8dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:weightSum="6">

        <ImageView
            android:id="@+id/item_image"
            loadImage="@{makeupItem}"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_gravity="center"
            android:layout_weight="1"
            android:scaleType="fitCenter"
            tools:src="@tools:sample/avatars" />

        <TextView
            android:id="@+id/item_name"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="2"
            android:fontFamily="@font/aclonica"
            android:gravity="center"
            android:text="@{makeupItem.name}"
            android:textColor="@color/black"
            android:textStyle="bold"
            tools:text="Item name" />


        <TextView
            android:id="@+id/item_price"
            loadPrice="@{makeupItem}"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="2"
            android:fontFamily="@font/aclonica"
            android:gravity="center"
            android:textColor="@color/black"
            tools:text="5$" />

        <Button
            android:id="@+id/delete_button"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:onClick="@{() -> deleteItemClickListener.onClick(makeupItem)}"
            app:icon="@drawable/ic_baseline_delete_outline_24"
            app:iconGravity="end" />


    </LinearLayout>

</com.google.android.material.card.MaterialCardView>
</layout>

Once you get to your fragment and declaring your adapter pass the adapter parameters which will do a certain viewModel function or whatever you need to implement on every click of your cardItem and delete button

adapter = CartAdapter(CartAdapter.CartItemClickListener { makeupItem ->
        viewModel.onMakeupItemClicked(makeupItem)
    }, CartAdapter.DeleteItemClickListener { makeupItem ->
        viewModel.deleteItemFromCart(makeupItem)
    }
    )

I hope this is the best practice answer and helps everyone has the same issue.

Upvotes: 1

Related Questions