Using LiveData to implement EventBus

Keywords: Android Fragment github less

Introduction

This article is a study note written after learning the article of the big guy and doing it yourself. The article of the big guy is well written. I can't describe it clearly by myself, so many parts of this article directly quote the article of the big guy.

Source code: https://github.com/LinYaoTian...

Design sketch:

Tip: it's best to have a preliminary understanding of Jetpack's LIfeCycle and LiveData.

Quote:

Basic concepts

For Android system, message passing is the most basic component. Different pages and components in each App are carrying out message passing.

Message passing can be used not only for the communication before the four Android components, but also for the communication before the asynchronous thread and the main thread.

For Android developers, there are many ways of message passing, from the earliest Handler, BroadcastReceiver, interface callback, to the popular communication bus framework EventBus and RxBus in recent years.

EventBus

When it comes to Android's communication bus framework, we have to mention EventBus.

EventBus is an Android event publish / subscribe framework, which simplifies Android event delivery by decoupling publishers and subscribers. EventBus can replace Android's traditional Intent, Handler, Broadcast or interface callback to transfer data and execute methods among Fragment, Activity and Service threads.

The most important feature of EventBus is simplicity and decoupling.

Before there is no EventBus, we usually use broadcast to monitor, or customize interface function callback. In some scenarios, we can also directly use Intent to carry simple data, or handle message passing between threads through Handler. But neither broadcasting nor Handler mechanism can satisfy our efficient development. EventBus simplifies the communication among the components in the application and between components and background threads. Once EventBus is launched, it is highly praised by developers.

Now it seems that EventBus has brought a new framework and idea to the Android Developer world, that is, news publishing and subscription This idea has been applied in many frameworks.

Subscription / publishing mode

Subscription publishing mode defines a "one to many" dependency, which enables multiple subscriber objects to listen to a topic object at the same time. When the subject object changes its state, it will notify all subscriber objects so that they can automatically update their state.

The subscription / publishing mode is very similar to the observer mode. I think the subscription / publishing mode is an enhanced version of the observer mode.

The differences between the two are as follows:

  • In the observer mode, there is a direct dependence between the observer and the observed.
  • In the subscription / publishing mode, there is an additional scheduling center before the subscriber and publisher, which avoids the dependency between the subscriber and publisher.

See this article for details: Observer mode vs publish subscribe mode

LiveData

In this paper, LiveData is used to implement EventBus. First, LiveData is introduced.

LiveData, like its name, is an observable data holder. Unlike conventional observable, LiveData can have life cycle awareness, which means it can correctly handle life cycle in Activity, Fragment and Service.

In fact, LiveData can't work alone. It depends on the life cycle component of Jectpack. Because life cycle has been encapsulated in the parent class of Activity, it's almost imperceptible in the process of using LiveData, so here's a brief introduction to life cycle.

The role of life cycle

  • Manage the lifecycle of components.
  • The third-party business can get the life cycle of the dependent components within itself, which is convenient to stop in time and avoid missing the execution time.

Students who want to know more can read these two articles:

Let's move on to LiveData:

The data source of LiveData is generally ViewModel, or other LiveData components that can be updated.

In general, we use LiveData's observe(). When the data is updated, LiveData will notify all its active observers. Unlike RxJava, LiveData only notifies active observers. For example, Activity is inactive when it is in the Destroyed state, so it will not be notified.

Of course, we can also use the observerForever() method of LiveData to subscribe. The difference is that observerForever() will not be affected by the life cycle of activities and other components, and as long as the data is updated, it will be notified.

Simple use of LiveData:

public class MainActivity extends AppCompatActivity {
   private static final String TAG="MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
          // Create LiveData object
        MutableLiveData<String> mutableLiveData  = new MutableLiveData<>();
        // Start subscribing
          mutableLiveData.observe(this, new Observer<String>() {
            @Override
            public void onChanged(@Nullable final String s) {
                Log.d(TAG, "onChanged:"+s);
            }
        });
          // Update the data, and finally call back the onChange() method above
        mutableLiveData.postValue("Android Advanced Trilogy");
    }
}

More details on LiveData can be found in this article Android Jetpack architecture components (IV) article takes you to understand livedata (use article)

What are the advantages of implementing with LiveData?

EventBus is a well-known communication bus Library in the industry, but it also has many shortcomings criticized by people:

  1. Manual registration and deregistration are required. A little carelessness may cause memory leakage.
  2. It is difficult to trace the event source of an error when using EventBus.
  3. Each event needs to define an event class, which is easy to cause class inflation.

The LiveDataBus implemented by LiveData has the following advantages:

  1. Life cycle awareness, no manual registration and anti registration.
  2. Has a unique trusted event source.
  3. Distinguish each event with a string to avoid class expansion.
  4. LiveData is the official Android library, which is more reliable.

principle

Composition of LiveDataBus

  • Message: the message can be any Object, and different types of messages can be defined, such as Boolean and String. You can also define messages of a custom type.
  • Message channel: LiveData plays the role of message channel. Different message channels are distinguished by different names. The name is of String type. You can get a LiveData message channel by name.
  • Message bus: message bus is implemented by a single example, and different message channels are stored in a HashMap.
  • Subscriber: the subscriber obtains the message channel through getChannel(), then calls observe() to subscribe to the message of this channel.
  • Release: the publisher gets the message channel through getChannel(), then calls setValue() or postValue() to publish the message.

Schematic diagram of LiveDataBus

Simple implementation

With LiveData, we can simply implement an event publish / subscribe framework:

public final class LiveDataBus {

    private final Map<String, MutableLiveData<Object>> bus;

    private LiveDataBus() {
        bus = new HashMap<>();
    }

    private static class SingletonHolder {
        private static final LiveDataBus DATA_BUS = new LiveDataBus();
    }

    public static LiveDataBus get() {
        return SingletonHolder.DATA_BUS;
    }

    public <T> MutableLiveData<T> getChannel(String target, Class<T> type) {
        if (!bus.containsKey(target)) {
            bus.put(target, new MutableLiveData<>());
        }
        return (MutableLiveData<T>) bus.get(target);
    }

    public MutableLiveData<Object> getChannel(String target) {
        return getChannel(target, Object.class);
    }
}

Yes, in just 27 lines of code, we have implemented an event publish / subscribe framework!

problem

LiveData is easy to use for a while. After that, we find that there is a problem with this simple LiveDataBus, that is, subscribers will receive messages published before subscription, similar to sticky messages. For a message bus, both sticky and non sticky messages must be supported. Let's take a look at how to solve this problem.

Let's first look at why this problem occurs:

Subscription method of LiveData

android.arch.lifecycle.LiveData

@MainThread
   public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
       assertMainThread("observe");
       // The current subscription request will be ignored when the current bound component (activity or fragment) status is deployed
       if (owner.getLifecycle().getCurrentState() == DESTROYED) {
           return;
       }
       // Turn to observer packaging with life cycle awareness
       LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
       ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
       // The corresponding observer can only be bound to one owner
       if (existing != null && !existing.isAttachedTo(owner)) {
           throw new IllegalArgumentException("Cannot add the same observer"
                   + " with different lifecycles");
       }
       if (existing != null) {
           return;
       }
       // lifecycle registration
       owner.getLifecycle().addObserver(wrapper);
   }

As you can see, LiveData will wrap our incoming parameters as wrapper s, then store them in a Map, and finally add observers through the LifeCycle component.

Update data method of LiveData

There are two methods for LiveData to update data. One is setValue() and the other is postValue(). The difference between the two methods is that postValue() is internally thrown to the main thread to perform the update data, so it is suitable for use in the sub thread; while setValue() is to update the data directly.

Just look at the setValue() method

android.arch.lifecycle.LiveData

@MainThread
protected void setValue(T value) {
    assertMainThread("setValue");
    // Send version + 1
    mVersion++;
    mData = value;
    // Information dissemination
    dispatchingValue(null);
}

Remember the mVersion here. It is the key to this problem. Every time you update the data, it will automatically increase. The default value is - 1. Then we follow up the dispatchingValue() method:

android.arch.lifecycle.LiveData

void dispatchingValue(@Nullable ObserverWrapper initiator) {
        // The judgment of mDispatchingValue is mainly to solve the problem of calling dispatchingValue concurrently
        // When the corresponding data observer is in the process of execution, if there is a new data change, the observer will not be notified again
        // So execution within the observer should not be time consuming
        if (mDispatchingValue) {
            mDispatchInvalidated = true;
            return;
        }
        mDispatchingValue = true;
        do {
            mDispatchInvalidated = false;
            if (initiator != null) {
                  // Here
                considerNotify(initiator);
                initiator = null;
            } else {
                for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
                        mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
                      // Here
                    considerNotify(iterator.next().getValue());
                    if (mDispatchInvalidated) {
                        break;
                    }
                }
            }
        } while (mDispatchInvalidated);
        mDispatchingValue = false;
    }

As you can see, regardless of the condition judgment, the considerNotify() method will be executed eventually, so we will continue to follow up:

android.arch.lifecycle.LiveData

private void considerNotify(ObserverWrapper observer) {
        if (!observer.mActive) {
            return;
        }
        if (!observer.shouldBeActive()) {
            observer.activeStateChanged(false);
            return;
        }
              // Judge version
        if (observer.mLastVersion >= mVersion) {
            return;
        }
        observer.mLastVersion = mVersion;
        //noinspection unchecked
        observer.mObserver.onChanged((T) mData);
    }

Finally, it's the most critical time!! If the mLastVersion of the ObserverWrapper is less than the mVersion of LiveData, the onChange() method will be executed to inform the observer that the data has been updated.

The default value of ObserverWrapper.mLastVersion is - 1. As long as LiveData has updated the data, mVersion will definitely be greater than - 1, so subscribers will immediately receive the latest messages published before subscription!!

Final realization

If we understand the root cause of the problem, we will be able to solve it.

There are many ways to solve this problem, among which the American League leaders The evolution of Android message bus: replacing RxBus and EventBus with LiveDataBus Use reflection to modify the mVersion value in LiveData to implement. There's another option Implementation of event bus based on LiveData , this scheme is based on the custom Observer wrapper class, because the sticky message will eventually call the Observer ා onChange() method, so we define the Observer wrapper class, maintain the actual number of subscription messages ourselves, and determine whether the real onChange() method needs to be triggered. The second scheme is used in this paper.

Step 1: customize the Observer wrapper class

For business expansion, here we define a Base base class:

internal open class BaseBusObserverWrapper<T>(private val mObserver: Observer<in T>, private val mBusLiveData: BusLiveData<T>) : Observer<T> {

    private val mLastVersion = mBusLiveData.version

    private val TAG = "BaseBusObserverWrapper"

    override fun onChanged(t: T?) {
        Logger.d(TAG,"msg receiver = " + t.toString())
        if (mLastVersion >= mBusLiveData.version){
            // The version number of LiveData has not been updated, which means there is no new data, just because
            // onChange() is called because the version number of the current Observer is lower than LiveData
            return
        }
        try {
            mObserver.onChanged(t)
        }catch (e:Exception){
            Logger.e(TAG,"error on Observer onChanged() = " + e.message)
        }
    }

    open fun isAttachedTo(owner: LifecycleOwner) = false

}

Here we save the mVersion value of LiveData. Each time we execute onChange(), we first determine whether some LiveData has updated the data. If not, we do not execute the observer's Observer.onChange() method.

Then two subclasses are defined, BusLifecycleObserver and BusAlwaysActiveObserver. BusLifecycleObserver is used for the events subscribed by livedata? Observer() method, and BusAlwaysActiveObserver is used for the events subscribed by livedata? Observerforever() method.

internal class BusLifecycleObserver<T>(private val observer: Observer<in T>, private val owner: LifecycleOwner, private val liveData: BusLiveData<T>)
    : BaseBusObserverWrapper<T>(observer,liveData),LifecycleObserver{

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun onDestroy(){
        liveData.removeObserver(observer)
        owner.lifecycle.removeObserver(this)
    }
}

For buslifecycle observer, when the lifecycle component is in Destroyed, the observer needs to be removed.

internal class BusAlwaysActiveObserver<T>(private val mObserver: Observer<in T>, private val mBusLiveData: BusLiveData<T>)
    : BaseBusObserverWrapper<T>(mObserver, mBusLiveData)

For busalwaysactivetobserver, observers are not affected by the component life cycle, so they do not need to be removed when the component is Destroyed.

Step 2: customize the LiveData class

Note: because getVersion() of LiveData is package access level! Therefore, BusLiveData must be defined in the same package as LiveData, that is, Android x.lifecycle package. Therefore, you need to create a package with the same name and put BusLiveData in it.

If Android x is not introduced into your project, you can also use android.arch.lifecycle under the v7 package. The same method can be used. However, it should be noted that the generics of LiveData's method parameters under android.arch.lifecycle package do not change. Therefore, copying the code directly here will cause some problems. You need to modify it according to the prompts of the compiler.

class BusLiveData<T>(private val mKey:String) : MutableLiveData<T>() {

    private val TAG = "BusLiveData"

    private val mObserverMap: MutableMap<Observer<in T>, BaseBusObserverWrapper<T>> = mutableMapOf()

    @MainThread
    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        val exist = mObserverMap.getOrPut(observer,{
            BusLifecycleObserver(observer,owner,this).apply {
                mObserverMap[observer] = this
                owner.lifecycle.addObserver(this)
            }
        })
        super.observe(owner, exist)
        Logger.d(TAG,"observe() called with: owner = [$owner], observer = [$observer]")
    }

    @MainThread
    override fun observeForever(observer: Observer<in T>) {
        super.observeForever(observer)
        val exist = mObserverMap.getOrPut(observer ,{
            BusAlwaysActiveObserver(observer,this).apply {
                mObserverMap[observer] = this
            }
        })
        super.observeForever(exist)
        Logger.d(TAG, "observeForever() called with: observer = [$observer]")
    }

    @MainThread
    fun observeSticky(owner: LifecycleOwner, observer: Observer<T>) {
        super.observe(owner, observer)
        Logger.d(TAG, "observeSticky() called with: owner = [$owner], observer = [$observer]")
    }

    @MainThread
    fun observeStickyForever(observer: Observer<T>){
        super.observeForever(observer)
        Logger.d(TAG, "observeStickyForever() called with: observer = [$observer]")
    }

    @MainThread
    override fun removeObserver(observer: Observer<in T>) {
        val exist = mObserverMap.remove(observer) ?: observer
        super.removeObserver(exist)
        Logger.d(TAG, "removeObserver() called with: observer = [$observer]")
    }

    @MainThread
    override fun removeObservers(owner: LifecycleOwner) {
        mObserverMap.iterator().forEach {
            if (it.value.isAttachedTo(owner)) {
                mObserverMap.remove(it.key)
            }
        }
        super.removeObservers(owner)
        Logger.d(TAG, "removeObservers() called with: owner = [$owner]")
    }

    @MainThread
    override fun onInactive() {
        super.onInactive()
        if (!hasObservers()) {
            // When LiveData does not have an active observer, you can remove the relevant instance
            LiveDataBusCore.getInstance().mBusMap.remove(mKey)
        }
        Logger.d(TAG, "onInactive() called")
    }

    @MainThread
    public override fun getVersion(): Int {
        return super.getVersion()
    }


}

The code is relatively short and simple. The main point is that for non sticky messages, that is, observer() and observer forever (), we need to use a custom wrapper class to wrap them. For sticky messages, you can simply use the LiveData default implementation.

Step 3: define management class

internal class LiveDataBusCore {

    companion object{

        @JvmStatic
        private val defaultBus = LiveDataBusCore()

        @JvmStatic
        fun getInstance() = defaultBus
    }

    internal val mBusMap : MutableMap<String, BusLiveData<*>> by lazy {
        mutableMapOf<String, BusLiveData<*>>()
    }

    fun <T> getChannel(key: String) : BusLiveData<T> {
        return mBusMap.getOrPut(key){
            BusLiveData<T>(key)
        } as BusLiveData<T>
    }
}

Step 4: define the entry class

class LiveDataBus {

    companion object{

        @JvmStatic
        @Synchronized
        fun <T> getSyn(key: String) : BusLiveData<T>{
            return get(key)
        }
      
        @JvmStatic
        fun <T> get(key: String) : BusLiveData<T>{
            return LiveDataBusCore.getInstance().getChannel(key)
        }
      
        private fun <T> get(key: String, type: Class<T>) : BusLiveData<T> {
            return LiveDataBusCore.getInstance().getChannel(key)
        }

        @JvmStatic
        fun <E> of(clz: Class<E>): E {
            require(clz.isInterface) { "API declarations must be interfaces." }
            require(clz.interfaces.isEmpty()) { "API interfaces must not extend other interfaces." }
            return Proxy.newProxyInstance(clz.classLoader, arrayOf(clz), InvocationHandler { _, method, _->
                return@InvocationHandler get(
                        // Event name is defined by collection class name event method name
                        // To ensure the uniqueness of the event
                        "${clz.canonicalName}_${method.name}",
                        (method.genericReturnType as ParameterizedType).actualTypeArguments[0].javaClass)
            }) as E
        }
    }


}

Use:

class TestLiveDataBusActivity : AppCompatActivity() {
  
    companion object{
        private const val TAG = "TestLiveDataBusActivity"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test_live_data_bus)

        LiveDataBus.get<String>("testObserver").observe(this, Observer<String> {
            Log.d(TAG, "testObserver = $it")
            test_observer_id.text = it
        })
          
          LiveDataBus.get<String>("testObserver").postValue("new value")
    }
}

LiveDataBus also provides an of() method, which is used for event constraints. What is a time constraint? That is to say, the key for our observers and publishers to obtain the message channel is a string, which may appear in the process of use. The key for publishers is itO and the key for observers is mistyped into it0, so here we can imitate the practice of Retrofit requesting dynamic agent. In the process of use, we need to define an interface first:

interface TestLiveEvents {
    fun event1(): MutableLiveData<String>
}

Use:

fun main() {
    LiveDataBus
            .of(TestLiveEvents::class.java)
            .event1()
            .postValue("new value")
}

summary

With the LiveData provided by Android, we can easily implement our own LiveDataBus. All the files only have the above classes. At the same time, we also avoid many disadvantages of EventBus!

Source code of LiveDataBus: https://github.com/LinYaoTian...

If you have any questions, please correct them in the comment area.

Posted by wgh on Fri, 15 Nov 2019 02:14:53 -0800