Public number: byte array, hope to help you 🤣🤣

I. Architecture guidelines

In daily development, we often talk about MVC, MVP, MVVM and other development patterns, which are actually different ways of presenting the application architecture. What application architecture are you currently using?

A good architecture should follow at least two principles

  • Separation of concerns. Separation of concerns means that each layer in the architecture should focus only on achieving a specific purpose. A common mistake is to make inActivityFragment(for example, completing network requests directly at the interface layer), such interface-based classes should only contain logic for interacting with the system and users, and you should keep these classes as lean as possible to avoid many life-cycle related problems
  • Drive the interface through the model. A model is the component responsible for processing application data. The model should be independent of the application’s interface layer and other application components so that it is not affected by the application’s life cycle and related concerns

On the first point. For a mobile device, its resources are fixed and extremely limited, and the system may terminate some application processes at any time to make room for foreground processes. Furthermore, even for foreground processes, we don’t own all activities and fragments, and the system can destroy them at any time due to unexpected conditions such as insufficient memory or system configuration changes. Separation of concerns separates the responsibilities at each level and avoids binding user data to the lifecycle of the interface so that it is not destroyed when something unexpected happens

On the second point. Model-driven interfaces should exist independently of the model as an observer, and should not hold data directly. Interfaces drive themselves by observing changes in data, and models can drive interface change by changing themselves. The best models to refer to here are persistent models, where the model’s lifetime needs to be longer than the interface or even the application process, typically the ViewModel and Room in Jetpack. This way, when something unexpected happens, the user doesn’t lose the data, and we can recover the data for the user later

Google’s recommended application architecture diagram is shown below

Where each component depends only on the component at the level below it. ViewModel is a concrete implementation of the principle of separation of concerns. It exists as a carrier and processor of user data. Activities/fragments only rely on ViewModel, which is used to respond to the input of the interface layer and drive the changes of the interface layer. Repository is used to provide a single data source and data storage domain for the ViewModel. Repository can rely on both the persistent data model and remote server data sources

Second, the advantages of LiveData

It is the LiveData contained in the ViewModel that I want to discuss in this article

As you can see from Google’s official recommended application architecture diagram, LiveData is included in the ViewModel. LiveData is a kind of observable data storage, Activity/Fragment is the observer, LiveData is the observed. LiveData has lifecycle awareness. When we register an Observer with LiveData bundled with LifecycleOwner, if the LifecycleOwner is in a STARTED or RESUMED state, Then, the observer is considered to be in the active state, and LiveData will send event notification to the observer only at this time. The inactive observer will not receive any event notification. When LifecycleOwner changes to DESTROYED, LiveData will automatically unbind the viewer to prevent memory leakage and excessive memory consumption. Therefore, LiveData has lifecycle awareness and activities/fragments do not need to create explicit and strict dependency paths with LiveData

ViewModel and LiveData can be seen as a joint implementation of the separation of concerns and model-driven interface principles. ViewModel provides the ability for user data to exist independently of the interface, and LiveData provides the ability to safely notify and drive interface changes

3. Defects of LiveData

LiveData is designed to have the following three flaws (or features) :

  1. Event notifications are received only if the Observer is at least STARTED. An Activity receives an event notification only after onStart and before onPause
  2. LiveData is sticky. Given that there is a static LiveData variable that already contains data, all activities listening on it will receive a callback notification of its current value, that is, a sticky message. This concept is similar to StickyEvent in EventBus
  3. The median value can be directly masked by the new value. If LiveData receives multiple values in succession while the Activity is in the background, the Activity will only receive a callback to the latest value when it returns to the foreground. The intermediate value is masked and the Activity will not feel the existence of the intermediate value at all

The above three characteristics are determined by the characteristics of the interface layer:

  • When the interface is in the background, there is no need to update the interface at this time, because the interface is completely invisible to the user, and the interface may never have a chance to come back to the foreground, so it only makes sense to update the interface when it comes back to the foreground
  • When the interface is accidentally destroyed, we need to rebuild the interface based on existing data, so LiveData is designed to be sticky
  • For the interface state value represented by LiveData, we often need only its latest state and do not need to deal with the intermediate value, so the intermediate value of LiveData can be directly masked by the new value

LiveData as a message notification component

If LiveData is simply viewed as a carrier of interface level status updates, then the above three features are quite reasonable. However, if we are using LiveData as the notification component for the global application, these three features can get in the way

Many developers have tried to declare a single instance of LiveData as a static variable, and then multiple activities can communicate by listening to that LiveData at the same time. The advantage of this approach is that cross-page communication can be implemented in a very simple manner, while also ensuring lifecycle safety. The disadvantage is that you cannot receive notifications when the Activity is in the onStop state and will receive dirty data such as sticky messages

What’s the point of receiving a notification when your Activity is in the onStop state? What’s the problem with receiving sticky messages? This can be illustrated by assuming a requirement

Assume that your App currently contains a list of circles. Each circle item contains a button that changes the attention status of the circle. When clicked, an animation is shown to the user for hundreds of milliseconds. Click item to jump to the circle details page, which also contains a button to change the circle’s attention status

The product now requires that the attention status between the two pages be unified in real time, i.e. when the circle details page changes its attention status, the circle list page also changes its attention status. To achieve this effect, you can declare a global static variable that notifies the circle of state changes across the page

object FocusRepository {

    //String indicates the circle ID. Boolean indicates the attention status of the circle
    val focusLiveData = MutableLiveData<Pair<String, Boolean> > ()}class CircleListActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_circle_list)
        // Set up a listener
        FocusRepository.focusLiveData.observe(this, Observer {

        })
    }

}

class CircleDetailsActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_circle_details)
        onCircleFocusStateChanged("100".true)}private fun onCircleFocusStateChanged(circleId: String, focused: Boolean) {
        FocusRepository.focusLiveData.value = Pair(circleId, focused)
    }

}
Copy the code

This approach leads to three problems:

  • When the user returns after the CircleDetailsActivity changes the circle’s attention state, the CircleListActivity will receive the event notification of focusLiveData when it returns to the foreground from the background, and then the animation will be triggered. In this case, we don’t want the user to see the animation, but rather to be able to trigger the animation in real time as the CircleListActivity changes its focus state. At this point, using LiveData does not meet our requirements. LiveData does not support sending notifications when the Activity is in the onStop state
  • Another problem arises if the CircleDetailsActivity changes the attention state of multiple circles in succession: the median value is directly masked by the latest value. This is also because LiveData does not support sending notifications when an Activity is in the onStop state
  • In cases where focusLiveData already has a value, the user gets a callback notification from focusLiveData when he first opens CircleListActivity. FocusLiveData becomes dirty when the CircleListActivity data is retrieved from the server and is guaranteed to be up to date and does not require callback notifications of local values. In this case, LiveData can also cause us problems, as sticky messages are actually dirty data

Fifth, EventLiveData

Considering that LiveData is not suitable for use as a global notification component for applications, I implemented a modified version of EventLiveData based on its source code to address LiveData’s shortcomings. EventLiveData is basically the same as LiveData, I just extend it

Send a message:

        val eventLiveData = EventLiveData<String>()

        // main thread call
        eventLiveData.setValue("leavesC")
        // child thread call
        eventLiveData.postValue("leavesC")
        // It can be called by any thread
        eventLiveData.submitValue("leavesC")
Copy the code

Do not receive sticky messages:

        // Do not receive sticky messages
        Observer callbacks are received after onResume and before onDestroy
        eventLiveData.observe(LifecycleOwner, Observer {

        })

        // Do not receive sticky messages
        // An Observer callback is received after onCreate and before onDestroy
        eventLiveData.observe(LifecycleOwner, Observer {

        }, false)
Copy the code

Receiving sticky messages:

        // Receive sticky messages
        Observer callbacks are received after onResume and before onDestroy
        eventLiveData.observeSticky(LifecycleOwner, Observer {

        })

        // Receive sticky messages
        // An Observer callback is received after onCreate and before onDestroy
        eventLiveData.observeSticky(LifecycleOwner, Observer {

        }, false)
Copy the code

Not bound to the lifecycle:

        // Do not receive sticky messages
        eventLiveData.observeForever(Observer {

        })

        // Receive sticky messages
        eventLiveData.observeForeverSticky(Observer {

        })
Copy the code

Introduce dependencies

EventLiveData is hosted to the JITpack and can be directly relied on remotely. GitHub address: github.com/leavesC/Eve…

	allprojects {
		repositories {
			maven { url 'https://jitpack.io' }
		}
	}
	
	dependencies {
	    implementation 'com. Making. LeavesC: EventLiveData: 1.0.0'
	}
Copy the code

Seven, the implementation principle

EventLiveData is based on the source code to transform the implementation of LiveData, after understanding the design concept and implementation principle of LiveData after the customization is actually very simple, here is a brief introduction to my implementation ideas

LiveData contains an internal mVersion variable to mark the version of the current value, that is, how old or new the value is. When a new value is passed externally (either setValue or postValue), the mVersion is incremented by +1

    @MainThread
    private fun setValue(value: T) {
        assertMainThread(
            "setValue"
        )
        mVersion++
        mData = value
        dispatchingValue(null)
    }
Copy the code

There is also an mLastVersion inside the ObserverWrapper that marks the extent to which the last value called back in the Observer was old and new

	private abstract class ObserverWrapper {
    	
    	// An external Observer that listens to LiveData
        final Observer<? super T> mObserver;
    	
    	// Is used to indicate whether the mObserver is active
        boolean mActive;
    
    	// Used to mark the degree of old and new of the last value called back in the Observer
        int mLastVersion = START_VERSION;

        ObserverWrapper(Observer<? superT> observer) { mObserver = observer; }}Copy the code

While the notice method considers the size of mLastVersion to be considered as a callback to the Observer, let’s consider the initial value of mLastVersion in the Observer and avoid notifying the Observer of the old value.

   private void considerNotify(ObserverWrapper observer) {...if (observer.mLastVersion >= mVersion) {
            return;
        }
        observer.mLastVersion = mVersion;
        observer.mObserver.onChanged((T) mData);
    }
Copy the code

Then, LifecycleBoundObserver’s shouldBeActive() method restricts call-back to only when Lifecycle’s current state is STARTED or RESUMED, so we can just change that, You can increase the effective life cycle range of the Observer

	class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
        @NonNull
        final LifecycleOwner mOwner;

        LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) {
            super(observer);
            mOwner = owner;
        }

        @Override
        boolean shouldBeActive(a) {
            Lifecycle can only happen if the current state of Lifecycle is STARTED or RESUMED
            // Lifecycle is considered to be active
            returnmOwner.getLifecycle().getCurrentState().isAtLeast(STARTED); }}Copy the code

Viii. Reference materials

  • Developer. The android. Google. Cn/jetpack/GUI…