[精华] 通过实例理解LiveData

Architecture components is now a big thing for Android developers. LiveData is one of the major component that we want to look in detail here.

In case you are are not familiar with Architecture Component, below might help.Android Architecture Components for Dummies in Kotlin (50 lines of code)Once upon a time, building Android App is as simple as writing all your code in an Activity, and you are done.medium.com

In my blog above, it covers ViewModels and LiveData. But the LiveData is illustrated like just a normal data passing mechanism from ViewModels to the View. I have not done LiveData justice, and now it’s pay back time for it.

So what’s so special about LiveData?

What is Live Data?

Formally it is defined as below

LiveData is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures LiveData only updates app component observers that are in an active lifecycle state

Well, it’s a mouth full of explanation.

To make it simple let me share with you some history to understand the context better, as that helps appreciating it.

In the beginning…

When Android first introduced, most developer started coding in a single activity project.

However, it is not ideal that everything lump into the single God Activity class. Android Activity is hard to be unit tested too.

Due to this, everyone came up with various architecture model like MVP, MVC, MVVM… etc, where the logic side is done by another part e.g. Presenter, ViewModel, Controller etc.

This is better as it separate logic from the view. However it also has it’s problem. The Presenter, ViewModel, Controller etc, are not aware of the Activity Lifecycle. It has to be told of the Activity’s lifecycle.


Google now started to think, instead of letting everyone like wild-wild west defining what to do, it came up with Architecture Components.

The components e.g. ViewModels have a special capability. They are could made aware of the Activity LiveCycle without need the Activity telling them (at least explicitly).

Other than ViewModel, the mean that is used to communicate information back from ViewModel to Activity, the data in another word, also have the ability to be aware of the Activity’s lifecycle.

That’s the reason is is call LiveData, data that is aware of the LiveCycle of the interested observer (e.g. Activity).

Illustrate LiveData by making it the central of universal…

To illustrate better, let’s put LiveData in the center as the below diagram

From the diagram, you could see that the LiveData could be modified by the ViewModels (or whatever you like to use to modify the liveData)…

Upon updated, it would then notify all it’s observers (activities, fragment, service etc). Nonetheless, unlike any other approach (e.g. Rxjava), it doesn’t blindly notify them all, but instead check their live state first.

If the observer is active, then it could be notified of the change of data in the LiveData. However, if the Observer is Paused or Destroyed, it would then not be notified.

This is so cool, as we no longer need to worry about unsubscribing it onPause or onDestroy.

Besides, once the Observer is resumed, it would be immediately notified of the latest data from the LiveData.

Type of LiveData

LiveData is actually just an Abstract Class. So it can’t be used as itself. Fortunately Google has implemented some simple concrete class we could make use of.

MutableLiveData

This is the most simplest LiveData, where it would just get updated and notify it’s observer.

Defining it is as simple as below (2 lines)

// Declaring it
val liveDataA = MutableLiveData<String>()// Trigger the value change
liveDataA.value = someValue// Optionally, one could use liveDataA.postValue(value) 
// to get it set on the UI thread

To observe the LiveData is also simple. I have a fragment that observe the LiveDataA as below

class MutableLiveDataFragment : Fragment() {

    private val changeObserver = Observer<String> { value ->
        value?.let { txt_fragment.text = it }
    }

    override fun onAttach(context: Context?) {
        super.onAttach(context)
        getLiveDataA().observe(this, changeObserver)
    }    // .. some other Fragment specific code ..
}

The result as below, once the data is set on LiveDataA i.e. 7567 and 6269, it is detected by the Fragment

From the code, you could see

getLiveDataA().observe(this, changeObserver)

but there’s no code to unsubscribe it when the Fragment is pausing or terminating.

Even without unsubscribing it, it won’t cause any problem.

Check out the below, even when the Fragment died, the LiveData value generate i.e. 1428 is not causing crashes due to setting on an inactive Fragment.

You would also notice when the Fragment came alive again, it will fetch the latest data i.e. 1428 from the LiveData.

Transformations.Map

Imagine if you are loading a data from a repository. Before you pass to the view, you would like to have it modified first.

We could still use LiveData, to pass the data across various entity as below.

We could transform the data from one LiveData and pass on to the other one using Transformations.map() function.

class TransformationMapFragment : Fragment() {

    private val changeObserver = Observer<String> { value ->
        value?.let { txt_fragment.text = it }
    }

    override fun onAttach(context: Context?) {
        super.onAttach(context)
        val transformedLiveData = Transformations.map(
                getLiveDataA()) { "A:$it" }
        transformedLiveData.observe(this, changeObserver)
    }    // .. some other Fragment specific code ..
}

The behavior as shown below, where the data i.e. 5116 is transformed to a new form i.e. A:5116, before passing over to the Fragment.

Using Transformations.map is helpful to ensure that the LiveData doesn’t pass information over when the destination e.g. ViewModel and View is dead.

This is so cool, as we don’t need to worry about unsubscribing it as well.

Let’s look at underlying how Trasnformation.map is done…

@MainThread
public static <X, Y> LiveData<Y> map(@NonNull LiveData<X> source,
        @NonNull final Function<X, Y> func) {
    final MediatorLiveData<Y> result = new MediatorLiveData<>();
    result.addSource(source, new Observer<X>() {
        @Override
        public void onChanged(@Nullable X x) {
            result.setValue(func.apply(x));
        }
    });
    return result;
}

Oh, it is using another type of LiveData named MediatorLiveData. Let’s dive into it…

MediatorLiveData

If you observe the above code, the main interesting part of MediatorLiveData is the ability to add source to it, and the code that change the content of the data.

This means we could have multiple LiveData feed to one destination through the MediatorLiveData as below.

We could use MediatorLiveData directly as below

class MediatorLiveDataFragment : Fragment() {

    private val changeObserver = Observer<String> { value ->
        value?.let { txt_fragment.text = it }
    }

    override fun onAttach(context: Context?) {
        super.onAttach(context)
        val mediatorLiveData = MediatorLiveData<String>()
        mediatorLiveData.addSource(getliveDataA())
              { mediatorLiveData.value = "A:$it" }
        mediatorLiveData.addSource(getliveDataB()) 
              { mediatorLiveData.value = "B:$it" }
        mediatorLiveData.observe(this, changeObserver)
    }    // .. some other Fragment specific code ..
}

With this, you could see below, the Fragment able to receive from both LiveDataA and LiveDataB as it change.

There’s one caveat though. If the Fragment is not alive, and the data changes both on LiveDataA and LiveDataB, when the Fragment came alive, the MediatorLiveData will take the LiveData that is last added as source, i.e. LiveDataB.

Shown above, you could see that the Fragment upon restored, will always take from the LiveDataB, regardless if LiveDataB has change latter compare to LiveDataA or not. This is because in the code, you could see that LiveDataB is last added source to the MediatorLiveData.

Transformations.SwitchMap

Having ability to listen from 2 sources of LiveData is nice. But what if we want to control to only listen to one, and not the other, and switch between them as needed, should we write our logic do to so?

We can, but we don’t need to. Google has provided us another nice function, Transformations.switchMap().

@MainThread
public static <X, Y> LiveData<Y> switchMap(@NonNull LiveData<X> trigger,
        @NonNull final Function<X, LiveData<Y>> func) {
    final MediatorLiveData<Y> result = new MediatorLiveData<>();
    result.addSource(trigger, new Observer<X>() {
        LiveData<Y> mSource;

        @Override
        public void onChanged(@Nullable X x) {
            LiveData<Y> newLiveData = func.apply(x);
            if (mSource == newLiveData) {
                return;
            }
            if (mSource != null) {
                result.removeSource(mSource);
            }
            mSource = newLiveData;
            if (mSource != null) {
                result.addSource(mSource, new Observer<Y>() {
                    @Override
                    public void onChanged(@Nullable Y y) {
                        result.setValue(y);
                    }
                });
            }
        }
    });
    return result;
}

The function is simply adding source and removing previous source accordingly. So there’s only one source always feed to the MediatorLiveData.

The Switch is also a liveData. So the entire operation looks as below.

To use it, would be simply as below

class TransformationSwitchMapFragment : Fragment() {

    private val changeObserver = Observer<String> { value ->
        value?.let { txt_fragment.text = it }
    }

    override fun onAttach(context: Context?) {
        super.onAttach(context)

        val transformSwitchedLiveData =
            Transformations.switchMap(getLiveDataSwitch()) { 
                switchToB ->
                if (switchToB) {
                     getLiveDataB()
                } else {
                     getLiveDataA()
                }
        }

        transformSwitchedLiveData.observe(this, changeObserver)
    }    // .. some other Fragment specific code ..
}

With this, we could easily control which data would feed to our view as below. The data updated to the Fragment is when the connected data source change, or when the switch changes.

This is very handy to control application that has data feed from different sources controlled by the certain setting (e.g. user login session).

References

The above GIF are all created using an example App I created. You could get the source code from belowelye/demo_android_livedata_illustrationDemo live data and illustrating it. Contain MutableLiveData, MediatorLiveData, Transform.Map and Transform.SwitchMap …github.com

Compile and play around it, to get a good solid idea of how LiveData works.

More example

In case you don’t like the data to be made available to from your LiveData upon subscribing to it, you could refer to the below blogLiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)A convenient way for a view (activity or fragment) to communicate with a ViewModel is to use LiveData observables. The…medium.com


I hope this post is helpful to you. You could check out my other interesting topics here.

Follow me on medium, TwitterorFacebook for little tips and learning on Android, Kotlin etc related topics. ~Elye~

原文链接:https://medium.com/@elye.project/understanding-live-data-made-simple-a820fcd7b4d0

发表评论

邮箱地址不会被公开。 必填项已用*标注