This is the second day of my participation in the More text Challenge. For more details, see more text Challenge

About Kotlin coroutines, ViewModel related knowledge, in this article will not do the introduction.

To get into this, what is a ViewModelScope?

Add rely on Tips before use ViewModelScope needs: implementation ‘androidx. Lifecycle: lifecycle – viewmodel – KTX: 2.3.0’

packageandroidx.lifecycle ... Ellipsis reversal...private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"

/** * CoroutineScope * [CoroutineScope] tied to this [ViewModel]. * When you call [viewModel.oncleared], * This scope will be canceled when ViewModel will be cleared, i.e [ViewModel.onCleared] is called * * This scope is bound to * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate] */
public val ViewModel.viewModelScope: CoroutineScope
    get() {
    	// First try to get a CoroutineScope object with JOB_KEY tag from the cache
    	// Returns viewModelScope if it hits
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if(scope ! =null) {
            return scope
        }
        // If the cache is not hit, it is added to the ViewModel via setTagIfAbsent()
        return setTagIfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context

    override fun close(a) {
        coroutineContext.cancel()
    }
}
Copy the code

This tells us that the viewModelScope object is an extended property of the ViewModel. And we know from the comments in the viewModelScope that the scope will be cleared when the ViewModel is cleared, which is called [viewModel.onCleared].

Here’s the question:

  1. When is the ViewModel cleared?
  2. Why the viewModelScope is canceled when [viewModel.onCleared] is called?

With the problem to see the source twice the result with half the effort!

Regarding the first question “When is the ViewModel cleared?” This will be covered in a future article on ViewModel

Let’s look at the second question “Why is the viewModelScope cleared when called [viewModel.onCleared]?”

Just look at the onCleared method for the ViewMode.

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;

    /** * This method is called when the ViewModel is no longer used and is destroyed. * This method will be called when this ViewModel is no longer used and will be destroyed. * 

* It is useful to prevent the ViewModel from leaking when the ViewModel observes some data and needs to clear the subscription to the ViewModel. * It is useful when ViewModel observes some data and you need to clear this subscription to * prevent a leak of this ViewModel. */

@SuppressWarnings("WeakerAccess") protected void onCleared() { } @MainThread final void clear() { mCleared = true; if(mBagOfTags ! =null) { synchronized (mBagOfTags) { for (Object value : mBagOfTags.values()) { // see comment for the similar call in setTagIfAbsent closeWithRuntimeException(value); } } } onCleared(); } @SuppressWarnings("unchecked") <T> T setTagIfAbsent(String key, T newValue) { T previous; synchronized (mBagOfTags) { previous = (T) mBagOfTags.get(key); if (previous == null) { mBagOfTags.put(key, newValue); } } T result = previous == null ? newValue : previous; if (mCleared) { closeWithRuntimeException(result); } return result; } @SuppressWarnings({"TypeParameterUnusedInFormals"."unchecked"}) <T> T getTag(String key) { if (mBagOfTags == null) { return null; } synchronized (mBagOfTags) { return (T) mBagOfTags.get(key); }}private static void closeWithRuntimeException(Object obj) { if (obj instanceof Closeable) { try { ((Closeable) obj).close(); } catch (IOException e) { thrownew RuntimeException(e); }}}}Copy the code

The onCleared() method is empty. “This scope will be cleared when the ViewModel is cleared, which is called [viewModel.oncleared].” ? How do I cancel without code? The ViewModel source code shows that onCleared() is called by clear(), so let’s see what clear() does:

@MainThread
    final void clear() {
    	// Mark that the current ViewModel has been cleared
        mCleared = true;
        / / determine whether mBagOfTags is empty, no is null, the value of the ergodic map and invoke the closeWithRuntimeException () method
        if(mBagOfTags ! =null) {
            synchronized (mBagOfTags) {
                for (Object value : mBagOfTags.values()) {
                    // see comment for the similar call in setTagIfAbsent
                    closeWithRuntimeException(value);
                }
            }
        }
        onCleared();
    }
Copy the code

Let’s see what mBagOfTags does:

   @Nullable
    private final Map<String, Object> mBagOfTags = new HashMap<>();
Copy the code

It’s a Map to find out when to call mbagoftags.put () :

   /** * sets the tags and keys associated with this viewModel. * Sets a tag associated with this viewModel and a key. * If the given newValue is Closeable, it will be closed once clear (). * If the given {@code newValue} is {@link Closeable},
     * it will be closed once {@link#clear()}. * <p> * If a value has been set for the given key, this call does nothing and returns the currently associated value, * If a value was already set for the given key, this calls do nothing and * returns currently associated value, the given {@codeNewValue} would be ignored * <p> * If ViewModel has been cleared, then close () will be called on the returned object if it implements closeable. The same object may receive multiple close calls, so methods should be idempotent. * If the ViewModel was already cleared then close() would be called on the returned object if * it implements {@link Closeable}. The same object may receive multiple close calls, so method
     * should be idempotent.
     */
    @SuppressWarnings("unchecked")
    <T> T setTagIfAbsent(String key, T newValue) {
    	/ / value
        T previous;
        synchronized (mBagOfTags) {
        	// Try to obtain whether the key is hit
            previous = (T) mBagOfTags.get(key);
            if (previous == null) {
            	// If no, put it in mBagOfTagsmBagOfTags.put(key, newValue); }}// Return value assignment
        T result = previous == null ? newValue : previous;
        // If this viewModel is marked clear
        if (mCleared) {
        	// We may call close () on the same object multiple times,
        	// But the Closeable interface requires the close method to be idempotent: "If the stream is closed, then calling this method has no effect."
            // It is possible that we'll call close() multiple times on the same object, but
            // Closeable interface requires close method to be idempotent:
            // "if the stream is already closed then invoking this method has no effect." (c)
            // Call Closeable's close method
            closeWithRuntimeException(result);
        }
        return result;
    }
Copy the code

The setTagIfAbsent() method is familiar? Turns out it was called when we got the viewModelScope object. We know that the viewModelScope object is cached in the mBagOfTags object of type Map.

The setTagIfAbsent annotation states that T newValue implements the Closeable interface. Let’s review how viewModelScope is fetched:

private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"

/** * CoroutineScope * [CoroutineScope] tied to this [ViewModel]. * When you call [viewModel.oncleared], * This scope will be canceled when ViewModel will be cleared, i.e [ViewModel.onCleared] is called * * This scope is bound to * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate] */
public val ViewModel.viewModelScope: CoroutineScope
    get() {
    	// First try to get a CoroutineScope object with JOB_KEY tag from the cache
    	// Returns viewModelScope if it hits
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if(scope ! =null) {
            return scope
        }
        // If the cache is not hit, it is added to the ViewModel via setTagIfAbsent()
        return setTagIfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context

    override fun close(a) {
        coroutineContext.cancel()
    }
}
Copy the code

The first time viewModelScope is called, the setTagIfAbsent(JOB_KEY, CloseableCoroutineScope) is called for caching. Look at the CloseableCoroutineScope class, which implements the Closeable interface and canceles the coroutineContext object in close().

So far, we know how the viewModelScope object is added to the ViewModel and the viewModelScope is cancelled when the ViewModel is cleared.

The second question is why the viewModelScope is cancelled when called [viewModel.onCleared]. Solve!