The premise

In Android MVVM mode, I used the ViewModel in Jetpack to implement the business layer. Of course, you can also use DataBinding. The choice of Android business layer architecture is explained in more detail in this article: API layer best practices for Android development.

The business layer is nothing more than network requests, storage operations and data processing operations, and then updates the processed data to LiveData, while the UI layer updates automatically. Where network requests I am using coroutines to carry them out, not threads.

The problem

To prevent memory leaks when an asynchronous task is still in progress while the UI is being destroyed, we all cancel the asynchronous task in the onCleared() method. How do I cancel an asynchronous task? Rather than cancel in every ViewModel, we are lazy enough to define a BaseVM class to store every Job object and cancel it. The code is as follows:

open class BaseVM : ViewModel() {val jobs = mutableListOf<Job>()
    override fun onCleared(a) {
        super.onCleared()
        jobs.forEach { it.cancel() }
    }
}
//UserVM
class UserVM : BaseVM() {
    val userData = StateLiveData<UserBean>() 
    fun login(a) {
        jobs.add(GlobalScope.launch {
            userData.postLoading()
            val result = "https://lixiaojun.xin/api/login".http(this).get<HttpResult<UserBean>>().await()
            if(result ! =null && result.succeed) {
                userData.postValueAndSuccess(result.data!!)
            } else {
                userData.postError()
            }
        })
    }
    
    fun register(a){ 
        / /...}}Copy the code

It looks neat and uniform, but it’s not the most elegant, and it has two problems:

  1. We need to manually cancel it. It’s 9102. It shouldn’t be
  2. It’s not flexible enough to foolishly cancel asynchronous tasks for all VMS. If the requirement of one of our asynchronous tasks for one VM is that even UI destruction should be done in the background (such as uploading data in the background), then this doesn’t meet the requirement

The best I can hope for is that we just focus on implementing asynchronous logic that automatically monitors UI destruction to kill itself, giving me more time to play Dota.

Analysis of the

The coroutines we open with GlobalScope do not monitor the UI lifecycle. If the parent ViewModel is responsible for managing and generating coroutines, and the child ViewModel starts coroutines directly from the coroutine objects generated by the parent class, The parent ViewModel removes all coroutines in onCleared, which automatically destroys coroutines.

When I got started, I found that the latest version of Jetpack’s ViewModel module just added this feature. It added an extension property, viewModelScope, to each ViewModel, and the coroutines we opened with this extension property automatically killed themselves when the UI was destroyed.

First, add the dependency, be sure to use androidx version oh:

def lifecycle_version = "2.2.0 - alpha01"
// ViewModel and LiveData
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
Copy the code

Rewrite the code above:

open class BaseVM : ViewModel() {override fun onCleared(a) {
        super.onCleared()
        // The parent class does nothing}}//UserVM
class UserVM : BaseVM() {
    val userData = StateLiveData<UserBean>() 
    fun login(a) {
        viewModelScope.launch {
            userData.postLoading()
            val result = "https://lixiaojun.xin/api/login".http(this).get<HttpResult<UserBean>>().await()
            if(result ! =null && result.succeed) {
                userData.postValueAndSuccess(result.data!!)
            } else {
                userData.postError()
            }
        }
    }
}
Copy the code

This code is elegant enough that you don’t have to care when the UI is destroyed, the coroutine will care, and there will never be any memory leaks. If we want an asynchronous task to be executed when the UI is destroyed, we can start it with GlobalScope.

Principle analysis:

The core code of viewModelScope is as follows:

private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"
val ViewModel.viewModelScope: CoroutineScope
        get() {
            val scope: CoroutineScope? = this.getTag(JOB_KEY)
            if(scope ! =null) {
                return scope
            }
            return setTagIfAbsent(JOB_KEY,
                CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main))
        }

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    override fun close(a) {
        coroutineContext.cancel()
    }
Copy the code

It does a few things:

  1. Added extended properties to ViewModelviewModelScopeThe advantage is that it is more convenient to use.
  2. Then rewriteviewModelScopeProperty, according toJOB_KEYRetrieve the CoroutineScope object, for nowJOB_KEYIt is fixed, and multiple keys may be added later.
  3. Is created if the CoroutineScope object is emptyCloseableCoroutineScopeObject and passsetTagIfAbsentMethod to cache, which is a thread-safe operation according to the method name.
  4. CloseableCoroutineScopeClass is a custom coroutine Scope object that accepts one coroutine object, of which there is only oneclose()Method, in which the elimination coroutine is taken

Then look at the core code of the ViewModel:

public abstract class ViewModel {
    // Can't use ConcurrentHashMap, because it can lose values on old apis (see b/37042460)
    @Nullable
    private final Map<String, Object> mBagOfTags = new HashMap<>();
    private volatile boolean mCleared = false;

    @SuppressWarnings("WeakerAccess")
    protected void onCleared(a) {}@MainThread
    final void clear(a) {
        mCleared = true;
        if(mBagOfTags ! =null) {
            synchronized (mBagOfTags) {
                for (Object value : mBagOfTags.values()) {
                    // see comment for the similar call in setTagIfAbsent
                    closeWithRuntimeException(value);
                }
            }
        }
        onCleared();
    }
    // Thread safe to store coroutine objects
    <T> T setTagIfAbsent(String key, T newValue) {
        T previous;
        synchronized (mBagOfTags) {
            //noinspection unchecked
            previous = (T) mBagOfTags.get(key);
            if (previous == null) {
                mBagOfTags.put(key, newValue);
            }
        }
        T result = previous == null ? newValue : previous;
        if (mCleared) {
            closeWithRuntimeException(result);
        }
        return result;
    }

    /** * Returns the tag associated with this viewmodel and the specified key. */
    @SuppressWarnings("TypeParameterUnusedInFormals")
    <T> T getTag(String key) {
        //noinspection unchecked
        synchronized (mBagOfTags) {
            return(T) mBagOfTags.get(key); }}private static void closeWithRuntimeException(Object obj) {
        if (obj instanceof Closeable) {
            try {
                ((Closeable) obj).close();
            } catch (IOException e) {
                throw newRuntimeException(e); }}}}Copy the code

As we can imagine, the ViewModel does several things:

  1. Provides a Map to store coroutine Scope objects and provides methods for set and GET
  2. inonClearedIterating over all Scope objects, calling their close, cancels the execution of the coroutine

The entire execution process is similar to our previous analysis, by having the parent class manage coroutines and kill them in onCleared.

conclusion

The VM layer can automatically monitor UI destruction naturally. I’ve been looking for a way to gracefully automatically cancel asynchronous tasks, and viewModelScope is by far the best solution.

Some people say I use MVP, not MVVM. There are several ways for the logical layer and UI layer to interact under the MVP architecture:

  1. To decouple, define interface intermodulation, tune to tune to bypass
  2. Sending messages using EventBus can be difficult to manage if the code is large and has hundreds of identifiers
  3. Kotlin’s coroutines and higher-order functions are also perfectly capable of crushing it

If I would have recommended MVP 3 years ago, now, trust me, use MVVM. ViewModel + Kotlin + coroutine is definitely the most advanced, efficient, and elegant stack combination.