Implementing Lifecycle‑Aware ViewHolder with MVVM in Android RecyclerView
This article proposes a solution to avoid data inconsistency caused by RecyclerView ViewHolder reuse by giving each ViewHolder its own lifecycle, integrating MVVM via a BaseLifecycleViewHolder, BaseLifeCycleAdapter, and VHLiveData, and demonstrates implementation details with Kotlin and Java code examples.
In Android app development, RecyclerView ViewHolders are frequently reused, which can lead to UI data mismatches when asynchronous operations update a ViewHolder that has already been recycled. To solve this, the article introduces a lifecycle‑aware ViewHolder that can participate in the MVVM pattern just like an Activity or Fragment.
Purpose : Enable each ViewHolder to have its own LifecycleOwner, use ViewModel and LiveData for data binding, and correctly unbind LiveData when the ViewHolder is recycled to prevent stale data from being displayed.
Solution Overview :
Create an abstract class BaseLifecycleViewHolder that extends RecyclerView.ViewHolder and implements LifecycleOwner .
Create BaseLifeCycleAdapter that registers the ViewHolder’s lifecycle in onBindViewHolder .
Introduce VHLiveData extending MutableLiveData with a bindLifecycleOwner method to replace the native observe call.
Use a resetVersion flag to handle cases where a ViewHolder is attached/detached without being recycled.
Technical Implementation
1. BaseLifecycleViewHolder
The class holds a LifecycleRegistry and overrides getLifecycle() :
private var mLifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this@BaseLifeCycleViewHolder)
override fun getLifecycle(): Lifecycle { return mLifecycleRegistry }2. Registering Lifecycle in the Adapter
@Override
public void onBindViewHolder(@NonNull VH holder, int position) {
holder.registerLifecycle(true);
super.onBindViewHolder(holder, position);
}3. Lifecycle Registration Logic
itemView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewDetachedFromWindow(v: View?) {
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
override fun onViewAttachedToWindow(v: View?) {
if (mLifecycleRegistry.currentState != Lifecycle.State.STARTED) {
registerLifecycle(false)
}
}
})The registerLifecycle(resetVersion: Boolean) method triggers lifecycle events and optionally increments a version counter to differentiate reuse instances:
fun registerLifecycle(resetVersion: Boolean) {
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
if (resetVersion) {
mViewHolderVersion++ // version increments on reuse
}
val bindList = bindLiveData(ArrayList
, Observer
>>())
bindList?.forEach {
it.first?.bindLifecycleOwner(this, it.second!!, resetVersion)
}
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
}4. Abstract bindLiveData Method
abstract fun bindLiveData(list: ArrayList
, Observer
>>): ArrayList
, Observer
>>?5. Example bindLiveData Implementation
@org.jetbrains.annotations.Nullable
@Override
public ArrayList
, Observer
>> bindLiveData(@NotNull ArrayList
, Observer
>> list) {
list.add(new Pair(mViewModel.getMRoomStatus(), new Observer
() {
@Override
public void onChanged(@Nullable Integer status) {
setRoomText(status);
}
}));
return list;
}6. Native LiveData Source Analysis
The article reviews the Android LiveData source, showing how observe() wraps the observer in a LifecycleBoundObserver and registers it with the owner’s lifecycle.
@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer
observer) {
if (owner.getLifecycle().getCurrentState() == DESTROYED) return;
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
if (existing != null && !existing.isAttachedTo(owner)) {
throw new IllegalArgumentException("Cannot add the same observer with different lifecycles");
}
if (existing != null) return;
owner.getLifecycle().addObserver(wrapper);
}Key internal methods such as activeStateChanged() , dispatchingValue() , and considerNotify() are examined to illustrate how LiveData decides when to deliver updates based on lifecycle state and version counters.
void activeStateChanged(boolean newActive) {
if (newActive == mActive) return;
mActive = newActive;
boolean wasInactive = LiveData.this.mActiveCount == 0;
LiveData.this.mActiveCount += mActive ? 1 : -1;
if (wasInactive && mActive) onActive();
if (LiveData.this.mActiveCount == 0 && !mActive) onInactive();
if (mActive) dispatchingValue(this);
}Version handling in setValue() is also highlighted:
@MainThread
protected void setValue(T value) {
assertMainThread("setValue");
mVersion++;
mData = value;
dispatchingValue(null);
}7. VHLiveData Customization
The custom VHLiveData class overrides bindLifecycleOwner to call the original observe and then, when resetVersion is true, uses reflection to copy the current mVersion into the observer’s mLastVersion , ensuring that recycled ViewHolders do not receive stale updates.
public class VHLiveData
extends MutableLiveData
{
public void bindLifecycleOwner(@NonNull LifecycleOwner owner, @NonNull Observer observer, boolean resetVersion) {
super.observe(owner, observer);
if (resetVersion) {
try {
Class hySuperClass = LiveData.class;
Field observers = hySuperClass.getDeclaredField("mObservers");
observers.setAccessible(true);
Object objectObservers = observers.get(this);
Class
classObservers = objectObservers.getClass();
Method methodGet = classObservers.getDeclaredMethod("get", Object.class);
methodGet.setAccessible(true);
Object objectWrapperEntry = methodGet.invoke(objectObservers, observer);
Object objectWrapper = null;
if (objectWrapperEntry instanceof Map.Entry) {
objectWrapper = ((Map.Entry) objectWrapperEntry).getValue();
}
if (objectWrapper != null) {
Class
classObserverWrapper = objectWrapper.getClass().getSuperclass();
Field lastVersion = classObserverWrapper.getDeclaredField("mLastVersion");
lastVersion.setAccessible(true);
Field version = hySuperClass.getDeclaredField("mVersion");
version.setAccessible(true);
Object objectVersion = version.get(this);
lastVersion.set(objectWrapper, objectVersion);
LogUtil.d("bigcatduan1", "set mLastVersion: " + objectVersion);
}
} catch (Exception e) {
LogUtil.e("bigcatduan1", "set mLastVersion failed");
e.printStackTrace();
}
}
}
}8. resetVersion Flag
The resetVersion flag is passed from BaseLifeCycleViewHolder to indicate whether the ViewHolder’s lifecycle has truly restarted (i.e., after a full recycle) or merely toggled between attached/detached without being reclaimed; only the former requires version reset.
Conclusion
The proposed architecture allows each ViewHolder to behave like an Activity/Fragment with its own lifecycle, enabling clean MVVM data binding and eliminating UI inconsistencies caused by RecyclerView reuse. Future work includes extending ViewModel sharing across ViewHolders similar to Activity‑Fragment communication.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.