series

Back to Jetpack: Dependencies and transitive relationships of Jetpack’s major components

Changes to using activities and fragments under AdroidX

Can you really use a Fragment? Fragments FAQ and new postures for using fragments on androidx

AndroidX Fragment1.2.2 Source code analysis

Fragment returns the stack preparation section

Jetpack’s Fragment return stack is a Fragment return stack demo

Never lose the state of AndroidX SaveState ViewModel-SaveState analysis

Even if you don’t use MVVM, understand the ViewModel — the functional boundaries of the ViewModel

【 Back Jetpack 】 Life will be used without Lifecycle

preface

We discussed the functional boundaries of the ViewModel earlier. Thanks to the longer lifecycle of the ViewModel, we can pass data to the activity after it is rebuilt and avoid memory leaks. But what if instead of getting the data every time we need it, we’re notified every time we have new data?

This article introduces LiveData, a life-cycle aware, observable, data holder. At the same time, it will simply analyze the source code implementation of LiveData

We are all Adapters

Before we talk about LiveData, let’s think about a question

What is the essence of Android development (or front-end development)?

For application layer app developers, the main job of the developer is Adapter

What is an Adapter

Image from Google Image

The essence of what we do is transform data into UI

Data may come from the network, from a local database, from memory, and the UI may be an activity or fragment.

Ideal data model

As mentioned above, the core job of an Android developer is to turn data into a UI. The ideal state for this process is that as the data changes, the UI follows. We can expand further: when the UI is visible to the user, the UI follows the changes as the data changes; When the UI is not visible to the user, we want to do nothing when the data changes, and do UI processing based on the latest data when the UI is visible to the user again.

LiveData is our ideal data model

Image from Android Dev Summit ’18-Fun with LiveData

LiveData can be summarized in three key words

  • lifecycle-aware

  • observable

  • data holder

observable

Different components in Android have different life cycles and different lifetimes

Therefore, we do not hold a reference to the Activity in the ViewModel, as this would cause memory leaks and even null Pointers when the Activity is rebuilt

Normally we hold a reference to the ViewModel in an Activity, so how do we communicate with each other, and how do we send data from the ViewModel to the Activity?

The answer is to have the Activity observe the ViewModel

LiveData is observables

lifecycle-aware

To solve the problem that when an observer observes some data, the data must retain a reference to the observer before it can be invoked, LiveData is designed to be aware of the lifecycle

When an activity/fragment is destroyed, it automatically unsubscribes

data holder

LiveData holds only a single, up-to-date data

In the figure above, the LiveData in the ViewModel is on the far right, and the activity/fragment observing this LiveData is on the left. Once we set a value for LiveData, that value is passed to the activity. In short, the LiveData value changes and the activity receives the latest value change. But when the observer is no longer restarted, data C is not sent to the activity. When the activity returns to the foreground, it will receive the latest value, data D. LiveData holds only a single, up-to-date data. When the activity executes the destruction process, the data E at this time also has no effect

Transformations

LiveData provides two transformations, map and Switch Map. Developers can also create custom MediatorLiveData

We all know that LiveData can provide communication between views and ViewModels, but if you have a third-party component (such as repository) that also holds LiveData. So how should it subscribe in the ViewModel? This component does not lifecycle

As our applications become more complex, Repository may look at data sources

So how does a view retrieve LiveData from a Repository?

One-to-one static transformation (MAP)

In the above example, the ViewModel simply forwards the data from repository to the View and then converts it to the UI Model. Whenever there is new data in the Repository, the ViewModel only needs the Map

class MainViewModel {
  val viewModelResult = Transformations.map(repository.getDataForUser()) { data ->
     convertDataToMainUIModel(data)}}Copy the code

The first parameter is the LiveData source (from repository) and the second parameter is a conversion function.

// Convert X to Y
inline fun <X, Y> LiveData<X>.map(crossinline transform: (X) -> Y): LiveData<Y> =
        Transformations.map(this) { transform(it) }
Copy the code

One-to-one dynamic conversion (switchMap)

Suppose you are looking at a user manager that provides users, and you need to provide user ids to start looking at Repository

You cannot write it to the ViewModel initialization process because the user ID is not available at this point

This is where switchMap comes in handy

class MainViewModel {
  val repositoryResult = Transformations.switchMap(userManager.userId) { userId ->
     repository.getDataForUser(userId)
  }
}
Copy the code

SwitchMap uses MediatorLiveData internally, so it is important to know because you need to use it when you want to combine multiple LiveData sources


        
        
       
inline fun <X, Y> LiveData<X>.switchMap(
    crossinline transform: (X) -> LiveData<Y>
): LiveData<Y> = Transformations.switchMap(this) { transform(it) }
Copy the code

One-to-many dependencies (MediatorLiveData)

MediatorLiveData allows you to add one or more data sources to a single observable LiveData

val liveData1: LiveData<Int> =...val liveData2: LiveData<Int> =...val result = MediatorLiveData<Int>()

result.addSource(liveData1) { value ->
    result.setValue(value)
}
result.addSource(liveData2) { value ->
    result.setValue(value)
}
Copy the code

In the example above, result is updated when either data source changes.

Note: Data is not merged, MediatorLiveData only handles notifications

To implement the transformation in the example, we need to combine two different LiveData into one

LiveData beyond the ViewModel — Reactive patterns using doubling and MediatorLiveData

One way to merge data using MediatorLiveData is to add the source and set the value in another way:

fun blogpostBoilerplateExample(newUser: String): LiveData<UserDataResult> {

    val liveData1 = userOnlineDataSource.getOnlineTime(newUser)
    val liveData2 = userCheckinsDataSource.getCheckins(newUser)

    val result = MediatorLiveData<UserDataResult>()

    result.addSource(liveData1) { value ->
        result.value = combineLatestData(liveData1, liveData2)
    }
    result.addSource(liveData2) { value ->
        result.value = combineLatestData(liveData1, liveData2)
    }
    return result
}
Copy the code

The actual composition of the data is done in the combineLatestData method

private fun combineLatestData(
        onlineTimeResult: LiveData<Long>,
        checkinsResult: LiveData<CheckinsResult>): UserDataResult {

    val onlineTime = onlineTimeResult.value
    val checkins = checkinsResult.value

    // Don't send a success until we have both results
    if (onlineTime == null || checkins == null) {
        return UserDataLoading()
    }

    // TODO: Check for errors and return UserDataError if any.

    return UserDataSuccess(timeOnline = onlineTime, checkins = checkins)
}
Copy the code

Check if values are ready and issue results (loading, failed or succeeded)

Incorrect use of LiveData

Incorrect use of var LiveData

var lateinit randomNumber: LiveData<Int>

fun onGetNumber(a) {
   randomNumber = Transformations.map(numberGenerator.getNumber()) {
       it
   }
}
Copy the code

There is an important point to understand here: the transformation creates a new LiveData when called (map and switchMap). In this example, randomNumber is exposed to the View, but it is reassigned every time the user clicks the button. Observers will only receive updates to the LiveData assigned to the var when they subscribe

// Only the first assigned value will be received
viewmodel.randomNumber.observe(this, Observer { number ->
    numberTv.text = resources.getString(R.string.random_text, number)
})
Copy the code

If the viewModel.randomNumber LiveData instance changes, there is never a callback. Also, previous LiveData is leaked, and these LiveData will no longer send updates

In short, don’t use Livedata in var

See Demo for a correct example

LiveData stickiness events

In general, we use LiveData to hold UI data and state, but there may be some problems if events are sent through it. These problems and solutions are here

The LifecycleOwner was passed in the fragment incorrectly

Androidx Fragment 1.2.0 adds new Lint checking To ensure that you use getViewLifecycleOwner() when viewing LiveData from onCreateView(), onViewCreated(), or onActivityCreated()

As shown, we have a Fragment, onCreate observes LiveData, creates the View through its normal life cycle, and then resumes. Now that you’re using LiveData, the UI will start displaying it. After that, the user clicks on the button and detach another fragment. Once the fragment stops, we won’t need the view in it anymore, so destroyView. After clicking the return button, the user returns to the previous fragment. Since we have destroyView, we need to create a new view and proceed to the normal life cycle, but a bug appears. This new View will not restore the state of LiveData because we are using LiveData from the Lifecycle Observe of the Fragment

We have two choices, onCreate or onCreateView using the Fragment’s Lifecycle Observe LiveData

The former has the advantage of a single registry, while the disadvantage is that there are bugs when making. The latter has the advantage of solving the bugs of recreate, but leads to duplicate registrations

The core of this issue is that a fragment has two life cycles: the fragment itself and the life cycle of the view inside the fragment

Androidx fragment 1.0 and support library 28 have viewLifecycle

Therefore, when viewing view-related LiveData, ViewLifecycleOwner can be passed in the LiveData Observe method in onCreateView(), onViewCreated(), or onActivityCreated() instead of this

Source structure

Let’s start with the main source structure of LiveData

  • LiveData
  • MutableLiveData
  • Observer

LiveData

LiveData is a data holder class that can be observed during a given life cycle. This means that an Observer can be added in pairs with the LifecycleOwner, and that Observer is notified of changes to the wrapper data only when the paired LifecycleOwner is active. If the LifecycleOwner is in a State like life.state. STARTED or life.state. RESUMED, it is considered active. Observers added through observeForever (Observer) are considered to be always active and therefore will always be notified of changes. For those observers, you need to manually call removeObserver (Observer)

If the corresponding life cycle to the Lifecycle State. The DESTROYED State, then add the life cycle of the observer will be automatically deleted. This allows activities and fragments to safely observe LiveData without worrying about leaks

In addition, LiveData has onActive() and onInactive() methods to be notified when the number of active observers changes between 0 and 1. This allows LiveData to release a lot of resources without any active observers.

The main methods are:

  • T getValue() gets the LiveData wrapped data
  • observe(LifecycleOwner owner, Observer
    observer) Sets the observer (main thread call)
  • SetValue (T value) Sets the value (main thread call), visibility is protected and cannot be used directly
  • PostValue (T value) setting (called by other threads), visibility protected cannot be used directly

MutableLiveData

The LiveData implementation class exposes setValue and postValue methods

Observer

Interface, with only the onChanged(T T) method internally, which is called when data changes

Source code analysis

Let’s see how LiveData implements its features through the source code

    1. How to control whether to receive callbacks when an activity or fragment is active or not?
    1. How to automatically unregister an observer when an Activity or fragment is destroyed?
    1. How to ensureLiveDataHave the latest data?

Let’s look at the LiveData observe method

// LiveData.java
@MainThread
public void observe(LifecycleOwner owner, Observer<? super T> observer) {
    if (owner.getLifecycle().getCurrentState() == DESTROYED) {
        // If the owner is in the DESTROYED state, it is ignored
        return;
    }
    // Wrap owner and Observer with LifecycleBoundObserver
    LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
    ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
    // If you have already added a direct return
    if(existing ! =null) {
        return;
    }
   
    owner.getLifecycle().addObserver(wrapper);
}

// LifecycleBoundObserver.java
class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
    @NonNull
    final LifecycleOwner mOwner;
    LifecycleBoundObserver(LifecycleOwner owner, Observer<? super T> observer) {
        super(observer); mOwner = owner; }}Copy the code

As we know from the source code, when we call the Observe method, we internally wrap the owner and Observer through LifecycleBoundObserver and add the observer through the addObserver method, so when the data changes, LifecycleBoundObserver’s onStateChanged method is called

// LiveData.LifecycleBoundObserver.java
@Override
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
    if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
        // The observer is automatically removed, and problem 2 is explained
        removeObserver(mObserver);
        return;
    }
    activeStateChanged(shouldBeActive());
}
Copy the code

Question 2 is explained because the removeObserver method is called when the lifecycle owner is in a DESTROYED state

Moving on, the activeStateChanged method call is passed shouldBeActive()

@Override
boolean shouldBeActive(a) {
    // Returns true for at least the STARTED state
    return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
}

void activeStateChanged(boolean newActive) {
    if (newActive == mActive) {
        // If the value is the same as last time, return (active or inactive)
        return;
    }
    mActive = newActive;
    boolean wasInactive = LiveData.this.mActiveCount == 0;
    // Change the number of active observers according to mActive (+ 1 or + 1)
    LiveData.this.mActiveCount += mActive ? 1 : -1;
    if (wasInactive && mActive) {
        onActive();
    }
    if (LiveData.this.mActiveCount == 0 && !mActive) {
        onInactive();
    }
    if (mActive) {
        // If it is active, data is sent and problem 1 is explained
        dispatchingValue(this); }}Copy the code

Lifecycle State comparison is involved here

Only STARTED and shouldBeActive() return true

The dispatchingValue method is called internally by notice method

private void considerNotify(ObserverWrapper observer) {
    if(! observer.mActive) {return;
    }
    // Determine the lifecycle owner status again
    if(! observer.shouldBeActive()) { observer.activeStateChanged(false);
        return;
    }
    // Compare version numbers
    if (observer.mLastVersion >= mVersion) {
        return;
    }
    observer.mLastVersion = mVersion;
    // Call the onChanged method of the mObserver we passed in
    observer.mObserver.onChanged((T) mData);
}
Copy the code

Notice that the Version number of the Observer is compared, and if it is the latest data, let us return

MVersion changes in the setValue method

@MainThread
protected void setValue(T value) {
    // Set mVersion to ++ each time
    mVersion++;
    mData = value;
    dispatchingValue(null);
}
Copy the code

Therefore, LiveData holds the latest data each time, and question 3 is explained

conclusion

Going back to the thought at the beginning of this article, the main job of Android developers is to convert data into UI, and LiveData is essentially “data-driven”, that is, by changing the state data, the controls in the view tree bound to the corresponding state data are redrawn. Flutter and the future Jetpack Compose use this mechanism. With ViewModel + LiveData, you can safely distribute the correct Data throughout the subscriber’s life cycle, enabling the developer to unknowingly complete a one-way dependency of UI -> ViewModel -> Data.

Architecture is often less about what you can do with it, and more about what you can’t do with it, so that developers are constrained to produce more robust code. Right

If you have any thoughts, please leave them in the comments section

About me

I am a Fly_with24

  • The Denver nuggets
  • Jane’s book
  • Github