Mobile Development 20 min read

Pitfalls of LiveData Observation and Lambda Optimizations in Android MVVM

When observing LiveData in Android MVVM, developers may be misled by log deduplication, Java‑8 lambda optimizations that reuse a single Observer, automatic replay of the latest value to new observers, and shared ViewModel navigation bugs, but using an Event wrapper, reflection to reset version counters, or switching to Kotlin Flow/SharedFlow can safely avoid these pitfalls.

vivo Internet Technology
vivo Internet Technology
vivo Internet Technology
Pitfalls of LiveData Observation and Lambda Optimizations in Android MVVM

Google's Jetpack MVVM suite promotes LiveData as a lifecycle‑aware, memory‑safe component that can replace EventBus or RxJava for state distribution in Android apps. While powerful, developers often encounter unexpected behavior when registering observers.

Observer callback count : In a test where an observer is added ten times in a loop, only two log callbacks appear. This is caused by Android's log system collapsing identical log lines that share the same timestamp and content. Adding a hash code to the log output reveals that all ten callbacks are actually invoked, but the log output is deduplicated.

Lambda optimization : Using Java 8 lambda syntax inside a loop leads the compiler to generate a single static Observer instance, so only one observer is registered. The following code demonstrates the issue:

public class JavaTestLiveDataActivity extends AppCompatActivity {
    private TestViewModel model;
    private String test = "12345";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_java_test_live_data);
        model = new ViewModelProvider(this).get(TestViewModel.class);
        test3();
        model.getCurrentName().setValue("3");
    }
    private void test3() {
        for (int i = 0; i < 10; i++) {
            model.getCurrentName().observe(this, new Observer
() {
                @Override
                public void onChanged(String s) {
                    Log.v("ttt", "s:" + s);
                }
            });
        }
    }
}

When the same loop is written with a lambda, the decompiled output shows a single static class:

private void test3() {
    for (int i = 0; i < 10; i++) {
        this.model.getCurrentName().observe(this, $$Lambda$JavaTestLiveDataActivity$zcrCJYfWItRTy4AC_xWfANwZkzE.INSTANCE);
    }
}
public final class $$Lambda$JavaTestLiveDataActivity$zcrCJYfWItRTy4AC_xWfANwZkzE implements Observer {
    public static final $$Lambda$JavaTestLiveDataActivity$zcrCJYfWItRTy4AC_xWfANwZkzE INSTANCE = new $$Lambda$JavaTestLiveDataActivity$zcrCJYfWItRTy4AC_xWfANwZkzE();
    public void onChanged(Object obj) {
        Log.v("ttt", "s:" + ((String) obj));
    }
}

The same behavior appears in Kotlin when the lambda does not capture external variables:

public final void test2() {
    MutableLiveData liveData = new MutableLiveData();
    int i = 0;
    do {
        int i2 = i;
        i++;
        liveData.observe(this, $$Lambda$KotlinTest$6ZY8yysFE1G_4okj2E0STUBMfmc.INSTANCE);
    } while (i <= 9);
    liveData.setValue(3);
}
public final class $$Lambda$KotlinTest$6ZY8yysFE1G_4okj2E0STUBMfmc implements Observer {
    public static final $$Lambda$KotlinTest$6ZY8yysFE1G_4okj2E0STUBMfmc INSTANCE = new $$Lambda$KotlinTest$6ZY8yysFE1G_4okj2E0STUBMfmc();
    public void onChanged(Object obj) {
        KotlinTest.m1490test2$lambda3((Integer) obj);
    }
}

When the lambda captures an external variable (e.g., outer ), the compiler generates a new anonymous class for each iteration, avoiding the optimization.

private void test3() {
    for (int i = 0; i < 10; i++) {
        model.getCurrentName().observe(this, new Observer() {
            public void onChanged(Object obj) {
                JavaTestLiveDataActivity.this.lambda$test33$0$JavaTestLiveDataActivity((String) obj);
            }
        });
    }
}

LiveData delivering previous values : LiveData keeps a version counter ( mVersion ) that increments on each setValue . Each observer wrapper stores mLastVersion . When an observer is added, if mLastVersion < mVersion , the observer receives the latest value immediately, which explains why an observer registered after a value change still receives that value.

Fragment navigation pitfall : Using a shared ActivityViewModel to navigate from a list fragment to a detail fragment can cause the list fragment to re‑navigate on back press because the LiveData still holds true . The observer fires again when the fragment’s onViewCreated runs.

Solutions :

Wrap the emitted value in an Event class that can be consumed only once.

Hook LiveData.observe via reflection to set mLastVersion equal to mVersion after registration.

Replace LiveData with Kotlin Flow / SharedFlow , which does not replay previous values unless explicitly configured.

Example of an Event wrapper:

class Event
(private val content: T) {
    var hasBeenHandled = false
        private set
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) null else { hasBeenHandled = true; content }
    }
    fun peekContent(): T = content
}

Example of a custom SmartLiveData that adjusts mLastVersion :

class SmartLiveData
: MutableLiveData
() {
    override fun observe(owner: LifecycleOwner, observer: Observer
) {
        super.observe(owner, observer)
        val livedataVersion = javaClass.superclass.superclass.getDeclaredField("mVersion").apply { isAccessible = true }
        val versionValue = livedataVersion.get(this)
        val mObserversField = javaClass.superclass.superclass.getDeclaredField("mObservers").apply { isAccessible = true }
        val observers = mObserversField.get(this)
        val getMethod = observers.javaClass.getDeclaredMethod("get", Any::class.java).apply { isAccessible = true }
        val wrapper = (getMethod.invoke(observers, observer) as Map.Entry<*, *>).value
        val mLastVersionField = wrapper!!.javaClass.superclass.getDeclaredField("mLastVersion").apply { isAccessible = true }
        mLastVersionField.set(wrapper, versionValue)
    }
}

Switching to MutableSharedFlow eliminates the replay issue:

class ListViewModel : ViewModel() {
    val _navigateToDetails = MutableSharedFlow
()
    fun userClicksOnButton() {
        viewModelScope.launch { _navigateToDetails.emit(true) }
    }
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    lifecycleScope.launch {
        model._navigateToDetails.collect { if (it) { /* navigate */ } }
    }
}

Conclusion : LiveData remains essential in Android architecture, but developers should be aware of lambda compiler optimizations, the default delivery of the latest value to new observers, and careful handling of shared ViewModels across fragments. Using an Event wrapper, reflection hooks, or migrating to Flow can mitigate these pitfalls.

JavaAndroidlambdaKotlinMVVMLiveDataObserver
vivo Internet Technology
Written by

vivo Internet Technology

Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.