One, foreword

In order to solve the architectural design confusion that has existed since the development of Android-App, Google has introduced jetpack-MvVM as a whole family bucket solution. As the core of the whole solution -LiveData, with its life cycle safety, memory safety and other advantages, even gradually replace EventBus, RxJava as the Android side of the state distribution component trend.

The app team of the official website mall also encountered some difficulties in the in-depth use of LiveData, especially in the use of LiveData observers. We will summarize and share these experiences here.

How many callbacks can the Observer receive

2.1 Why Can I Receive a Maximum of two Notifications

This is a typical case. When debugging the message bus, we usually print some logs on the receiver of the message to help us locate the problem. However, the printing of logs may also cause some confusion for us to locate the problem.

Let’s start by defining a minimalist ViewModel:

public class TestViewModel extends ViewModel {
    private MutableLiveData<String> currentName;
    public MutableLiveData<String> getCurrentName(a) {
        if (currentName == null) {
            currentName = new MutableLiveData<String>();
        }
        returncurrentName; }}Copy the code

Then take a look at our activity code;

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(a) {
 
        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); }}); }}}Copy the code

And you can think about, what is the result of running this program? We created a Livedata and observed it 10 times, each time with a different Observer. It looks like we’ve bound 10 observers to a single data source. When we modify the data source, we should have 10 notifications. Run to see the result:

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
Copy the code

Why did I only receive 2 callback notifications when I registered 10 observers? Can I write it another way?

Let’s add something to the Log code like print hashCode and see the result:

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
Copy the code

This time the result is normal, in fact, for many message bus debugging has a similar problem.

In fact, for a Log system, if it decides that if the timestamp is the same, then the following Log is the same, then it won’t print it again. We must pay attention to this detail, otherwise, in many cases, it will affect our judgment of the problem. Going back to the code we did not add hashCode to, we can see that the Log printed two times, but the notification received 10 times. Why print two? Because your timestamps are consistent, so are the follow-ups.

2.2 Weird compiler optimizations

And that’s not the end of the story:

The above code running in Android Studio will become gray, I believe a lot of code clean people a look will know why, it is not Java8 lambda, IDE automatically to prompt us to let us optimize the way to write bai, and a mouse on the automatic optimization, convenient.

The gray is gone, the code is concise, and the KPI is waving to me. Run it and try:

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

Strange, why only one log this time? Is it the Log system? Let me try adding a timestamp:

Take a look at the execution result:

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

Strange, why is it still only one log? I added 10 observers to the for loop. Is lambda causing the problem? Well, we can type up the number of observers and see what went wrong. Take a look at the source code, as shown below: our observers are actually stored in the map, we can extract the size of the map to see why.

Reflection take the size, notice that the LiveData we usually use is MutableLiveData, and this value is in LiveData, so it is getSuperclass().

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());
   }
Copy the code

Take a look at the execution result:

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
Copy the code

The map size is 1, not 10, so you can only receive one notification. So the question is, why do I add 10 observers to my for loop when I change it to lambda? Let’s decompile (using JADX directly to decompile our Debug app) and take a look.

private void test3(a) {
        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)); }}Copy the code

The compiler has added the same static observer (instead of 10), which explains why the map size is 1. We can delete lambda again, and see what happens when we decompile.

One last question, does this lamda optimization work all the time in any scenario? Let’s try it the other way:

private String outer = "123456";
 
private void test3(a) {
  for (int i = 0; i < 10; i++) {
   model.getCurrentName().observe(this, s -> Log.v("ttt"."s:"+ s + outer)); }}Copy the code

Notice that we’re also using lambda, but we’re introducing an external variable, which is different from the way we wrote lambda before. Look at the result of this decomcompilation;

private void test3(a) {
        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); }}); }}Copy the code

Take comfort in seeing the new keyword, which bypassed the optimization of Java8 lambda compilation.

1.3 Will Kotlin’s lambda notation have any holes

Given that most people now use Kotlin, let’s see if Kotlin’s lamda has the same kind of glitches as the Java8 lambda.

Take a look at Kotlin’s lambda:

fun test2(a) {
      val liveData = MutableLiveData<Int>()
      for (i in 0.9.) {
          liveData.observe(this,
              { t -> Log.v("ttt"."t:$t") })
      }
      liveData.value = 3
  }
Copy the code

Take a look at the result of decompilation:

public final void test2(a) {
        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); }}Copy the code

It seems that Kotlin’s lambda compilation is just as radical as Java8 lambda compilation, which optimise you into an object by default based on the for loop. Similarly, let’s let the lambda access external variables to see if there is any “negative optimization” left.

val test="12345"
fun test2(a) {
    val liveData = MutableLiveData<Int>()
    for (i in 0.9.) {
        liveData.observe(this,
            { t -> Log.v("ttt"."t:$t $test") })
    }
    liveData.value = 3
}
Copy the code

Take a look at the result of the decompilation:

public final void test2(a) {
       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);
   }

Copy the code

Everything is normal. Finally, is the non-lambda notation for normal Kotlin the same as the non-lambda notation for Java?

fun test1(a) {
       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
}
Copy the code

Take a look at the result of the decompilation:

public final void test11(a) {
        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);
}
Copy the code

All is well, and here we can make a conclusion.

For lambda scenarios in the middle of a for loop, both the Java8 compiler and the Kotlin compiler will optimize to use the same lambda by default when your lambda does not use external variables or functions.

The compiler’s starting point is good, the for loop in new different objects, will certainly lead to a certain degree of performance degradation (new out of something, after all, the last is to gc), but may not meet our expectations, the optimization is often may even cause our mistakes in some scenarios, so be careful when using.

2. Why did LiveData receive the message before Observe

2.1 Analyze the source code to find the cause

Let’s look at an example:

fun test1(a) {
        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()
 
}
Copy the code

This code means that I first updated a LiveData with a value of 3, and then 3s later I liveData registered an observer. Note here that I updated the value of LiveData first and registered the observer after a period of time, so at this point, I should theoretically not receive liveData messages. Since you sent the message first, I will observe it later, but the program execution result is:

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
Copy the code

This is weird and does not fit the design of a common message bus framework. Come and see what the source code is?

Every time we observe we create a wrapper. Let’s see what the wrapper does.

Note that the Wrapper has an onStateChanged method, which is the core of the event distribution. Let’s remember this entry for the moment and go back to our previous Observe method. The last line calls the addObserver method.

The process will eventually go to this dispatchEvent method, so keep going.

The mLifeCycleObserver is actually the LifecycleBoundObserver object new from the observe method that we started with, which is the wrapper variable. The onStateChanged method passes through a series of calls to the duties notify method shown below.

And the duties of the notice method are only one.

If mLastVersion < mVersion, the observer onchaged method will be called back to <strong=””>.

So let’s see how these two values change. First look at this mVersion;

You can see that the default value is start_version, which is -1. But every time I setValue it’s going to increase by 1.

The mLastVersion in the Observer has an initial value of -1.

To sum up:

  • The initial mVersion of Livedata is -1.

  • After one setValue, her value becomes 0.

  • An ObserverWrapper will be created each subsequent observe.

  • The Wrapper has an mLastVersion inside it that’s -1, and Observe’s function call will eventually go through a series of procedures to let the Notified method consider that LiveData’s mVersion is 0.

  • 0 is obviously greater than the Observer’s mlastVersion-1, so the observer listener must be triggered at this point.

2.2 Be careful with ActivityViewModels

This feature of Livedata can lead to disastrous results in some scenarios. For example, in the scenario of a single Activity with multiple fragments, it is inconvenient to synchronize data with the activity-fragment without the jetpack-MvVM component. But with the Jetpack-MVVM component, it becomes much easier to implement this mechanism. Here’s an example from the official website:

class SharedViewModel : ViewModel(a){
    val selected = MutableLiveData<Item>()
 
    fun select(item: Item) {
        selected.value = item
    }
}
 
class MasterFragment : Fragment(a){
 
    private lateinit var itemSelector: Selector
 
   
    private val model: SharedViewModel by activityViewModels(a)
 
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        itemSelector.setOnClickListener { item ->
            // Update the UI}}}class DetailFragment : Fragment(a){
    private val model: SharedViewModel by activityViewModels(a)
 
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
            // Update the UI}}})Copy the code

Simply share the ActivityViewModel between two fragments. It’s easy to use, but it can cause serious problems in some situations. In this scenario, we have an activity that displays the ListFragment by default. After clicking the ListFragment, we jump to the DetailFragment. Here’s the code:

class ListViewModel : ViewModel(a){
    private val _navigateToDetails = MutableLiveData<Boolean>()
 
    val navigateToDetails : LiveData<Boolean>
        get(a) = _navigateToDetails
 
    fun userClicksOnButton(a) {
        _navigateToDetails.value = true}}Copy the code

Take a look at the core ListFragment;

class ListFragment : Fragment(a){
     
    private val model: ListViewModel by activityViewModels(a)
 
    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()
            }
        }
    }
}
Copy the code

So what we’re going to do is we’re going to call our viewModel userClicksOnButton method and change the navigateToDetails liveData value to true, and then we’re going to listen for that liveData value, If true, jump to Detail fragment.

This process is fine at first glance. It can indeed jump to the DetailFragment after clicking it. However, when we click the return key on the DetailFragment page, it will theoretically return to the ListFragment. But the actual result is to go back to the ListFragment and immediately jump to the DetailFragment.

Why is that? The problem is in the Fragment life cycle, when you press the return key, the onViewCreated of the ListFragment will be executed again, and this time you observe that the value of Livedata was true, This triggers a jump to the DetailFragment process. Your page will never return to the list page.

2.3 Solution 1: Introduce the middle layer

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

class ListViewModel : ViewModel(a){
    private val _navigateToDetails = MutableLiveData<Event<Boolean>>()
 
    val navigateToDetails : LiveData<Event<Boolean>>
        get() = _navigateToDetails
 
 
    fun userClicksOnButton(a) {
        _navigateToDetails.value = Event(true)
    }
}
 
 
open class Event<out T> (private val content: T) {
 
    var hasBeenHandled = false
        private set // Only external reads are allowed but external writes are not allowed
 
    /** * The value taken by this function can only be consumed once */
    fun getContentIfNotHandled(a): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }
 
    /** * if you want to consume the previous value, call this method */
    fun peekContent(a): T = content
}

Copy the code

So we can just call getContentIfNotHandled() when we are listening:

model.navigateToDetails.observe(viewLifecycleOwner, { t -> t.getContentIfNotHandled()? .let {if (it){
                   parentFragmentManager.commit {
                       replace<DetailFragment>(R.id.fragment_container_view)
                       addToBackStack("name")}}}})Copy the code

2.4 Solution 2: Hook LiveData Observe method

In each observe, the value of mLastVersion is less than the value of mVersion, which is the root cause of the problem. Therefore, we can use reflection to set the value of mLastVersion to be equal to version in each Observer.

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
        // Get the value of liveData version
        val livedataVerionValue = livedataVersion.get(this)
        / / take mObservers Filed
        val mObserversFiled = javaClass.superclass.superclass.getDeclaredField("mObservers")
        mObserversFiled.isAccessible = true
        // Take the mObservers object
        val objectObservers = mObserversFiled.get(this)
        // Get the class SafeIterableMap to which the mObservers belong
        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
        //ObserverWrapperval mLastVersionField = objectWrapper!! .javaClass.superclass.getDeclaredField("mLastVersion")
        mLastVersionField.isAccessible = true
        // Assign the value of mVersion to mLastVersion to make it equal
        mLastVersionField.set(objectWrapper, livedataVerionValue)
 
    }
}
Copy the code

2.5 Solution 3: Use Kotlin-flow

If you’re still using Kotlin, the solution to this problem is much simpler and even more manageable. During this year’s Google I/O conference, Yigit made it clear in Jetpack’s AMA that Livedata exists to take care of Java users and will continue to be maintained in the near future. As a substitute for Livedata, Flow will gradually become the mainstream in the future (after all, Kotlin is gradually becoming the mainstream now), and if Flow is used, the above situation can be easily solved.

Rewrite the viewModel

class ListViewModel : ViewModel(a){
    val _navigateToDetails = MutableSharedFlow<Boolean>()
    fun userClicksOnButton(a) {
        viewModelScope.launch {
            _navigateToDetails.emit(true)}}}Copy the code

Then rewrite the listening mode can be;

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")}}}}}Copy the code

Let’s focus on SharedFlow, the heat flow constructor;

Its actual function is: when there is a new subscriber collect (collect is the observe in Livedata), it sends some data (replay) that has been sent before to collect. The default value is 0. So our code will not receive the previous message. Here you can try to change this replay to 1, you can reproduce the previous Livedata problem. This solution is superior to the previous two solutions, with the only drawback being that Flow does not support Java and only supports Kotlin.

Third, summary

On the whole, even with Kotlin Flow, LiveData is still an indispensable component of the current Android client architecture. After all, its life cycle security and memory security are so delicious that they can effectively reduce the burden of our ordinary business development. There are only three things to focus on when using it:

  • Use lambda smart tips from Android Studio with caution

  • Pay attention to whether Observe really needs to listen to messages before registering them

  • Be careful when using ActivityViewModel between activities and fragments.

Author: Wu Yue, Vivo Internet Front End Team