View Models For Recyclerview Items
Solution 1:
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:
classRecyclerViewAdapter(privateval liveDataToObserve: LiveData<T>, privateval 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:
classRecyclerViewAdapter(privateval liveDataToObserve: LiveData<T>, privateval lifecycleOwner: LifecycleOwner) : RecyclerView.Adapter<ViewHolder>() {
overridefun onCreateViewHolder: ViewHolder {// Some piece of code for binding
binding.lifecycleOwner = this@RecyclerViewAdapter.lifecycleOwner
// Another piece of code and return viewholder
}
}
classViewHolder(privateval 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
:
If you're using coroutines then you can use
lifecycleScope
fromlifecycleOwner
to call your operation and then provide data back to particular observingLiveData
without explicitly handling clear out case (LifecycleScope
would take care of it for you).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 duringonViewRecycled()
oronViewDetachedFromWindow()
callbacks. Important thing here isLiveData
which respects lifecycle of givenLifecycleOwner
, not the ongoing async operation.
Solution 2:
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
:
classMyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), ViewModelStoreOwner {
privatevar viewModelStore: ViewModelStore = ViewModelStore()
overridefungetViewModelStore(): 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.
Solution 3:
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:
@OverridepublicvoidonCreate(@Nullable Bundle savedInstanceState) {
finalItemListAdapteradapter=newItemListAdapter();
binding.getRoot().setAdapter(adapter);
viewModel = newViewModelProvider(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:
@Overridepublic Holder onCreateViewHolder(ViewGroup parent, int viewType) {
returnnewHolder(parent);
}
@OverridepublicvoidonBindViewHolder(Holder holder, int position) {
holder.lifecycle.setCurrentState(Lifecycle.State.RESUMED);
holder.bind(getItem(position));
}
@OverridepublicvoidonViewRecycled(Holder holder) {
holder.lifecycle.setCurrentState(Lifecycle.State.DESTROYED);
}
// Idk, but these both may be used to pause/resume, while bind/recycle for start/stop.@OverridepublicvoidonViewAttachedToWindow(Holder holder) { }
@OverridepublicvoidonViewDetachedFromWindow(Holder holder) { }
Holder. It implements LifecycleOwner, which allows to unsubscribe automatically, just copied from androidx.activity.ComponentActivity
sources so all should be okay :D :
staticclassHolderextendsRecyclerView.Holder implementsLifecycleOwner {
/*pkg*/LifecycleRegistrylifecycle=newLifecycleRegistry(this);
/*pkg*/ Holder(ViewGroup parent) { /* creating holder using parent's context */ }
/*pkg*/voidbind(ItemViewModel viewModel) {
viewModel.getItem().observe(this, binding.text1::setText);
}
@Overridepublic 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!
publicclassItemListViewModelextendsViewModel {
privatefinal MutableLiveData<List<ItemViewModel>> items = newMutableLiveData<>();
publicItemListViewModel() {
final List<String> list = Items.getInstance().getItems();
// create "nested" view-models which start background job immediatelyfinal List<ItemViewModel> itemsViewModels = list.stream()
.map(ItemViewModel::new)
.collect(Collectors.toList());
items.setValue(itemsViewModels);
}
public LiveData<List<ItemViewModel>> getItems() { return items; }
@OverrideprotectedvoidonCleared() {
// 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!publicclassItemViewModel {
privatefinal MutableLiveData<String> item = newMutableLiveData<>();
privatefinal AtomicReference<Disposable> work = newAtomicReference<>();
publicItemViewModel(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; }
publicvoidcancel() {
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.
Solution 4:
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(){
privateval listSubject = BehaviorSubject.create<List<Items>>()
...
privatefunobserveData() {
viewModel.listLiveData.observe(viewLifecycleOwner) { list ->
listSubject.onNext(list)
}
}
}
RecyclerView
classMyAdapter(
privateval listObservable: BehaviorSubject<List<Items>>
) : RecyclerView.Adapter<MyViewHolder>() {
[...]
overridefunonBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bindToData(getItem(position))
}
overridefunonViewRecycled(holder: MyViewHolder) {
holder.onViewRecycled()
}
...
classMyViewHolder(val binding: LayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
privatevar disposable: Disposable? = nullfunbindToData(item: Item) = with(binding) {
titleTv.text = item.title
disposable = listObservable.subscribe(::setItemList) <- Here You listen
}
funonViewRecycled() {
disposable?.dispose()
}
}
Post a Comment for "View Models For Recyclerview Items"