preface

Jetpack AAC series of articles:

What about Jetpack Lifecycle? Also the liver? It’s time for Jetpack LiveData to find out

Lifecycle was analyzed and learned how to listen gracefully to Lifecycle. This article will focus on one of the specific application scenarios for Lifecycle: The principles and use of LiveData. Through this article, you will learn:

1. Why is LiveData needed? 3. Principle of LiveData 4. Advantages and disadvantages of LiveData and its solutions

1. Why is LiveData needed?

An example of an asynchronous callback

What do you do when a feature needs to pull data from the web and present it on a page? It’s easy to think of three steps:

1. Request the network interface to obtain data. 2. The page invokes the interface and passes in a callback object. 3. Data notifies the UI of updates through the callback interface.

Typical code is as follows:

Lateinit var listener: InfoNotify fun getUserInfo(notify: Thread.sleep(2000) {// update this file to notify thread.sleep (2000) .notify(100) }.start() } interface InfoNotify { fun notify(a : Int) } }Copy the code

GetUserInfo (xx) passes in a callback object and notifies the callback interface when it gets the data in the thread:

findViewById(R.id.original_callback).setOnClickListener((v)->{ NetUtil.INSTANCE.getUserInfo(new NetUtil.InfoNotify() { @Override public void notify(int a) { runOnUiThread(()->{ Toast.makeText(LiveDataActivity.this, "a=" + a, Toast.LENGTH_SHORT).show(); }); }}); });Copy the code

This is a general approach to getting asynchronous information and presenting it, but it’s not perfect, with three problems:

First problem: When the App is returned to the desktop, the network interface will return data, and then the Toast will pop up. If we want to stop the Toast after the App is withdrawn to the background, we need to determine whether the current App is visible in the foreground before playing the Toast.

Second problem: If you exit LiveDataActivity while calling the network, then Toast the network data after it returns, because the Activity no longer exists, and Crash will occur. The ways to avoid this are as follows:

RunOnUiThread (()->{// If the Activity is being destroyed or already destroyed, there is no need for Toast. If (! LiveDataActivity.this.isFinishing() && ! LiveDataActivity.this.isDestroyed()) { Toast.makeText(LiveDataActivity.this, "a=" + a, Toast.LENGTH_SHORT).show(); }});Copy the code

Third problem: We know that inner classes hold references to outer classes, and new Netutil.infonotify () means that an anonymous inner class is built, and this inner class object is held by NetUtil. When the Activity exits, it cannot be released because it is held by an anonymous inner class, causing a memory leak. This can be avoided by: 1) Remove the InfoNotify listener from NetUtil in Activity onDestroy(). 2) Wrap InfoNotify objects with weak references in NetUtil.

As you can see, in order to solve the above three questions, need a lot more additional code, and the code is repetitive/representative is higher, so we expect to have a way to help us implement a simple asynchronous/synchronous communication problem, we need only look at the data, regardless of life cycle, such as memory leak problem. LiveData just fits the bill.

2. How to use LiveData

Simple synchronous use mode

It is divided into three steps: Step 1: Construct LiveData

Public class SimpleLiveData {//LiveData receives generic parameters private MutableLiveData<String> name; public MutableLiveData<String> getName() { if (name == null) { name = new MutableLiveData<>(); } return name; }}Copy the code

LiveData is an abstract class, and MutableLiveData is an implementation subclass. The above code actually wraps the data we are interested in in MutableLiveData, of type String. To get an instance of MutableLiveData, encapsulate it in Simple EliveData.

Step 2: Listen for LiveData changes With SimpleLiveData in hand, let’s see how to manipulate it:

Private void handleSingleLiveData() {// Construct LiveData simpleLiveData = new simpleLiveData (); Simplelivedata.getname ().observe(this, (data)-> { MakeText (LiveDataActivity. This, "singleLiveData Name :" + data, Toast.LENGTH_SHORT).show(); }); }Copy the code

Step 3: Actively change LiveData since there is an observer listening, there must be a place to actively initiate notification.

findViewById(R.id.btn_change_name).setOnClickListener((v)->{ int a = (int)(Math.random() * 10); LiveData simpleliveData.getName ().setValue("singleName:" + a); });Copy the code

SetValue (xx) will notify the observer of the second step when the LiveData data changes, and the observer will refresh the UI (Toast).

Simple asynchronous usage

As you may have noticed, the above data changes are initiated in the main thread, but our actual scenario is more likely to be initiated in the child thread, simulating the child thread to initiate data changes:

findViewById(R.id.btn_change_name).setOnClickListener((v)->{ new Thread(()->{ int a = (int)(Math.random() * 10); LiveData simpleliveData.getName ().postValue("singleName:" + a); }).start(); });Copy the code

Start the thread and update LiveData in the thread using postValue(xx).

It should be noted that when analyzing LiveData, many articles are used to explain it together with ViewModel, which makes it difficult for beginners to understand. In fact, they are different things and can be used separately. After the analysis of the two, and then used together will be more clear context.

3. Principle of LiveData

By comparing the traditional callback and LiveData, we find that LiveData has several disadvantages of simple use and no traditional callback. Next, we analyze how it avoids these disadvantages with questions.

Add observer

#LiveData.java public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? Super T> observer) {// This method caller must be in the main thread assertMainThread("observe"); If (owner.getLifecycle().getCurrentState() == DESTROYED) {// ignore return; } / / packaging observer LiveData. LifecycleBoundObserver wrapper = new LiveData. LifecycleBoundObserver (the owner, the observer). // Add the wrapper result to the Map liveData. ObserverWrapper existing = mobServers. putIfAbsent(observer, wrapper); . // Listen for lifecycle owner.getlifecycle ().addobserver (wrapper); }Copy the code

LifecycleBoundObserver:

#LiveData.java class LifecycleBoundObserver extends LiveData.ObserverWrapper implements LifecycleEventObserver { @NonNull final LifecycleOwner mOwner; LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) { super(observer); mOwner = owner; } boolean shouldBeActive() { return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED); }... @Override public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) { Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState(); If (currentState == DESTROYED) {// removeObserver(mObserver); // no longer distributes return; } Lifecycle.State prevState = null; while (prevState ! = currentState) { prevState = currentState; // Notify the observer activeStateChanged(shouldBeActive()); currentState = mOwner.getLifecycle().getCurrentState(); }}... }Copy the code

OnStateChanged () is the method defined in the LifecycleEventObserver interface, which inherits from LifecycleObserver. OnStateChanged () is called when the host (Activity/Fragment) life cycle changes.

We notice in the notes: “Important 1”

removeObserver(mObserver)
Copy the code

The goal is to remove the previously added observer from the Map. When the host (Activity/Fragment) is in a DESTROYED state, remove the LiveData listener to avoid memory leakage. This solves the third problem: memory leaks.

ShouldBeActive () shouldBeActive() is used to check whether the current host is active or not. The active state is defined as: the host state should be >=”STARTED” after activity.onstart () and before activity.onpause ().

When the host is active, it continues to notify the UI of data changes and then refreshes the UI. If it is inactive, for example, when the App loses focus (onPause() is called), the UI is not refreshed.

Inform observer

The observer receives notification of the data from two sources:

1. The life cycle of the host changes. 2. Triggered by calling setValue()/postValue().

The first scenario is analyzed above, and the second scenario is analyzed next.

The call stack LiveData. SetValue ()

First look at the method implementation:

# livedata.java protected void setValue(T value) {assertMainThread("setValue") must be called on the main thread; // add mVersion++; MData = value; // dispatchingValue(null); }Copy the code

Then look at dispatchingValue (xx)

#LiveData.java void dispatchingValue(@Nullable LiveData.ObserverWrapper initiator) { ... do { mDispatchInvalidated = false; if (initiator ! = null) {// Close-notice (adsense); initiator = null; } else {// Iterator< map. Entry<Observer<? super T>, LiveData.ObserverWrapper>> iterator = mObservers.iteratorWithAdditions(); iterator.hasNext(); ) { considerNotify(iterator.next().getValue()); if (mDispatchInvalidated) { break; } } } } while (mDispatchInvalidated); mDispatchingValue = false; }Copy the code

A search shows that dispatchingValue(xx) is called from two places. The observer receives notification of data from two sources.

When setValue(xx)/postValue(xx) is actively called, because no observer is specified for distribution, all observers are notified through the call. When the Lifecycle changes, data distribution is handled independently because each observer is bound to Lifecycle.

As shown, let notice (xx) is finally called:

# liveData.java private void considerNotify(LiveData.obServerWrapper observer) { observer.mActive) { return; } // Lifecycle will not be notified if observer.mactive is not assigned in time. if (! observer.shouldBeActive()) { observer.activeStateChanged(false); return; } if (observer.mlastVersion >= mVersion) {return; if (observer.mlastVersion >= mVersion) {return; } // Update the observer version observer.mlastVersion = mVersion; The observer / / final notice observer. MObserver. OnChanged (mData (T)); }Copy the code

As you can see, regardless of where the data notifications come from, they end up notifying observers only when they are active. This solves the first and second problems from the beginning.

No distinction between active/inactive

Of course, active or not is determined by calling the ObserverWrapper method, so if you want to receive data changes regardless of whether active or not, you can call the following method when adding an observer:

  simpleLiveData.getName().observeForever(s -> {
            Toast.makeText(LiveDataActivity.this, "singleLiveData name:" + s, Toast.LENGTH_SHORT).show();
        });
Copy the code

This method is called with no LifecycleOwner instance passed in, so the Observer is not associated with Lifecycle and of course there is no division between active and inactive. More intuitive is the name of the Observer: AlwaysActiveObserver (AlwaysActive). Bind Lifecycle Observer name: LifecycleBoundObserver (with restrictions).

LiveData. PostValue () call stack

#LiveData.java protected void postValue(T value) { boolean postTask; // Both child threads and main threads need to modify mPendingData. Synchronized (mDataLock) {// whether mPendingData is still queued for sending //mPendingData == NOT_SET postTask = mPendingData is not queued  == NOT_SET; mPendingData = value; } if (! PostTask) {// Indicate that the last Runnable has not been executed // directly return, do not need to switch to the main thread to return; } / / switch to the main thread execution Runnable ArchTaskExecutor. GetInstance () postToMainThread (mPostValueRunnable); }Copy the code

There is one obvious feature:

When postValue(xx) is called quickly, the data will be updated to the latest data stored in mPendingData. If the last data change has not been sent, no new Runnable will be executed. Therefore, it is possible that the observer will not receive all data changes, but will only be guaranteed to receive the latest updates.

Switch to the main thread to execute Runnable:

#LiveData.java private final Runnable mPostValueRunnable = new Runnable() { public void run() { Object newValue; synchronized (mDataLock) { newValue = mPendingData; // reset state mPendingData = NOT_SET; } // Send data change setValue((T) newValue); }};Copy the code

The role postValue (xx) :

Store the data in temporary variables and switch to the main thread to execute setValue(xx) to distribute the data changes.

4. Advantages and disadvantages of LiveData and its solutions

advantage

As you can probably tell from the fundamentals, LiveData is relatively simple and quick to get started. Its advantages are obvious:

A. Life cycle perception:

With Lifecycle, the state of each phase of the Lifecycle can be perceived and the different Lifecycle states can be processed accordingly.

Because you can sense the life cycle:

  • You can update the UI while active.
  • The UI keeps data up to date (from inactive to active, it always receives the latest data).
  • The observer does not need to be removed manually and there is no memory leak.
  • Activities/Fragments do not update the UI if they are not alive, avoiding crashes.
  • Sticky events are designed in such a way that new observers do not have to actively retrieve the latest data again.

There is an additional feature: With a slight twist, LiveData can be used as inter-component messaging.

B. Real-time data synchronization

When called by the main thread: livedata.setValue (xx) can directly notify the observer of the data. When called by the child thread: livedata.postValue (xx) holds the data temporarily, and switches to the main thread to call setValue(xx) to hold the data temporarily. Therefore, the steps from data change -> send notification -> observer receive data are not significantly time-consuming, and the UI can listen to data changes in real time.

disadvantage

A. PostValue (xx) data is lost

PostValue (xx) stores data in the mPendingData variable each time it is called, so subsequent data overwrites previous data. LiveData ensures that the UI gets the latest data, while the data changes may be lost in the process. The reason for the problem: Not every data change is posted to the main thread. So to notify each time, you need to repackage LiveData as follows:

public class LiveDataPostUtil { private static Handler handler; public static <T> void postValue(MutableLiveData<T> liveData, T data) { if (liveData == null || data == null) return; if (handler == null) { handler = new Handler(Looper.getMainLooper()); } handler.post(new CustomRunnable<>(liveData, data)); } static class CustomRunnable<T> implements Runnable{ private MutableLiveData<T> liveData; private T data; public CustomRunnable(MutableLiveData<T> liveData, T data) { this.liveData = liveData; this.data = data; } @Override public void run() { liveData.setValue(data); }}}Copy the code

B. Sticky events

I’m sure you’ve seen some blog analysis and know about the LiveData stickiness issue. Sticky events:

An observer that registers after a data change has occurred and receives notification of the change.

Let’s see what happens when this happens. Define a singleton that holds LiveData globally:

public class GlobalLiveData { private static class Inner { static GlobalLiveData ins = new GlobalLiveData(); } public static GlobalLiveData getInstance() { return Inner.ins; } private SimpleLiveData simpleLiveData; private GlobalLiveData() { simpleLiveData = new SimpleLiveData(); } public SimpleLiveData getSimpleLiveData() { return simpleLiveData; }}Copy the code

Listen for data changes in activity.oncreate () :

GlobalLiveData.getInstance().getSimpleLiveData().getName().observe(this, new Observer<String>() { @Override public void onChanged(String s) { Toast.makeText(LiveDataActivity.this, "global name:" + s, Toast.LENGTH_SHORT).show(); }});Copy the code

Then click the button to send the data change:

    findViewById(R.id.btn_change_name).setOnClickListener((v)->{
     GlobalLiveData.getInstance().getSimpleLiveData().getName().setValue("from global");
    });
Copy the code

After the data change is sent, the observer is notified and Toast, and everything is fine.

When the Activity closes and reopens, the Toast still pops up.

The phenomenon of viscous events occurs. It is clearly a newly registered observer, and there is no new data change at this time, but still receive the previous data. This and the implementation of LiveData, look at the core source code implementation:

# liveData.java private void considerNotify(LiveData.obServerWrapper observer) { When setValue/postValue occurs, mVersion++ // compares the current data version of LiveData with the data version of the observer. If the current data version of LiveData is found to be larger than the current data version of LiveData, it indicates that the observer has not been notified before and therefore needs to be notified. Otherwise, it does not notify the observer. if (observer.mLastVersion >= mVersion) { return; } // Keeping the version of the observer data consistent with the LiveData version indicates that the observer has consumed the latest data. observer.mLastVersion = mVersion; observer.mObserver.onChanged((T) mData); }Copy the code

Back to the process:

1, when the initial LiveData. MVersion = 1, ObserverWrapper. MLastVersion = 1, so not data into the Activity for the first time. If livedata.setValue () is clicked, livedata.mversion = 0; Because LiveData. MVersion > ObserverWrapper mLastVersion, so observers can receive notification. 3, when to exit the Activity came in again, because ObserverWrapper is new, new ObserverWrapper. MLastVersion = 1, and LiveData. MVersion = 0, Or greater than ObserverWrapper mLastVersion, therefore can still receive notification.

To solve this problem, the obvious idea is to start with the Version field, while LiveData and ObserverWrapper do not have exposed methods to modify version, so reflection comes to mind.

By reflecting changes ObserverWrapper. MLastVersion values, in keep with LiveData. When your first registered mVersion values are consistent.

This is also the mainstream solution for many blogs, because it is a bit too much to reflect the Map and then reflect the version of the Observer inside. Here is a solution, just get the liveData.mversion, LiveData provides the method:

    int getVersion() {
        return mVersion;
    }
Copy the code

So we just need to call this reflection method:

public class EasyLiveData<T> extends LiveData<T> { @Override public void observe(@NonNull @NotNull LifecycleOwner owner,  @NonNull @NotNull Observer<? super T> observer) { super.observe(owner, new EasyObserver<>(observer)); } @Override public void observeForever(@NonNull @NotNull Observer<? super T> observer) { super.observeForever(new EasyObserver<>(observer)); } @Override protected void setValue(T value) { super.setValue(value); } @Override protected void postValue(T value) { super.postValue(value); } class EasyObserver<T> implements Observer<T>{ private Observer observer; private boolean shouldConsumeFirstNotify; public EasyObserver(Observer observer) { this.observer = observer; shouldConsumeFirstNotify = isNewLiveData(EasyLiveData.this); } @override public void onChanged(T T) {Override public void onChanged(T T) { if (shouldConsumeFirstNotify) { observer.onChanged(t); } else {shouldConsumeFirstNotify = true if LiveData was changed before, this change should not be handled; } } private boolean isNewLiveData(LiveData liveData) { Class ldClass = LiveData.class; try { Method method = ldClass.getDeclaredMethod("getVersion"); method.setAccessible(true); Int version = (int)method.invoke(liveData); // If the version is -1, it indicates the initial state and no data changes have taken place in LiveData. return version == -1; } catch (Exception e) { e.printStackTrace(); } return true; }}}Copy the code

If you don’t want sticky events, use EasyLiveData as described above. Sticky/non-sticky events are compared as follows:

Viscous event

Inviscid event

Look at the advantages and disadvantages dialectically

The advantages of LiveData are obvious, but the disadvantages are also prominent. Although it is a disadvantage, from another perspective, it is different people’s opinion:

My guess is that LiveData is designed not for notification but for the UI to be aware of the latest data without having to request it again. Of course, in order to make LiveData more suitable for our application scenarios, it can be modified appropriately according to the above methods.

If you are developing in Java, LiveData is a sharp edge, and if you are using Kotlin, consider Flow.

The next article will analyze viewModels and thoroughly clarify why viewModels can store data and where they are used.

This article is based on: implementation ‘androidx. Appcompat: appcompat: 1.4.1’

LiveData demo & tools

If you like, please like, pay attention to your encouragement is my motivation to move forward

Continue to update, with me step by step system, in-depth study of Android

4, View Measure/Layout/Draw 5, Android events distribution of full service 6, Android invalidate postInvalidate/requestLayout thoroughly clarify 7, how do you determine the Android Window size/onMeasure () to be executed multiple times Android event driver Handler-message-Looper 9, Android keyboard in one move 10, Android coordinates completely clear 11, Android Activity/Window/View background Android IPC series 14, Android Storage series 15, Java concurrent series no longer confusion 16, Java thread pool series 17, Android Jetpack Android Jetpack is easy to learn and easy to understand