Using viewmodels with recyclerviews

Viewed 2022

This all very complex I hope I can explain it well. So, I'm using recyclerview to show a list of User Profiles in form of cards. Each card contains 2 buttons for different actions. Now these buttons require observing to a LiveData object inside a ViewModel (I'm using MVVM, Android architecture components and Kotlin extensions) when clicked. One button in particular needs to observe to different live data based on some condition. This is why I've created a function which takes the condition and returns suitable OnClickListener. I pass this to the recyclerview adapter.

Now the problem is I use the same RecyclerView in multiple fragments. Everything works fine when i use the recyclerview in the same fragment where the function is written. Everywhere else I get the error like following:

java.lang.IllegalStateException: Fragment HomeFragment{1ef5330} (b299012d-c9c2-427f-8387-a7888289701b)} not attached to an activity.
        at androidx.fragment.app.Fragment.requireActivity(Fragment.java:833)
        at 
com.halalrishtey.HomeFragment$$special$$inlined$activityViewModels$2.invoke(FragmentViewModelLazy.kt:80)
        at com.halalrishtey.HomeFragment$$special$$inlined$activityViewModels$2.invoke(Unknown Source:0)
        at androidx.lifecycle.ViewModelLazy.getValue(ViewModelProvider.kt:52)
        at androidx.lifecycle.ViewModelLazy.getValue(ViewModelProvider.kt:41)
        at com.halalrishtey.HomeFragment.getUserVM(Unknown Source:7)
        at com.halalrishtey.HomeFragment.access$getUserVM$p(HomeFragment.kt:25)
        at com.halalrishtey.HomeFragment$genInterestBtnListener$1.onClick(HomeFragment.kt:65)
        at android.view.View.performClick(View.java:7125)
        at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:967)
        at android.view.View.performClickInternal(View.java:7102)
        at android.view.View.access$3500(View.java:801)
        at android.view.View$PerformClick.run(View.java:27336)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

And the function which creates the OnClickListener looks something like this:

fun genInterestBtnListener(
        condition: Boolean,
        v: View
    ): View.OnClickListener {
        return View.OnClickListener {
            if (!condition) {
                userVM.initInterest().observe(viewLifecycleOwner, Observer { msg ->
                    Toast.makeText(context, msg, Toast.LENGTH_SHORT)
                        .show()
                    //More code here
                })
            } else {
                userVM.removeInterest().observe(viewLifecycleOwner, Observer { msg ->
                    Toast.makeText(context, msg, Toast.LENGTH_SHORT)
                        .show()
                })
            }
        }
    }

I can't observe to things inside the adapter, so whats the optimal solution for this without repeating code??

Update: I declare userVM like this:

private val userVM: UserViewModel by activityViewModels()

Also Here's my adapter code:

class CardDataRVAdapter(private var items: List<ProfileCardData>) :
    RecyclerView.Adapter<CardDataRVAdapter.CardDataViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CardDataViewHolder {
        val inflatedView =
            LayoutInflater.from(parent.context).inflate(R.layout.profile_card, parent, false)
        return CardDataViewHolder(inflatedView)
    }

    override fun onBindViewHolder(holder: CardDataViewHolder, position: Int) {
        val card = items[position]
        holder.bindCard(card)
    }

    override fun getItemCount() = items.size

    class CardDataViewHolder constructor(
        v: View
    ) : RecyclerView.ViewHolder(v), View.OnClickListener {
        private var view: View = v
        private var cardData: ProfileCardData? = null

        init {
            v.setOnClickListener(this)
        }

        override fun onClick(p0: View?) {
            //TODO: Implement a proper onClickListener
            Toast.makeText(
                p0?.context,
                "${p0?.cardTitleTextView?.text} Card was clicked",
                Toast.LENGTH_SHORT
            ).show()
        }


        companion object {
            private val KEY = "CARD"
        }

        fun bindCard(card: ProfileCardData) {
            this.cardData = card

            if (card.isUserShortlisted) {
                view.showInterestBtn.setIconResource(R.drawable.ic_favorite)
            } else {
                view.showInterestBtn.setIconResource(R.drawable.ic_favorite_border)
            }

            if (card.data.photoUrl.length > 5) {
                Picasso.get().load(card.data.photoUrl)
                    .into(view.cardImageView)

                Picasso.get().load(card.data.photoUrl)
                    .into(view.cardAvatarImageView)
            }

            view.cardTitleTextView.text = card.data.displayName
            view.cardSubtitleTextView.text = "${card.data.age} - ${card.data.height}"

            view.showInterestBtn.setOnClickListener(card.showBtnInterestListener)
            view.sendMessageBtn.setOnClickListener(card.messageBtnListener)
        }
    }
}
2 Answers

Based on my understanding, every LiveData should be observed within the fragment/activity where instance of ViewModel class created. I think the problem occurs because you trying to observe LiveData outside the fragment where its created. You can observe LiveData outside fragment/activity with observeForever, but you'll also need to call removeObserver to avoid memory leaks

cmiiw

I think your issue is because you're using view. in your bindCard()function.

Is the showBtnInterestListener in your class? You might move it to your Adapter as it'll be more appropriate that the Adapter knows about Listener instead of the data class

Could you try to change your Adapter as such:

You declare the object you'll interact with in your class and then in the bind you call them.

class CardDataViewHolder constructor(
        v: View
    ) : RecyclerView.ViewHolder(v), View.OnClickListener {
        private var view: View = v
        private var cardData: ProfileCardData? = null

        val showInterestBtn = view.findViewById(R.id.show_interest)
        val sendMessageBtn = view.findViewById(R.id.message_btn)
        val title = view.findViewById(R.id.card_title)
        val subtitle = view.findViewById(R.id.card_subtitle)

        companion object {
            private val KEY = "CARD"
        }

        fun bindCard(card: ProfileCardData) {
            this.cardData = card

            if (card.isUserShortlisted) {
                showInterestBtn.setIconResource(R.drawable.ic_favorite)
            } else {
                showInterestBtn.setIconResource(R.drawable.ic_favorite_border)
            }

            if (card.data.photoUrl.length > 5) {
                Picasso.get().load(card.data.photoUrl)
                    .into(view.cardImageView)

                Picasso.get().load(card.data.photoUrl)
                    .into(view.cardAvatarImageView)
            }

            title.text = card.data.displayName
            subtitle.text = "${card.data.age} - ${card.data.height}"

            showInterestBtn.setOnClickListener(card.showBtnInterestListener)
            sendMessageBtn.setOnClickListener(card.messageBtnListener)
        }
    }
Related