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

For Android Developer today, Google Jetpack is one of the most fundamental architectural components. Since its launch, it has dramatically changed our development model and made it easier to develop, which requires a certain level of understanding of how some of the sub-components work. So I decided to write a series of articles on Jetpack source code parsing, which I hope will help you 🤣🤣

Series article navigation

  • Jetpack (1) – Lifecycle
  • Jetpack (2) -Lifecycle derivatives
  • Jetpack (3) – LiveData source code in detail
  • Jetpack (4) – LiveData derivatives source code in detail
  • Jetpack (5) – Startup
  • Jetpack (6) – ViewModel
  • Jetpack (7) – SavedStateHandle

Last article introduced the source code implementation of LiveData, this article will introduce a series of derived classes and derived methods of LiveData

The source code described in this article is based on the current latest releases of the following dependency libraries:

    implementation "Androidx. Lifecycle: lifecycle - livedata: 2.2.0." "
    implementation "Androidx. Lifecycle: lifecycle - livedata - core: 2.2.0." "
Copy the code

First, subclass LiveData

Let’s start with a few subclasses of LiveData

The setValue() and postValue() methods of LiveData have protected access, so we need to subclass them to update values in our daily development

1, MutableLiveData

The source code for MutableLiveData is simple, simply raising access to the setValue() and postValue() methods to public so that outsiders can call them directly

public class MutableLiveData<T> extends LiveData<T> {

    /**
     * Creates a MutableLiveData initialized with the given {@code value}.
     *
     * @param value initial value
     */
    public MutableLiveData(T value) {
        super(value);
    }

    /** * Creates a MutableLiveData with no value assigned to it. */
    public MutableLiveData(a) {
        super(a); }@Override
    public void postValue(T value) {
        super.postValue(value);
    }

    @Override
    public void setValue(T value) {
        super.setValue(value); }}Copy the code

2, MediatorLiveData

MediatorLiveData is a subclass of MutableLiveData, the source code is relatively simple, the total is less than one hundred lines. MediatorLiveData can be used either to listen on other LiveData as data sources or to use it as plain MutableLiveData

Here’s a simple example of MediatorLiveData. If we have an EditText for the user name and need to display the length of the user name on the interface, we can use MediatorLiveData to convert ** user name (String) ** to the required data type Int. NameLengthLiveData gets notified whenever the data in nameLiveData changes

	/ * * *@Author: leavesC
     * @Date: 2021/03/24 18:04
     * @Desc:
     * @Github: https://github.com/leavesC * /
    private val nameLiveData = MutableLiveData<String>()

    private val nameLengthLiveData = MediatorLiveData<Int> ()// use nameLiveData as the data source
    // nameLengthLiveData is notified whenever nameLiveData's data changes
    nameLengthLiveData.addSource(nameLiveData) { name ->
        nameLengthLiveData.value = name.length
    }
    nameLengthLiveData.observe(this, Observer {
        Log.e("TAG"."name length: $it")})Copy the code

Take a look at its addSource method. The main logic is to wrap the external data source and corresponding data listener onChanged as source objects, and then check whether the source object and onChanged object have been cached inside mSources. Avoid adding data sources and observers repeatedly

    privateSafeIterableMap<LiveData<? >, Source<? >> mSources =new SafeIterableMap<>();
	
    @MainThread
    public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<? super S> onChanged) {
        Source<S> e = newSource<>(source, onChanged); Source<? > existing = mSources.putIfAbsent(source, e);if(existing ! =null&& existing.mObserver ! = onChanged) {// The same source object has been passed in before
        	// However, the Observer passed at that time is not the same as the Observer passed locally
         	// Throw an exception directly
            throw new IllegalArgumentException(
                    "This source was already added with the different observer");
        }
        if(existing ! =null) {
            // The addSource method has been called with the same source and onChanged object
            // So return directly
            return;
        }
        // If MediatorLiveData currently has an active Observer listening on it
        // Call the plug() method of the Source object
        if(hasActiveObservers()) { e.plug(); }}// Remove the listening behavior on the data source
	@MainThread
    public <S> void removeSource(@NonNull LiveData<S> toRemote) { Source<? > source = mSources.remove(toRemote);if(source ! =null) { source.unplug(); }}Copy the code

Note that MediatorLiveData does not allow multiple Observers to be added to the same data source because, for usage purposes, the developer passes the value received from the data source through a series of logical calculations to MediatorLiveData in the Observer. This conversion process only takes place once, so adding multiple observers is moot

Let’s look at the definition of the Source class. The Source itself is also an Observer. It listens for LiveData sent in from outside, and when it receives a value, it directly calls back to the Observer to forward the data

	 private static class Source<V> implements Observer<V> {
        final LiveData<V> mLiveData;
        final Observer<? super V> mObserver;
        int mVersion = START_VERSION;

        Source(LiveData<V> liveData, final Observer<? super V> observer) {
            mLiveData = liveData;
            mObserver = observer;
        }
		
        // Listen on the external data source mLiveData
        void plug(a) {
            mLiveData.observeForever(this);
        }

        // Remove listening on the external data source mLiveData
        void unplug(a) {
            mLiveData.removeObserver(this);
        }

        @Override
        public void onChanged(@Nullable V v) {
            if(mVersion ! = mLiveData.getVersion()) { mVersion = mLiveData.getVersion(); mObserver.onChanged(v); }}}Copy the code

In addition, in order to optimize performance, MediatorLiveData will actively remove the listening behavior of the data source when all external observers remove it. When an Observer starts listening on MediatorLiveData, it is triggered to start listening on the data source

    @CallSuper
    @Override
    protected void onActive(a) {
        for(Map.Entry<LiveData<? >, Source<? >> source : mSources) { source.getValue().plug(); }}@CallSuper
    @Override
    protected void onInactive(a) {
        for(Map.Entry<LiveData<? >, Source<? >> source : mSources) { source.getValue().unplug(); }}Copy the code

This is all MediatorLiveData source code introduction, as long as the first understanding of LiveData internal implementation principle, you can quickly understand MediatorLiveData event callback process. One of the most convenient aspects of MediatorLiveData is that it allows multiple calls to the addSource method to add different data sources. This allows us to aggregate different data channels (e.g. local database cache, network request results, etc.) and distribute them from a single outlet

Second, the Transformations

Lifecycle LiveData is a utility-type method class that provides three static methods to simplify the use of MediatorLiveData

1, the map

The map(LiveData

, Function

) method is used to simplify adding data sources to MediatorLiveData. In most cases, we use MediatorLiveData by first converting data source type X to our target data type Y and then calling back the data using the setValue method. The map method abstracts this data type conversion process into the Function

interface, and hides the setValue process inside the Map method
,>
,>

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

    public interface Function<I.O> {
        /**
        * Applies this function to the given input.
        *
        * @param input the input
        * @return the function result.
        */
        O apply(I input);
    }
Copy the code

Example:

    / * * *@Author: leavesC
     * @Date: 2021/03/24 18:04
     * @Desc:
     * @Github: https://github.com/leavesC * /
    private val nameLiveData = MutableLiveData<String>()

    private val nameLengthLiveData: LiveData<Int> = Transformations.map(nameLiveData) {
        it.length
    }

    // use nameLiveData as the data source
    // nameLengthLiveData is notified whenever nameLiveData's data changes
    nameLengthLiveData.observe(this, {
        Log.e("TAG"."name length: $it")})Copy the code

2, switchMap

The logic of the switchMap method is relatively convoluted and can be useful in cases where some logical computations are delivered via LiveData (for example, the Room database supports return of query results as LiveData). It is easier to understand the role by assuming a realistic requirement

Suppose you are needed to achieve a user name to query all the functionality of the list of users matching, through to the database or network requests such as time consuming way to obtain the matching results, in order to avoid blocking the main thread, need to put child thread to complete the matching process, the main thread by means of a callback to get results

First, suppose there is a UserDataSource that provides the getUsersWithNameLiveData(String) method to request the result of the match and pass the result of the request through LiveData as the return value. The switchMap method also uses MediatorLiveData internally, using nameQueryLiveData as the data source. Whenever the setNameQuery(String) method changes the user name, the switchMap method receives an update notification. The getUsersWithNameLiveData(String) method is then automatically triggered to make the request. In the end, the external can simply listen for the return value of the getUsersWithNameLiveData() method to get the final request result, regardless of what method is used inside the ViewModel to get the result value

    class UserViewModel : ViewModel() {

        val nameQueryLiveData = MutableLiveData<String>()

        lateinit var userDataSource: UserDataSource

        fun getUsersWithNameLiveData(a): LiveData<List<String>> {
            return Transformations.switchMap(nameQueryLiveData) { name ->
                return@switchMap userDataSource.getUsersWithNameLiveData(name)
            }
        }

        fun setNameQuery(name: String) {
            nameQueryLiveData.value = name
        }

    }

    interface UserDataSource {

        fun getUsersWithNameLiveData(name: String): LiveData<List<String>>

    }
Copy the code

After understanding the above requirements, the implementation logic of switchMap will be much simpler. SwitchMap only encapsulates the monitoring behavior of data sources and the transformation process of data. In some special cases (where the results are returned as LiveData) it also saves developers some code

	@MainThread
    @NonNull
    public static <X, Y> LiveData<Y> switchMap(
            @NonNull LiveData<X> source,
            @NonNull final Function<X, LiveData<Y>> switchMapFunction) {
        final MediatorLiveData<Y> result = new MediatorLiveData<>();
        // Build a MediatorLiveData internally and use source as its data source
        result.addSource(source, new Observer<X>() {
            
            // Cache the result of each request
            LiveData<Y> mSource;

            @Override
            public void onChanged(@Nullable X x) {
                // Trigger the logic for the external world to get the result value of LiveData based on the request value x
                // Corresponds to the getUsersWithNameLiveData method in the example above
                // This process is lazy, i.e. the request is triggered only if the data source changes
                LiveData<Y> newLiveData = switchMapFunction.apply(x);
                if (mSource == newLiveData) {
                    // If newLiveData is already available, there is no need to listen for its callback results repeatedly
                    // Just return it
                    return;
                }
                if(mSource ! =null) {
                    // When the new value arrives, remove the listening for the old value
                    result.removeSource(mSource);
                }
                mSource = newLiveData;
                if(mSource ! =null) {
                    result.addSource(mSource, new Observer<Y>() {
                        @Override
                        public void onChanged(@Nullable Y y) {
                            // Wait until you get the result of the request for getUsersWithNameLiveData
                            // call the result back outresult.setValue(y); }}); }}});return result;
    }
Copy the code

3, distinctUntilChanged

The distinctUntilChanged() method is used to filter out consecutive repeated callback values, which are considered valid only if the result of this callback is different from the last one

    @MainThread
    @NonNull
    public static <X> LiveData<X> distinctUntilChanged(@NonNull LiveData<X> source) {
        final MediatorLiveData<X> outputLiveData = new MediatorLiveData<>();
        outputLiveData.addSource(source, new Observer<X>() {
			
            // Is used if the callback value is received for the first time
            boolean mFirstTime = true;

            @Override
            public void onChanged(X currentValue) {
                final X previousValue = outputLiveData.getValue();
                // There are three conditions for this equation to be true
                //1. The callback is received for the first time, i.e. mFirstTime is true
                //2. The last callback value is null, but the current callback value is not null
                //3. The last callback value is not null and is inconsistent with the current callback value
                if (mFirstTime
                        || (previousValue == null&& currentValue ! =null) || (previousValue ! =null && !previousValue.equals(currentValue))) {
                    mFirstTime = false; outputLiveData.setValue(currentValue); }}});return outputLiveData;
    }
Copy the code

Third, ComputableLiveData

ComputableLiveData is a class under the lifecycle- LiveData dependency library. Although named with LiveData, it does not actually inherit directly from any class or interface. ComputableLiveData can be said to provide a way to execute time-consuming tasks more safely, which is characterized by life cycle monitoring, reactive triggering of time-consuming tasks, and obtaining task execution results through LiveData as an intermediary

Let’s take a look at a simple example to understand how to use it. If you need to implement a function to compress the specified image and display the compressed image in ImageView, you need to consider the following points:

  • Compressing images is a time-consuming process that needs to be put into child threads
  • The process of compressing images cannot be triggered multiple times at the same time, so it needs to be atomic
  • A callback is needed to get the compression result on the main thread
  • When the page exits, cancel the callback to avoid memory leaks and NPE problems

Based on the above points, it is very simple to implement computable table Data, which provides guarantees for the above points

By listening on liveData inside ComputableLiveData, it can automatically trigger the thread pool inside ComputableLiveData to execute time-consuming tasks, and finally get the execution results of tasks in the main thread. And because LifecycleOwner objects can be passed in, life cycle safety is guaranteed

class CompressImgLiveData(private val filePath: String) : ComputableLiveData<Bitmap>() {

    override fun compute(a): Bitmap {
        // Perform time-consuming tasks....
        return compress()
    }

    private fun compress(a): Bitmap {
        TODO()
    }

}

 val compressImgLiveData = CompressImgLiveData("sdcard/xxxx.jpg")
 compressImgLiveData.liveData.observe(LifecycleOwner, Observer { bitmap ->
     // Get the result bitmap
 })
Copy the code

It can be seen that the Computable table Data encapsulates most of the processing logic, only one compute() method is exposed to externally implement the execution body of time-consuming tasks, which is very convenient for users

ComputableLiveData has four global variables. In order to ensure that time-consuming tasks can only be executed by one thread at a time, two AtomicBoolean variables are used to mark the execution state of time-consuming tasks, avoiding read/write contention in multi-threaded situations and ensuring atomicity of the compute() method

    // The thread pool used to execute time-consuming tasks
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final Executor mExecutor;

    // Trigger time-consuming tasks and receive the execution results of time-consuming tasks
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final LiveData<T> mLiveData;

    // Is used to mark whether the result value of the current time-consuming task is obsolete, or true if it is
    // Time consuming tasks are rendered obsolete externally by calling invalidate()
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final AtomicBoolean mInvalid = new AtomicBoolean(true);

    // Indicates whether a time-consuming task is being performed
    // In the executing state, the value is true
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final AtomicBoolean mComputing = new AtomicBoolean(false);
Copy the code

ComputableLiveData has two constructors that essentially open the entry point for thread pool objects passed in from outside. MRefreshRunnable is automatically triggered when the number of external observers listening on mLiveData increases from zero to zero

	/** * Creates a computable live data that computes values on the arch IO thread executor. */
    @SuppressWarnings("WeakerAccess")
    public ComputableLiveData(a) {
        // Use the default thread pool provided by Jetpack
        this(ArchTaskExecutor.getIOThreadExecutor());
    }

    /**
     * Creates a computable live data that computes values on the specified executor.
     *
     * @param executor Executor that is used to compute new LiveData values.
     */
    @SuppressWarnings("WeakerAccess")
    public ComputableLiveData(@NonNull Executor executor) {
        // Thread pools can be customized
        mExecutor = executor;
        mLiveData = new LiveData<T>() {
            @Override
            protected void onActive(a) {
                // When the number of external observers listening on LiveData grows from zero
                // The time-consuming task is triggeredmExecutor.execute(mRefreshRunnable); }}; }Copy the code

The execution of time-consuming tasks is placed inside mRefreshRunnable, the execution state of compute() is marked by two AtomicBoolean variables, and the body of the task is placed inside the while loop, which is automatically re-executed when the task becomes obsolete

	@VisibleForTesting
    final Runnable mRefreshRunnable = new Runnable() {
        @WorkerThread
        @Override
        public void run(a) {
            boolean computed;
            do {
                computed = false;
                // compute can happen only in 1 thread but no reason to lock others.

                //1. If mComputing is false, set it to true. Means that the compute() method is not currently being executed, making the equation true inside if
                // This limits time-consuming tasks to one thread at a time
                //2. If mComputing is true, it means that the compute() method is executing and the do while loop is not blocked
                if (mComputing.compareAndSet(false.true)) {
                    // as long as it is invalid, keep computing.
                    try {
                        T value = null;
                        //1. If mInvalid is true, set it to false and the equation is true
                        //2. If mInvalid is false, the equation is invalid and the while loop is broken

                        // Since the operation that sets mInvalid to false only occurs here, the second case only occurs after entering the while loop
                        while (mInvalid.compareAndSet(true.false)) {
                            computed = true;
                            value = compute();
                        }
                        // If computed is true, that means compute() is complete and no exceptions are thrown during execution
                        // The result value is directly called back externally
                        if(computed) { mLiveData.postValue(value); }}finally {
                        // release compute lock
                        // Release the status lock and set the current state to non-computed
                        mComputing.set(false); }}// check invalid after releasing compute lock to avoid the following scenario.
                // Thread A runs compute()
                // Thread A checks invalid, it is false
                // Main thread sets invalid to true
                // Thread B runs, fails to acquire compute lock and skips
                // Thread A releases compute lock
                // We've left invalid in set state. The check below recovers.

                // If both computed and mInvalid are true, the cycle is restarted
                // This means that the compute() method executed successfully, but has been externally rendered obsolete and needs to be re-executed
            } while(computed && mInvalid.get()); }};Copy the code

The invalidate() method can be used to trigger a new execution of a task when the external determination of the result value of the compute() method is invalid. When mInvalidationRunnable is executed, there are two cases of mInvalid:

  1. The value mInvalid is true. If the equation is not true, it means that nowcompute()Compute () has not been executed yet, or has previously been set to obsolete (compute() is automatically reexecuted), return
  2. The value mInvalid is false. MInvalid will be set to true, if the equation holds, and mRefreshRunnable can be executed in several states
    • MRefreshRunnable has been executed. As long as isActive is true, mRefreshRunnable will be triggered again
    • MRefreshRunnable is still in execution, andwhile (mInvalid.compareAndSet(true, false))It is being executed. Changing mInvalid to true causes the while loop to be re-executed, thus refiringcompute()The purpose of
    • MRefreshRunnable is still running,while (mInvalid.compareAndSet(true, false))Executed, but not yetwhile (computed && mInvalid.get())Statements. If mInvalid is changed to true, thenwhile (computed && mInvalid.get())The equation is true, and the while loop is reexecuted, thus refiringcompute()The purpose of
    // invalidation check always happens on the main thread
    @VisibleForTesting
    final Runnable mInvalidationRunnable = new Runnable() {
        @MainThread
        @Override
        public void run(a) {
            // Check whether the external listener is listening on LiveData
            boolean isActive = mLiveData.hasActiveObservers();
            // If mInvalid is false, set it to true
            if (mInvalid.compareAndSet(false.true)) {
                // A time-consuming task is triggered only if LiveData is currently being listened on externally
                if(isActive) { mExecutor.execute(mRefreshRunnable); }}}};/**
     * Invalidates the LiveData.
     * <p>
     * When there are active observers, this will trigger a call to {@link #compute()}.
     */
    // Set the current time-consuming task to obsolete
    // When an external Observer listens on LiveData, the time-consuming task is triggered again
    public void invalidate(a) {
        ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
    }
Copy the code

Compute () is the abstract method that needs to be implemented by subclasses, the body of the task

    // Implement the specific logic of time-consuming tasks externally
    // TODO https://issuetracker.google.com/issues/112197238
    @SuppressWarnings({"WeakerAccess", "UnknownNullness"})
    @WorkerThread
    protected abstract T compute(a);
Copy the code