Common Issues with LiveData Usage

LiveData serves as a powerful tool in data-driven architectures, especially when used alongside ViewModel for binding data to Fragments or Activities. It abstracts away the complexity of managing data lifecycle manually, which is handled by the framwork. How ever, certain design aspects of LiveData can lead to unexpected behaviors during usage.

Sticky Behavior

Symptoms

When setValue is called before observing the LiveData, or when observing occurs in onCreate without prior setValue/postValue, subsequent lifecycle changes may trigger observation callbacks.

Explanation

In addition to setValue and postValue, LiveData also observes the lifecycle of its host component and notifies observers accordingly.

The registration of an observer through observe creates a LifecycleBoundObserver:

@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
    assertMainThread("observe");
    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);
}

The LifecycleBoundObserver listens to lifecycle events and invokes activeStateChanged upon state changes:

@Override
public void onStateChanged(@NonNull LifecycleOwner source,
        @NonNull Lifecycle.Event event) {
    Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
    if (currentState == DESTROYED) {
        removeObserver(mObserver);
        return;
    }
    Lifecycle.State prevState = null;
    while (prevState != currentState) {
        prevState = currentState;
        activeStateChanged(shouldBeActive());
        currentState = mOwner.getLifecycle().getCurrentState();
    }
}

Within activeStateChanged, considerNotify determines whether to propagate updates:

private void considerNotify(ObserverWrapper observer) {
    if (!observer.mActive) {
        return;
    }
    if (!observer.shouldBeActive()) {
        observer.activeStateChanged(false);
        return;
    }
    if (observer.mLastVersion >= mVersion) {
        return;
    }
    observer.mLastVersion = mVersion;
    observer.mObserver.onChanged((T) mData);
}

Here, mLastVersion starts at -1 and mVersion at 0. This implies that even without explicit value setting, a change due to lifecycle transitions will be observed—a behavior designed to align data with UI lifecycle. Such data is referred to as State Data. For scenarios resembling EventBus, where events should only be consumed once, Google introduced SingleLiveEvent to address sticky behavior.

SingleLiveEvent Implementation

public class SingleLiveEvent<T> extends MutableLiveData<T> {

    private static final String TAG = "SingleLiveEvent";

    private final AtomicBoolean mPending = new AtomicBoolean(false);

    @MainThread
    @Override
    public void observe(LifecycleOwner owner, final Observer<? super T> observer) {

        if (hasActiveObservers()) {
            Log.w(TAG, "Multiple observers registered but only one will be notified of changes.");
        }

        super.observe(owner, new Observer<T>() {
            @Override
            public void onChanged(@Nullable T t) {
                if (mPending.compareAndSet(true, false)) {
                    observer.onChanged(t);
                }
            }
        });
    }

    @MainThread
    @Override
    public void setValue(@Nullable T t) {
        mPending.set(true);
        super.setValue(t);
    }

    public void call() {
        setValue(null);
    }
}

This ensures that only one observer receives each update, mitigating sticky issues. However, it introduces another limitation: multiple observers will only receive values via the first one. A solution for this is the UnPeekLiveData pattern from KunMinX.

Lost Values with postValue

Symptoms

Repeatedly updating a LiveData may result in some intermediate values being lost.

Cause

In postValue, if the previous value has not been consumed, the current one gets discarded to prevent excessive refreshes:

protected void postValue(T value) {
    boolean postTask;
    synchronized (mDataLock) {
        postTask = mPendingData == NOT_SET;
        mPendingData = value;
    }
    if (!postTask) {
        return;
    }
    ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}
volatile Object mPendingData = NOT_SET;
private final Runnable mPostValueRunnable = new Runnable() {
    @SuppressWarnings("unchecked")
    @Override
    public void run() {
        Object newValue;
        synchronized (mDataLock) {
            newValue = mPendingData;
            mPendingData = NOT_SET;
        }
        setValue((T) newValue);
    }
};

Solution One

Use Kotlin’s Flow API instead.

Solution Two

Encapsulate LiveData using a class like SafeCount to manage concurrent updates:

public class SafeCount {
    private final AtomicInteger targetCount = new AtomicInteger(0);
    private final AtomicInteger tmpCount = new AtomicInteger(0);
    private final ConcurrentLinkedQueue<Integer> pendingData = new ConcurrentLinkedQueue<>();

    private final MutableLiveData<AtomicInteger> liveData;

    SafeCount(MutableLiveData<AtomicInteger> value) {
        liveData = value;
    }

    void reset() {
        targetCount.set(0);
        tmpCount.set(0);
        liveData.setValue(tmpCount);
    }

    void increment() {
        pendingData.offer(targetCount.incrementAndGet());
        XThread.runOnUiThread(() -> {
            while (!pendingData.isEmpty()) {
                Integer value = pendingData.poll();
                if (value != null) {
                    tmpCount.set(value);
                    liveData.setValue(tmpCount);
                }
            }
        });
    }
}

public class XThread {
    private final static Handler sMainHandler = new Handler(Looper.getMainLooper());

    public static void runOnUiThread(Runnable runnable) {
        if(isOnMainThread()){
            runnable.run();
        } else {
            sMainHandler.post(runnable);
        }
    }

    public static boolean isOnMainThread() {
        return Looper.myLooper() == Looper.getMainLooper();
    }
}

Tags: Android livedata mvvm architecture-components observable-pattern

Posted on Sun, 10 May 2026 11:12:09 +0000 by foolguitardude