24

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.

Mariusz
  • 1,825
  • 5
  • 22
  • 36
  • 1
    I'd suggest not to use ViewModels for each items because ViewModels are basically connected to Activity/Fragment lifecycle and it would kill that purpose if you try to bind it to each items (It would be shared between items though context is same throughout the recycler view) apart from that you can have live data exposed by that VM to your adapter and observe it in your Adapter or in ViewHolder via lifecycleOwner reference. – Jeel Vankhede Apr 23 '20 at 16:53
  • Sorry, I am having trouble understanding your concern. Can you describe it in a different way? – Mariusz Apr 25 '20 at 06:22
  • RecyclerView items are NOT ViewModelStoreOwners, and you probably don't want them to be one. ViewModel is not a view model, it's a data cache across config changes. You definitely don't need one for each recyclerview item. – EpicPandaForce Apr 26 '20 at 03:33

4 Answers4

46

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 = this@RecyclerViewAdapter.lifecycleOwner
        // 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.

æ-ra-code
  • 2,140
  • 30
  • 28
Jeel Vankhede
  • 11,592
  • 2
  • 28
  • 58
  • Thank you for this answer. II would like to address two things: 1) What is the source of this input LifecycleOwner? Activity/Fragment? If yes, then I am not sure if this is the right Lifecycle object which we should be observing. Notice that with this approach even if an item is scrolled out of the screen, then Lifecycle is still in a RESUMED state 2) Using onViewRecycled() or onViewDetachedFromWindow() will make my async operation stop on configuration change for given item and I don't want to do that. – Mariusz Apr 28 '20 at 13:20
  • @Mariusz Edited the post, please check it once and let me know if it clears your concern. – Jeel Vankhede Apr 29 '20 at 07:50
  • 1
    Thanks for the update. I see you proposed to scope the async operation to a lifecycle(owner) or onViewRecycled/DetachedFromWindow() but I want my async operation to be continued even on configuration change and according to my understanding of your comment it cannot be achieved. – Mariusz May 01 '20 at 12:36
  • Hm.. _The view model is an abstraction of the view exposing public properties and commands_ ([wiki](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel#Components_of_MVVM_pattern)). Lets imagine, next day we need to create a dynamic list view of what previously was a whole set of different screens with own viewmodels, and then number of them is multiplied 10 times, each with own state. Like some rich card list/grid view media news dashboard with calendar, for example. I think _ViewModels are meant to be used every where we need abstraction over views__. – æ-ra-code Feb 12 '21 at 18:10
  • Should I then "unimplement" view models created previously for static screens, cuz there are no one-click binding, lifecycle & scope management out of a box google can provide? Bonus point is good by the way, thanks! Ideally we should provide map of view-models <-> corresponding scopes, recycler should maintain all binding. But again scopes is only part of ui optimization. So ui should work just with plain list of ViewModels, where a type of appropriate view is defined by some mapping function. – æ-ra-code Feb 12 '21 at 18:11
  • @aeracode I agree with you on the comment that view models are meant to be abstractions over views but, as per my understanding even though it's abstraction view models are driver between views/UI to domain/business logic. Plus the wiki link you shared is some abstract and general across software architecture practices. – Jeel Vankhede Feb 12 '21 at 18:38
  • But when you consider in Android, view models are more tweaked in a way that they are closely tied to lifecycle of given UI. meaning more views you have, more objects of view model you'll end up creating. Now in such case of recycler view, we all know that view is usually same across items of recycler view and view models serve purpose of providing data and interactions. data can be different but interactions are same in most of the cases (i.e. clicks on product item increase counter by one) for which we had already answer/solution prior to MVVM. – Jeel Vankhede Feb 12 '21 at 18:43
  • So, that's why I kinda feel it's misuse of view models on recycler view. – Jeel Vankhede Feb 12 '21 at 18:44
  • Yes, case I describe is rare, usually we have list of some merely simple data. Issue that we can't say that ViewModel can't be used there just because android team made such "framework". Dot NET/WPF has better approaches for example. So if we need View Model to operate in lists we need implement own realization of MVVM. E.g. bind/unbind vm when view attached/detached : https://stackoverflow.com/questions/31683811/recyclerview-callback-when-view-is-no-longer-visible – æ-ra-code Feb 12 '21 at 21:20
  • 1
    I agree and I don't mind it at all. It's that usually when someone asks about MVVM or view model in android they're using library provided by AndroidX which can cause such mess IMO. – Jeel Vankhede Feb 12 '21 at 21:23
  • Sorry, I took that very close, these misconceptions google unintendingly bring makes me worry. Yet still better than in some JS world most hyped frameworks. (like make troubles and then invent way how to solve them) Can you please edit your answer a bit - e.g. adding `androidx's` to ViewModel or something like, so then I can revert downvote and upvote instead? Thanks. – æ-ra-code Feb 12 '21 at 22:09
  • 1
    Great implementation, thanks – Nicolas Mage Mar 01 '22 at 03:46
5

Don't know if google has nice support for nested ViewModel's, looks like not. Thankfully, we don't need to stick to androidx.lifecycle.ViewModel to apply MVVM approach where we need. And there is a small example I decided to write:

Fragment, nothing changes:

    @Override public void onCreate(@Nullable Bundle savedInstanceState) {
        final ItemListAdapter adapter = new ItemListAdapter();
        binding.getRoot().setAdapter(adapter);

        viewModel = new ViewModelProvider(this).get(ItemListViewModel.class);
        viewModel.getItems().observe(getViewLifecycleOwner(), adapter::submitList);
    }

ItemListAdapter, in addition to populate view, it also becomes responsible for notifying item's observers - should they continue to listen, or not. In my example adapter was ListAdapter which extends RecyclerView.Adapter, so it receives list of items. This is unintentionally, just edited some code I already have. It's probably much better to use different base implementation, but it's acceptable for demonstration purposes:

    @Override public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new Holder(parent);
    }

    @Override public void onBindViewHolder(Holder holder, int position) {
        holder.lifecycle.setCurrentState(Lifecycle.State.RESUMED);
        holder.bind(getItem(position));
    }

    @Override public void onViewRecycled(Holder holder) {
        holder.lifecycle.setCurrentState(Lifecycle.State.DESTROYED);
    }

    // Idk, but these both may be used to pause/resume, while bind/recycle for start/stop.
    @Override public void onViewAttachedToWindow(Holder holder) { }
    @Override public void onViewDetachedFromWindow(Holder holder) { }

Holder. It implements LifecycleOwner, which allows to unsubscribe automatically, just copied from androidx.activity.ComponentActivity sources so all should be okay :D :

static class Holder extends RecyclerView.Holder implements LifecycleOwner {

    /*pkg*/ LifecycleRegistry lifecycle = new LifecycleRegistry(this);

    /*pkg*/ Holder(ViewGroup parent) { /* creating holder using parent's context */ }

    /*pkg*/ void bind(ItemViewModel viewModel) {
        viewModel.getItem().observe(this, binding.text1::setText);
    }

    @Override public Lifecycle getLifecycle() { return lifecycle; }
}

List view-model, "classique" androidx-ish ViewModel, but very rough, also provide nested view models. Please, pay attention, in this sample all view-models start to operate immediately, in constructor, until parent view-model is commanded to clear! Don't Try This at Home!

public class ItemListViewModel extends ViewModel {

    private final MutableLiveData<List<ItemViewModel>> items = new MutableLiveData<>();

    public ItemListViewModel() {
        final List<String> list = Items.getInstance().getItems();

        // create "nested" view-models which start background job immediately
        final List<ItemViewModel> itemsViewModels = list.stream()
                .map(ItemViewModel::new)
                .collect(Collectors.toList());

        items.setValue(itemsViewModels);
    }

    public LiveData<List<ItemViewModel>> getItems() { return items; }

    @Override protected void onCleared() {
        // need to clean nested view-models, otherwise...
        items.getValue().stream().forEach(ItemViewModel::cancel);
    }
}

Item's view-model, using a bit of rxJava to simulate some background work and updates. Intentionally I do not implement it as androidx....ViewModel, just to highlight that view-model is not what google names ViewModel but what behaves as view-model. In actual program it most likely will extend, though:

// Wow, we can implement ViewModel without androidx.lifecycle.ViewModel, that's cool!
public class ItemViewModel {

    private final MutableLiveData<String> item = new MutableLiveData<>();

    private final AtomicReference<Disposable> work = new AtomicReference<>();

    public ItemViewModel(String topicInitial) {
        item.setValue(topicInitial);
        // start updating ViewModel right now :D
        DisposableHelper.set(work, Observable
            .interval((long) (Math.random() * 5 + 1), TimeUnit.SECONDS)
                    .map(i -> topicInitial + " " + (int) (Math.random() * 100) )
                    .subscribe(item::postValue));
    }

    public LiveData<String> getItem() { return item; }

    public void cancel() {
        DisposableHelper.dispose(work);
    }

}

Few notes, in this sample:

  • "Parent" ViewModel lives in activity scope, so all its data (nested view models) as well.
  • In this example all nested vm start to operate immediately. Which is not what we want. We want to modify constructors, onBind, onRecycle and related methods accordingly.
  • Please, test it on memory leaks.
æ-ra-code
  • 2,140
  • 30
  • 28
  • 1
    Thank you for your answer! Your answer is pretty much the solution to my problem. I think that the only thing which you missed is that the Holder's Lifecycle is still in the RESUMED state when its Activity is backgrounded (but still alive) but it can be improved with additional set of callbacks. – Mariusz Feb 28 '21 at 20:09
  • Thanks for this note: tbh, I didn't check it. But if that is a case, please have a look at `onViewDetachedFromWindow` - it may help. – æ-ra-code Mar 01 '21 at 18:51
  • Not related: If somebody is curious how LiveData depends on lifecycle - check this [this](https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-lifecycle-release/lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/LiveData.java#415) source. – æ-ra-code Mar 01 '21 at 18:56
4

Although that is true that Android uses ViewModels in Android Architecture Components it does not mean that they are just part of AAC. In fact, ViewModels are one of the components of the MVVM Architecture Pattern, which is not Android only related. So ViewModel's actual purpose is not to preserve data across Android's lifecycle changes. However, because of exposing its data without having a View's reference makes it ideal for the Android specific case in which the View can be recreated without affecting to the component that holds its state (the ViewModel). Nonetheless, it has other benefits such as facilitating the Separation of Concerns among others.

It is also important to mention that your case can not be 100% compared to the ViewPager-Fragments case, as the main difference is that the ViewHolders will be recycled between items. Even if ViewPager's Fragments are destroyed and recreated, they will still represent the same Fragment with that same data. That is why they can safely bind the data provided by their already existing ViewModel. However, in the ViewHolder case, when it is recreated, it can be representing a totally new item, so the data its supposed ViewModel could be providing may be incorrect, referencing the old item.

That being said you could easily make the ViewHolder become a ViewModelStoreOwner:

class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), ViewModelStoreOwner {

    private var viewModelStore: ViewModelStore = ViewModelStore()

    override fun getViewModelStore(): ViewModelStore = viewModelStore
}

This can still be useful if the data provided by the ViewModel is the same independently of the ViewHolder's item (shared state between all items). However, if that is not the case, then you would need to invalidate the ViewModelStore by calling viewModelStore.clear() and create a new ViewModel instance probably in ViewHolder's onViewRecycled. You will loose the advantage of keeping the state no matter the view's lifecycle, but can sometimes still be useful as to follow Separation of Concerns.

Finally, regarding to the option of using a LiveData instance to control the state, no matter if it is provided by a ViewHolder's shared or specific ViewModel or it is passed through the Adapter, you will need a LifecycleOwner to observe it. A better approach to using the current Fragment or Activity lifecycle is to just use the specific ViewHolder's actual lifecycle, as they are actually created and destroyed, by making them implement the LifecycleOwner interface. I created a small library which does exactly that.

Sarquella
  • 1,129
  • 9
  • 26
  • thank you for your answer. 1) You are right that my comparison with ViewPager is not correct - the Fragment needs to create its own ViewModel. 2) Regarding ViewModel & ViewModel in your answer - my items' ViewModels should live within the same ViewModelStore as their parent. I wouldn't want onCleared() to be called in methods like onViewRecycled(), because even if the view is not visible I might still want to keep some long running operation or its state. 3) Regarding your library - it's not handling scenarios where Activity/Fragment host of view ViewHolder is paused. – Mariusz May 01 '20 at 12:25
0

I followed this wonderfull answer HERE by aeracode with a one exception. Instead of ViewModel I've used Rx BehaviourSubject that work perfectly for me. In case of coroutines You can use alternatively StateFlow.

clas MyFragment: Fragment(){

   private val listSubject = BehaviorSubject.create<List<Items>>()
   ...
   private fun observeData() {
        viewModel.listLiveData.observe(viewLifecycleOwner) { list ->
            listSubject.onNext(list)
        }
   }
}

RecyclerView

class MyAdapter(
    private val listObservable: BehaviorSubject<List<Items>>
) : RecyclerView.Adapter<MyViewHolder>() {
   [...]
   override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.bindToData(getItem(position))
    }

    override fun onViewRecycled(holder: MyViewHolder) {
        holder.onViewRecycled()
    }
    ...
    class MyViewHolder(val binding: LayoutBinding) :
        RecyclerView.ViewHolder(binding.root) {

        private var disposable: Disposable? = null

        fun bindToData(item: Item) = with(binding) {
            titleTv.text = item.title
            disposable = listObservable.subscribe(::setItemList) <- Here You listen
        }

        fun onViewRecycled() {
            disposable?.dispose()
        }
}
murt
  • 3,790
  • 4
  • 37
  • 48