Search code examples
androidandroid-recyclerviewandroid-architecture-componentsandroid-viewmodel

View models for RecyclerView items


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.


Solution

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