Search code examples
androidkotlinandroid-recyclerviewandroid-adapterandroid-databinding

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

Solution

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