Preface

In order to solve the problem of confusing architecture design that has existed since Android-App development, Google has launched the Jetpack-MVVM series of solutions. As the core of the whole solution - LiveData, with its lifecycle security, memory security and other advantages, and even gradually replace EventBus, RxJava as the trend of Android side state distribution components.

The mall app team has encountered some difficulties in the process of using LiveData in depth, especially in the use of LiveData observers encountered a lot of pitfalls, we put these experiences here to do a summary and share.

How many callbacks can the Observer actually receive

Why do we receive at most 2 notifications

This is a typical case, when debugging a message bus scenario, we usually print some log logs at the receiver of the message to help us locate the problem, however the log printing can sometimes bring some confusion to our problem location, you can see the following example.

We start by defining a minimalist ViewModel:

1
2
3
4
5
6
7
8
9
public class TestViewModel extends ViewModel {
    private MutableLiveData<String> currentName;
    public MutableLiveData<String> getCurrentName() {
        if (currentName == null) {
            currentName = new MutableLiveData<String>();
        }
        return currentName;
    }
}

Then look at our activity code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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<String>() {
                @Override
                public void onChanged(String s) {
                    Log.v("ttt", "s:" + s);
                }
            });
        }
    }
}

Can you think of what the result of this program run would be? We create a Livedata and then Observe this Livedata 10 times, each time new a different Observer object, and it looks like we have made 10 Observer bindings to a data source. When we modify this data source, we should have 10 notifications. Run it and see the result.

1
2
2021-11-21 15:20:07.662 27500-27500/com.smart.myapplication V/ttt: s:3
2021-11-21 15:20:07.662 27500-27500/com.smart.myapplication V/ttt: s:3

Strange, why do I only get 2 callback notifications when I’ve obviously registered 10 observers? Try a different way of writing it?

Let’s add a part to the Log code like printing the hashCode and look at the result.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3  hashCode:217112568
2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3  hashCode:144514257
2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3  hashCode:72557366
2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3  hashCode:233087543
2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3  hashCode:22021028
2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3  hashCode:84260109
2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3  hashCode:94780610
2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3  hashCode:240593619
2021-11-21 15:22:59.377 27912-27912/com.smart.myapplication V/ttt: s:3  hashCode:207336976
2021-11-21 15:22:59.378 27912-27912/com.smart.myapplication V/ttt: s:3  hashCode:82154761

This time the result is normal, in fact for many message bus debugging there are similar problems.

In fact, for the Log system, if he determines that the timestamp is the same, and the content of the Log behind it is also the same, then he will not print the content repeatedly. It is important to pay attention to this detail here, otherwise in many cases, it will affect our judgement of the problem. If we go back to the code where we did not add the hashCode, we will understand it if we look more closely: only the Log prints two items, but the notification is received 10 times, so why print two items? Because your timestamp is the same and the subsequent content is the same.

Strange compilation optimizations

It doesn’t end here, look at the following diagram.

sobyte

The above code will be greyed out in android studio, I believe many people with code cleanliness will know why, this is the lambda of Java8, ide automatically gives us a hint to let us optimize the writing method, and a mouse click will automatically optimize it, very convenient.

sobyte

The grey is gone, the code is cleaner, try running it

1
2021-11-21 15:31:50.386 29136-29136/com.smart.myapplication V/ttt: s:3

Strange, why is there only one log this time? Is it still the Log logging system? I’ll try adding a timestamp then

sobyte

Look again at the execution results

1
2021-11-21 15:34:33.559 29509-29509/com.smart.myapplication V/ttt: s:3 time:1637480073559

I’ve added the observer 10 times in the for loop here. Is the lambda causing the problem? Well, let’s type out the number of Observers and see what’s wrong. Look at the source code, as shown below: our observers are actually stored inside this map, we can find out the reason by taking out the size of this map.

sobyte

Reflect to read this size, note that the LiveData we normally use is MutableLiveData and this value is in the LiveData, so it is getSuperclass().

1
2
3
4
5
6
private void hook(LiveData liveData) throws Exception {
       Field map = liveData.getClass().getSuperclass().getDeclaredField("mObservers");
       map.setAccessible(true);
       SafeIterableMap safeIterableMap = (SafeIterableMap) map.get(liveData);
       Log.v("ttt", "safeIterableMap size:" + safeIterableMap.size());
   }

Look again at the execution results

1
2
2021-11-21 15:40:37.010 30043-30043/com.smart.myapplication V/ttt: safeIterableMap size:1
2021-11-21 15:40:37.013 30043-30043/com.smart.myapplication V/ttt: s:3 time:1637480437013

Sure enough, the map size here is 1, not 10, so I must have received only 1 notification. So the question is, I’ve obviously added 10 observers to the for loop, why did I change to lambda and have 1 observer? Let’s decompile (using jadx to decompile our debug app directly) and see.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
private void test3() {
        for (int i = 0; i < 10; i++) {
            this.model.getCurrentName().observe(this, $$Lambda$JavaTestLiveDataActivity$zcrCJYfWItRTy4AC_xWfANwZkzE.INSTANCE);
        }
}
 
public final /* synthetic */ class $$Lambda$JavaTestLiveDataActivity$zcrCJYfWItRTy4AC_xWfANwZkzE implements Observer {
    public static final /* synthetic */ $$Lambda$JavaTestLiveDataActivity$zcrCJYfWItRTy4AC_xWfANwZkzE INSTANCE = new $$Lambda$JavaTestLiveDataActivity$zcrCJYfWItRTy4AC_xWfANwZkzE();
 
    private /* synthetic */ $$Lambda$JavaTestLiveDataActivity$zcrCJYfWItRTy4AC_xWfANwZkzE() {
    }
 
    public final void onChanged(Object obj) {
        Log.v("ttt", "s:" + ((String) obj));
    }
}

It is clear to see that the compiler has been clever in the compilation process because of the use of Java8 lambda, so the compiler automatically helps us to optimise Chengdu to add the same static observer, not 10, which explains why the map size is 1. We can remove the lambda again and see if the decompilation works.

One last question: does this lambda optimisation work regardless of the scenario? Let’s try a different way of writing

1
2
3
4
5
6
7
private String outer = "123456";
 
private void test3() {
  for (int i = 0; i < 10; i++) {
   model.getCurrentName().observe(this, s -> Log.v("ttt", "s:" + s + outer));
  }
}

Note that although we also use lambda in this way, we have introduced external variables, which are different from the previous lambda, and look at the result of decompiling this way.

1
2
3
4
5
6
7
8
9
private void test3() {
        for (int i = 0; i < 10; i++) {
            this.model.getCurrentName().observe(this, new Observer() {
                public final void onChanged(Object obj) {
                    JavaTestLiveDataActivity.this.lambda$test33$0$JavaTestLiveDataActivity((String) obj);
                }
            });
        }
}

It’s reassuring to see the new keyword, and this way of writing gets around the Java8 lambda compilation optimizations.

Does Kotlin’s lambda writing style have pitfalls?

Considering that most people use Kotlin nowadays, let’s try to see if Kotlin’s lambda writing style has the same pitfalls as Java8’s lambda.

Take a look at the way lambda is written in Kotlin.

1
2
3
4
5
6
7
8
fun test2() {
      val liveData = MutableLiveData<Int>()
      for (i in 0..9) {
          liveData.observe(this,
              { t -> Log.v("ttt", "t:$t") })
      }
      liveData.value = 3
  }

Look again at the results of the decompilation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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 /* synthetic */ class $$Lambda$KotlinTest$6ZY8yysFE1G_4okj2E0STUBMfmc implements Observer {
    public static final /* synthetic */ $$Lambda$KotlinTest$6ZY8yysFE1G_4okj2E0STUBMfmc INSTANCE = new $$Lambda$KotlinTest$6ZY8yysFE1G_4okj2E0STUBMfmc();
 
    private /* synthetic */ $$Lambda$KotlinTest$6ZY8yysFE1G_4okj2E0STUBMfmc() {
    }
 
    public final void onChanged(Object obj) {
        KotlinTest.m1490test2$lambda3((Integer) obj);
    }
}

It seems that Kotlin’s lambda compilation is just as aggressive as Java8’s lambda compilation, in that it takes the for loop and optimises it to an object by default. Likewise, let’s see if there’s any more “negative optimisation” by having the lambda access external variables.

1
2
3
4
5
6
7
8
9
val test="12345"
fun test2() {
    val liveData = MutableLiveData<Int>()
    for (i in 0..9) {
        liveData.observe(this,
            { t -> Log.v("ttt", "t:$t $test") })
    }
    liveData.value = 3
}

See the result of the decompilation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public final void test2() {
       MutableLiveData liveData = new MutableLiveData();
       int i = 0;
       do {
           int i2 = i;
           i++;
           liveData.observe(this, new Observer() {
               public final void onChanged(Object obj) {
                   KotlinTest.m1490test2$lambda3(KotlinTest.this, (Integer) obj);
               }
           });
       } while (i <= 9);
       liveData.setValue(3);
   }

Everything is working fine. Finally, let’s look at the non-lambda writing of ordinary Kotlin. Is it the same as the non-lambda writing of Java?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fun test1() {
       val liveData = MutableLiveData<Int>()
       for (i in 0..9) {
           liveData.observe(this, object : Observer<Int> {
               override fun onChanged(t: Int?) {
                   Log.v("ttt", "t:$t")
               }
           })
       }
       liveData.value = 3
}

See the result of the decompilation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public final void test11() {
        MutableLiveData liveData = new MutableLiveData();
        int i = 0;
        do {
            int i2 = i;
            i++;
            liveData.observe(this, new KotlinTest$test11$1());
        } while (i <= 9);
        liveData.setValue(3);
}

Everything is fine, and here we can draw a conclusion.

For scenarios where a lambda is used in the middle of a for loop, when no external variables or functions are used in your lambda, then either the Java8 compiler or the Kotlin compiler will by default optimize it for you to use the same lambda.

The compiler’s starting point is a good one, and newing different objects in a for loop will of course lead to some degree of performance degradation (after all, what’s new ends up in gc), but such optimizations may often not meet our expectations, and may even cause us to misjudge in certain scenarios, so be careful when using them.

Why LiveData receives messages before Observe

Analyze the source code to find the reason

Let’s look at an example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fun test1() {
        val liveData = MutableLiveData<Int>()
        Log.v("ttt","set live data value")
        liveData.value = 3
        Thread{
            Log.v("ttt","wait start")
            Thread.sleep(3000)
            runOnUiThread {
                Log.v("ttt","wait end start observe")
                liveData.observe(this,
                    { t -> Log.v("ttt", "t:$t") })
            }
        }.start()
 
}

What this code means is that I update a livedata to a value of 3, and then 3s later I livedata register an observer. Note here that I updated the livedata value first and then registered the observer some time later, so in theory I should not have received the livedata message at this point. Because you sent the message first and I observed it later. But the result of the program execution is :

1
2
3
4
2021-11-21 16:27:22.306 32275-32275/com.smart.myapplication V/ttt: set live data value
2021-11-21 16:27:22.306 32275-32388/com.smart.myapplication V/ttt: wait start
2021-11-21 16:27:25.311 32275-32275/com.smart.myapplication V/ttt: wait end start observe
2021-11-21 16:27:25.313 32275-32275/com.smart.myapplication V/ttt: t:3

This one is just plain weird and doesn’t fit the design of a message bus framework that we commonly see. Let’s take a look at the source code to see what’s really going on.

sobyte

Each time we observe we create a wrapper, see what this wrapper does.

sobyte

Note that this wrapper has an onStateChanged method, which is the core of the whole event distribution, so let’s remember this entry for now. Going back to our previous observe method, the last line is a call to the addObserver method, let’s see what is done in this method.

sobyte

Eventually the flow will go to this dispatchEvent method and continue to follow.

sobyte

The mLifeCycleObserver is in fact the LifecycleBoundObserver object we created in the observe method at the beginning, which is the variable of the wrapper. The onStateChanged method will eventually go through a series of calls to the considerNotify method as shown in the diagram below.

sobyte

sobyte

And the whole considerNotify method has only one effect.

sobyte

If the value of mLastVersion is < mVersion, then the onchaged method of the observer will be triggered, which means that it will call back to our observer method.

Let’s see how these two values change. First look at this mVersion.

sobyte

You can see that this value defaults to start_version, which is -1, but it is added by 1 each time the value is set.

sobyte

The initial value of mLastVersion in our observer is -1.

sobyte

To conclude.

  • The initial value of Livedata’s mVersion is -1.
  • After one setValue, her value becomes 0.
  • An ObserverWrapper is created each time you observe.
  • The Wrapper has a mLastVersion inside which is -1. Observe function calls will eventually go through a series of processes to the considerNotify method.
  • 0 is obviously greater than the observer’s mLastVersion-1, so the observer’s listener function will definitely be triggered at this point.

Be careful with ActivityViewModels

This feature of Livedata can have disastrous consequences in certain scenarios, for example, in a single Activity with multiple Fragments, it is very inconvenient to synchronize the Activity-Fragment without the Jetpack-mvvm component, but with the Jetpack-mvvm component, it is very easy to implement this mechanism. But with the Jetpack-mvvm component, it is very easy to implement this mechanism. Here is an example from the website.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class SharedViewModel : ViewModel() {
    val selected = MutableLiveData<Item>()
 
    fun select(item: Item) {
        selected.value = item
    }
}
 
class MasterFragment : Fragment() {
 
    private lateinit var itemSelector: Selector
 
   
    private val model: SharedViewModel by activityViewModels()
 
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        itemSelector.setOnClickListener { item ->
            // Update the UI
        }
    }
}
 
class DetailFragment : Fragment() {
    private val model: SharedViewModel by activityViewModels()
 
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
            // Update the UI
        })
    }
}

Just make the set of ActivityViewModel shared between the 2 fragments. It’s easy to use, but in some scenarios it can lead to some serious problems. Let’s take a look at this scenario, we have an activity that shows the ListFragment by default, after clicking on the ListFragment we will jump to the DetailFragment.

Take a look at the code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class ListViewModel : ViewModel() {
    private val _navigateToDetails = MutableLiveData<Boolean>()
 
    val navigateToDetails : LiveData<Boolean>
        get() = _navigateToDetails
 
    fun userClicksOnButton() {
        _navigateToDetails.value = true
    }
}

A further look at the core ListFragment.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class ListFragment : Fragment() {
     
    private val model: ListViewModel by activityViewModels()
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
         
    }
 
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        model.navigateToDetails.observe(viewLifecycleOwner, { t ->
            if (t) {
                parentFragmentManager.commit {
                    replace<DetailFragment>(R.id.fragment_container_view)
                    addToBackStack("name")
                }
            }
        })
    }
 
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_list, container, false).apply {
            findViewById<View>(R.id.to_detail).setOnClickListener {
                model.userClicksOnButton()
            }
        }
    }
}

You can see that our implementation mechanism is to click the button after we call the viewModel’s userClicksOnButton method to navigateToDetails this livedata value to true, and then listen to this LiveData value, if it is true, then jump to Detail this details fragment.

This process at first glance is no problem, after clicking can indeed jump to the DetailFragment, but when we clicked the return button in the DetailFragment page, the theory will return to the ListFragment, but the actual implementation results are back to the ListFragment immediately after jumping to the DetailFragment.

Why is this? When you press the return button, the onViewCreated of the ListFragment will be executed again, and this time you observe, the value before Livedata is true, so it will trigger the process of jumping to DetailFragment again. The result is that your page never goes back to the list page.

Solution One: Introducing an Intermediate Layer

As the old saying goes, all problems in computing can be solved by introducing an intermediate layer. Here too, we can try the idea of “a message is only consumed once” to solve the above problem. For example, if we wrap the LiveData values in a layer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class ListViewModel : ViewModel() {
    private val _navigateToDetails = MutableLiveData<Event<Boolean>>()
 
    val navigateToDetails : LiveData<Event<Boolean>>
        get() = _navigateToDetails
 
 
    fun userClicksOnButton() {
        _navigateToDetails.value = Event(true)
    }
}
 
 
open class Event<out T>(private val content: T) {
 
    var hasBeenHandled = false
        private set // 只允许外部读 不允许外部写这个值
 
    /**
     * 通过这个函数取的value 只能被消费一次
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }
 
    /**
     * 如果想消费之前的value 那就直接调用这个方法即可
     */
    fun peekContent(): T = content
}

This way we can just call the method getContentIfNotHandled() when we do the listening.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
model.navigateToDetails.observe(viewLifecycleOwner, { t ->
           t.getContentIfNotHandled()?.let {
               if (it){
                   parentFragmentManager.commit {
                       replace<DetailFragment>(R.id.fragment_container_view)
                       addToBackStack("name")
                   }
               }
           }
       })

Solution 2: Hook LiveData’s observe method

We have analyzed the previous article, each time observe, mLastVersion value is less than the value of mVersion is the root of the problem, then we use reflection, each time the observer will be set to the value of mLastVersion and version equal to it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class SmartLiveData<T> : MutableLiveData<T>() {
    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        super.observe(owner, observer)
        //get livedata version
        val livedataVersion = javaClass.superclass.superclass.getDeclaredField("mVersion")
        livedataVersion.isAccessible = true
        // 获取livedata version的值
        val livedataVerionValue = livedataVersion.get(this)
        // 取 mObservers Filed
        val mObserversFiled = javaClass.superclass.superclass.getDeclaredField("mObservers")
        mObserversFiled.isAccessible = true
        // 取 mObservers 对象
        val objectObservers = mObserversFiled.get(this)
        // 取 mObservers 对象 所属的class SafeIterableMap
        val objectObserversClass = objectObservers.javaClass
        val methodGet = objectObserversClass.getDeclaredMethod("get", Any::class.java)
        methodGet.isAccessible = true
        //LifecycleBoundObserver
        val objectWrapper = (methodGet.invoke(objectObservers, observer) as Map.Entry<*, *>).value
        //ObserverWrapper
        val mLastVersionField = objectWrapper!!.javaClass.superclass.getDeclaredField("mLastVersion")
        mLastVersionField.isAccessible = true
        //将 mVersion的值 赋值给 mLastVersion 使其相等
        mLastVersionField.set(objectWrapper, livedataVerionValue)
 
    }
}

Solution 3: Use Kotlin-Flow

If you’re still using Kotlin, the solution to this problem is much simpler, and even the process is made more manageable. At this year’s Google I/O conference, Yigit made it clear in the Jetpack AMA that Livedata exists to take care of Java users and will continue to be maintained in the short term (what that means is something you can taste for yourself), and that Flow, the replacement for Livedata, will become mainstream in the future (after all, Kotlin is becoming mainstream now). If you use Flow, the above scenario can be solved.

Rewrite the viewModel.

1
2
3
4
5
6
7
8
class ListViewModel : ViewModel() {
    val _navigateToDetails = MutableSharedFlow<Boolean>()
    fun userClicksOnButton() {
        viewModelScope.launch {
            _navigateToDetails.emit(true)
        }
    }
}

then just rewrite the way it listens.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        lifecycleScope.launch {
            model._navigateToDetails.collect {
                if (it) {
                    parentFragmentManager.commit {
                        replace<DetailFragment>(R.id.fragment_container_view)
                        addToBackStack("name")
                    }
                }
            }
        }
    }

We focus on the constructor for SharedFlow, a heat flow.

sobyte

The default value is 0. So our code above will not receive the previous message. Here you can try changing this replay to 1 to reproduce the previous Livedata problem. The only drawback is that Flow does not support Java, only Kotlin.

Summary

On the whole, even with Kotlin Flow, LiveData is still an indispensable part of the current Android client architecture components, after all, its lifecycle safety and memory safety are too fragrant, which can effectively reduce the burden of our usual business development, and when using him we just need to pay attention to 3 aspects to avoid the pitfalls.

  • Be careful with the lambda smart hints given by Android Studio
  • Pay more attention to whether you really need Observe to register the message before listening
  • Be careful when using ActivityViewModel between Activity and Fragment.