LiveDataBus is a cliche, but today we’re doing something something different. Nonsense not much to say, first address: github.com/cyixlq/Live…

Let’s start with some of LiveDataBus’s well-worn advantages:

  • There is no need to register unregistered as EventBus does, and it can be automatically registered and unregistered, avoiding memory leaks caused by forgetting to register unregistered
  • Event sending is not done by reflection, but now EventBus is done by APT
  • Anything else? I haven’t thought of that yet.

Next, let’s take a look at some of the first issues to be overcome in making LiveData an event bus type framework:

  • When the component changes from inactive to active, the value from Observe is sent. (How to put this problem, you say it is a problem, but some business scenarios do need (sticky mode, but does not need to send every time after onStop recovery), you say it is not a problem, but most of the scenarios we really only need data after the subscription event.
  • LiveData data loss problem. How does LiveData determine if a component is active? You can do it in codemOwner.getLifecycle().getCurrentState().isAtLeast(STARTED)Yeah, at least if it’s STARTED, it’s active. What life cycle callbacks are performed to qualify as STARTED? We can see by looking at the getStateAfter method of the LifecycleRegistry class that both onStart and onPause are STARTED (I won’t post the code here for my typography purposes, but you can check it out for yourself). Therefore, the component receives no data from onCreate/onStop, let alone onDestroy. 😁 but this is not a problem because LiveData thinks it is pointless to update the interface when you can’t see it, and LiveData itself is not designed to transmit events, it is designed to update the UI, you have to force it to be an event bus frameworkGoogleWhat can we do about it?Google heart OS: you this is not strong man lock male?. If you post a value several times in a short period of time, only the latest value will be sent.
  • LiveData’s sticky events are a bit illogical. It’s not really… (Stop, stop, I get it, it’s not a problem.)

Let’s take a look at how we overcame these problems in the past to make LiveData a simple LiveDataBus.

  • When observing LiveData, reflection modifies the value of mLastVersion in ObserverWrapper to match the value of mVersion in LiveData, so that when the value is distributed during a life-cycle state change, It is not assumed that LiveData needs to be updated because the version at subscription time is smaller than the version in LiveData. (Then some students will say: ah ah ~, then you use reflection will affect my response time of 6 and 7s APP running speed ah, after all, everyone said that reflection performance is very low! | don’t worry, we then look down)
  • Other problems to solve and expand the corresponding implementation is a bit tricky, because we can not modify the source of LiveData, so we have today’s article. My backhand is just copying code, one Ctrl + C and one Ctrl + V. Google, your code is mine! Heh heh, surprise!

Ok, now it’s time for us to show off. In fact, LiveData is the most critical two classes, which gives us a good opportunity to show (copy) body (generation) hand (code). The two classes are LiveData and SafeIterableMap.

There’s nothing wrong with copying LiveData, but what is this SafeIterableMap thing? This is a Map modeled by the Google team using the linked list data structure, which can safely remove deleted elements while traversing. All subscribers of this LiveData are stored in this Map. Interested partners can click me to view a principle of its implementation analysis.

So why do I have to copy this? Can’t I have an import? Not bad, but this class has added a @restrictto (restrictt.scope.library_group_prefix) annotation, which I’ll check out. This class can only be used in classes with the same package name prefix. If you want to force it, you can also import it directly, even though it will make the code red, but it will compile properly. You can also add the annotation @suppressLint (“RestrictedApi”) on the corresponding member variables or methods. But isn’t it tiring? Wouldn’t it be nice if I Ctrl+ C and Ctrl+ V and modify it to remove this restriction? It is also possible that Google will impose other restrictions on this annotation in the future, such as direct crashes to restrict calls, or failure to compile.

At this point, we can start customizing our LiveDataBus, but in order to make this Google thing ours completely, we’ll just change the name, LiveData to LiveEvent, and we’ll call the event Bus LiveEventBus. Ps: I really shouldn’t call it LiveData, because we customize it a lot

  1. First copy the SafeIterableMap source code into the project and remove it@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX), it is also ok not to go, because the LiveEvent class and the class package name prefix must be the same, but in order to facilitate external use, remove the smell?
  2. Copy the LiveData source code into the project, rename it LiveEvent, and change the constructor name. ArchTaskExecutor and GenericLifecycleObserver classes are also annotated with restricted annotations, so change the corresponding code to the corresponding code in this class:
    Class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver // 1. GenericLifecycleObserver found nothing, directly inherits LifecycleEventObserver, Class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver // 2. Source code if (! ArchTaskExecutor.getInstance().isMainThread()) // 2. If (looper.getMainLooper ().getThread()! = Thread.currentThread())Copy the code
  3. Change the postValue method to send only the latest postValue multiple times at the same time. Change the modifier to public. We can find out from the source code that postValue posts the task to the main thread through the Handler and then we can call setValue, so we can call setValue every time we execute postValue to determine if the current calling thread is the main thread, and if it is the main thread, we can call setValue directly, Otherwise use Handler post to the main thread to execute setValue. While removing useless variables:mDataLock.mPendingData.mPostValueRunnable. Add a member variable Handler to switch to the main thread. To follow through, and to be more semantically appropriate, we can rename the postValue method postEvent:
    Private final Handler mHandler = new Handler(looper.getMainLooper ()); Public void postEvent(T value) {if (looper.getMainLooper ().getThread() == thread.currentThread ()); setValue(value); else mHandler.post(() -> setValue(value)); }Copy the code
  4. Change the setValue method to receive a value only between onStart and onPause life cycles. Simply extend the constraint mentioned above to the CREATED scope (between onCreate and onStop), which also eliminates the problem of re-sending an event when the Activity resumes after onStop. Modify the shouldBeActive method in the LifecycleBoundObserver class as follows. In addition, the setValue method is not used externally. It can be called indirectly through postEvent, so it can be modified to be private.
    @Override
    boolean shouldBeActive() {
        return mOwner.getLifecycle().getCurrentState().isAtLeast(CREATED);
    }
    Copy the code
  5. In order to solve the problem that the component will send the previous value of observe when switching from inactive state to active state, and in order to expand the need for this requirement, let’s directly create a mode that can control whether sticky is required. We add the isStickyMode member variable to ObserverWrapper and add the constructor to it as follows:
    final boolean isStickyMode; ObserverWrapper(Observer<? super T> observer, final boolean isStickyMode) { mObserver = observer; this.isStickyMode = isStickyMode; } // AlwaysActiveObserver(Observer<? super T> observer, boolean isStickyMode) { super(observer, isStickyMode); } LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer, boolean isStickyMode) { super(observer, isStickyMode); mOwner = owner; }Copy the code

    In addition, an observeSticky method needs to be added to indicate that the observeSticky method is observing in sticky mode, and the observe method needs to be modified to indicate that the observeSticky method is not observing in sticky mode. ObserveForever is doing the same. The code is as follows:

    @MainThread public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) { assertMainThread("observe"); realObserve(owner, observer, false); } @MainThread public void observeSticky(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) { assertMainThread("observeSticky"); realObserve(owner, observer, true); } @MainThread private void realObserve(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer, boolean isStickyMode) { if (owner.getLifecycle().getCurrentState() == DESTROYED) { // ignore return; } LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer, isStickyMode); ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper); if (existing ! = null && ! existing.isAttachedTo(owner)) { throw new IllegalArgumentException("Cannot add the same observer" + " with different lifecycles"); } if (existing ! = null) { return; } owner.getLifecycle().addObserver(wrapper); } @MainThread public void observeForever(@NonNull Observer<? super T> observer) { assertMainThread("observeForever"); realObserveForever(observer, false); } @MainThread public void observeForeverSticky(@NonNull Observer<? super T> observer) { assertMainThread("observeForeverSticky"); realObserveForever(observer, true); } @MainThread private void realObserveForever(@NonNull Observer<? super T> observer, boolean isStickyMode) { AlwaysActiveObserver wrapper = new AlwaysActiveObserver(observer, isStickyMode); ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper); if (existing ! = null && existing instanceof LiveEvent.LifecycleBoundObserver) { throw new IllegalArgumentException("Cannot add the same observer" + " with different lifecycles"); } if (existing ! = null) { return; } wrapper.activeStateChanged(true); }Copy the code
  6. With the isStickyMode variable, the version comparison is not necessary. The original LiveData logic was to call the dispatchingValue method when the component went from inactive to active. The dispatchingValue method calls duties Notify, which compares the Observer and LiveData versions, If the mLastVersion in the Observer is less than the mVersion in LiveData, the Observer has not received the latest data, and a distribution is performed. Now we have the isStickyMode variable control, and the active state ranges from onCreate to onStop (the only life cycle outside this range is onDestroy, but LiveData will automatically remove the Observer at this point). Version comparisons do not help, and can be used to simplify variables and methods (it also prevents exceptions if you send too many events and mVersion exceeds the maximum value of int, although it is unlikely that your App has sent over 2 billion events and users have not shut your App down). Notice the code logic that determines whether the version ignores the distribution can be changed to whether value is assigned to evaluate the distribution as follows:
    If (mData == NOT_SET) {if (mData == NOT_SET) {return; }Copy the code
  7. Finally, we write a utility class that wraps the usage methods:
    public class LiveEventBus { private static final class SingleHolder { private static final LiveEventBus INSTANCE = new LiveEventBus(); } public static LiveEventBus get() { return SingleHolder.INSTANCE; } private final ConcurrentHashMap<Object, LiveEvent<Object>> mEventMap; private LiveEventBus() { mEventMap = new ConcurrentHashMap<>(); } public <T> LiveEvent<T> with(@NonNull final String key, @NonNull final Class<T> clazz) { return realWith(key, clazz); } public <T> LiveEvent<T> with(@NonNull final Class<T> clazz) { return realWith(null, clazz); } @SuppressWarnings("unchecked") private <T> LiveEvent<T> realWith(final String key, final Class<T> clazz) { final Object objectKey; if (key ! = null) { objectKey = key; } else if (clazz ! = null) { objectKey = clazz; } else { throw new IllegalArgumentException("key and clazz, one of which must not be null"); } LiveEvent<Object> result = mEventMap.get(objectKey); if (result ! = null) return (LiveEvent<T>) result; synchronized (mEventMap) { result = mEventMap.get(objectKey); if (result == null) { result = new LiveEvent<>(); mEventMap.put(objectKey, result); } } return (LiveEvent<T>) result; }}Copy the code

We ended up using a few hundred lines of code to create a really cool event bus framework that doesn’t reflect and doesn’t affect the speed of an APP that needs six or seven seconds to respond. You can even remove unwanted external methods and member variables from LiveEvent that are no longer used. To further reduce the number of lines of code, you can even remove comments! I did that, and the final LiveEvent was just over 200 lines of code.

Of course, this is not the end of the story, we can further optimize it, for example, every time we create a LiveEvent object we create a Handler, we can share a Handler to post tasks to the main thread, and then we can also extract the thread judgment methods into a common class, So let’s call this class DefaultTaskExecutor! This is already in the AndroidX package, so let’s just copy it and use it as a tool class. And then there’s an inherited parent that also has a restricted annotation, right? Forget it. It’s not like you can’t use it, so just go. The override annotation has also been removed, and there is a method for switching to the IO thread, keep the EMMM, in case we need it later, just change the thread name to the one we want to define. For ease of use, change it to a singleton, and then replace the LiveEvent class where you can use this class method.

Finally, the LiveEventBus usage, which is fairly simple, follows MainActivity and SecondActivity

Declaration: This article may be updated as the project code changes, please refer to the project code shall prevail!

Updated April 16, 2021

  • There are business requirements that do require custom active state situations, such as having to receive events in onResume to ensure the security of View updates. So add observeCustom(LifecycleOwner, Lifecycle.State, Observer) and observeCustomSticky(LifecycleOwner, Lifecycle. The Observer) method
  • Reference to point 6 aboveWith the isStickyMode variable, version comparisons are unnecessary...If the user sets the customized Activity state to RESUMED, onResume will be called back multiple times if an Activity or Fragment goes to the front and back several times. To ensure that events are received only once, version comparisons are necessary to ensure that the Observer does not receive any more of the RESUMED version after it receives the value of the latest version
  • The code has been updated to GitHub, welcome to check it out