View models for RecyclerView items

Mariusz picture Mariusz · Apr 22, 2020 · Viewed 8.1k times · Source

My activity has a Google's ViewModel that fetches some model items. These items are then transformed into adapter items of a RecyclerView. There are also many types of adapter items supported by one RecyclerView.

I would like to have separate view model object for each of these model objects so that I can have more complex logic encapsulated only within that "small" view model.

Currently when I have some asynchronous logic (that needs to be stopped in onCleared()) that is related only to some adapter item I have to somehow route callbacks through main view model so that everything is properly unregistered.

I was considering using ViewModelProvider::get(key, modelClass) but my items are changing over time and I can't find a nice way to "clear" old items.

How are you handling these cases in your projects?

Edit: To add more information about my concern, maybe in different words: I want my "small" ViewModel to live as long as the model item which it represents. It means that:

  • I must receive onCleared() callback in the same scenarios in which parent of these items receive
  • I must receive onCleared() callback when item is no longer

Edit: Please try to compare it to a ViewPager with Fragments as items. Every individual model item is represented as a Fragment with its ViewModel. I would like achieve something similar but for RecyclerView.

Answer

Jeel Vankhede picture Jeel Vankhede · Apr 27, 2020

androidx.lifecycle.ViewModel's are not meant to be used on RecyclerView items by default

Why?

ViewModel is AAC (Android Architecture Component) whose sole purpose is to survive configuration changes of Android Activity/Fragment lifecycle, so that data can be persisted via ViewModel for such case.

This achieved by caching VM instance in storage tied to hosting activity.

That's why it shouldn't be used on RecyclerView (ViewHolder) Items directly as the Item View itself would be part of Activity/Fragment and it (RecyclerView/ViewHolder) doesn't contain any specific API to provide ViewModelStoreOwner (From which ViewModels are basically derived for given Activity/Fragment instance).

Simplistic syntax to get ViewModel is:

ViewModelProvider(this).get(ViewModel::class.java)

& here this would be referred to Activity/Fragment context.

So even if you end up using ViewModel in RecyclerView Items, It would give you same instance due to context might be of Activity/Fragment is the same across the RecyclerView which doesn't make sense to me. So ViewModel is useless for RecyclerView or It doesn't contribute to this case much.


TL;DR

Solution?

You can directly pass in LiveData object that you need to observe from your Activity/Fragment's ViewModel in your RecyclerView.Adapter class. You'll need to provide LifecycleOwner as well for you adapter to start observing that given live data.

So your Adapter class would look something like below:

class RecyclerViewAdapter(private val liveDataToObserve: LiveData<T>, private val lifecycleOwner: LifecycleOwner) : RecyclerView.Adapter<ViewHolder>() {
    
    init {
        liveDataToObserve.observe(lifecycleOwner) { t ->
            // Notify data set or something...
        }
    }

}

If this is not the case & you want to have it on ViewHolder class then you can pass your LiveData object during onCreateViewHolder method to your ViewHolder instance along with lifecycleOwner.

Bonus point!

If you're using data-binding on RecyclerView items then you can easily obtain lifecyclerOwner object from your binding class. All you need to do is set it during onCreateViewHolder() something like below:

class RecyclerViewAdapter(private val liveDataToObserve: LiveData<T>, private val lifecycleOwner: LifecycleOwner) : RecyclerView.Adapter<ViewHolder>() {
    
    override fun onCreateViewHolder: ViewHolder {
        // Some piece of code for binding
        binding.lifecycleOwner = [email protected]
        // Another piece of code and return viewholder
    }

}

class ViewHolder(private val someLiveData: LiveData<T>, binding: ViewDataBinding): RecyclerView.ViewHolder(binding.root) {
    
    init {
        someLiveData.observe(requireNotNull(binding.lifecycleOwner)) { t->
            // set your UI by live data changes here
        }
    }
}

So yes, you can use wrapper class for your ViewHolder instances to provide you LiveData out of the box but I would discourage it if wrapper class is extending ViewModel class.

As soon as concern about mimicking onCleared() method of ViewModel, you can make a method on your wrapper class that gets called when ViewHolder gets recycled or detaches from window via method onViewRecycled() or onViewDetachedFromWindow() whatever fits best in your case.


Edit for comment of @Mariusz: Concern about using Activity/Fragment as LifecycleOwner is correct. But there would be slightly misunderstanding reading this as POC.

As soon as one is using lifecycleOwner to observe LiveData in given RecyclerViewHolder item, it is okay to do so because LiveData is lifecycle aware component and it handles subscription to lifecycle internally thus safe to use. Even if you can explicitly remove observation if wanted to, using onViewRecycled() or onViewDetachedFromWindow() method.

About async operation inside ViewHolder:

  1. If you're using coroutines then you can use lifecycleScope from lifecycleOwner to call your operation and then provide data back to particular observing LiveData without explicitly handling clear out case (LifecycleScope would take care of it for you).

  2. If not using Coroutines then you can still make your asyc call and provide data back to observing LiveData & not to worry about clearing your async operation during onViewRecycled() or onViewDetachedFromWindow() callbacks. Important thing here is LiveData which respects lifecycle of given LifecycleOwner, not the ongoing async operation.