Kotlin collaboration with architecture components and underlying principle analysis

kotlin's coroutine encapsulates the API of threads. This threading framework makes it easy for us to write asynchronous code.

Although the collaboration process is very convenient, it will be more convenient if it is used together with the KTX extension of the architecture component provided by Google.

1. add KTX dependency

//Using the Kotlin collaboration with architecture components

//ViewModelScope
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
//LifecycleScope
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
//liveData
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'

2. viewModelScope

2.1 using collaboration in ViewModel in the old way

Before using ViewModelScope, let's review the previous ways of using collaboration in ViewModel. Manage the CoroutineScope by yourself and cancel it when it is not needed (usually in onCleared()). Otherwise, it may cause problems such as resource waste and memory leakage.

class JetpackCoroutineViewModel : ViewModel() {
    //When using a collaboration in this ViewModel, you need to use this job to facilitate the cancellation control
    private val viewModelJob = SupervisorJob()
    
    //Specify where the collaboration is executed, and uiScope can be easily cancelled by viewModelJob
    private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
    
    fun launchDataByOldWay() {
        uiScope.launch {
            //Execute in the background
            val result = getNetData()
            //Modify UI
            log(result)
        }
    }
    
    override fun onCleared() {
        super.onCleared()
        viewModelJob.cancel()
    }
    
    //Switch time-consuming tasks to IO threads for execution
    private suspend fun getNetData() = withContext(Dispatchers.IO) {
        //Analog network time
        delay(1000)
        //Simulation return results
        "{}"
    }
}

There seems to be a lot of boilerplate code, and it's easy to forget to cancel the collaboration when you don't need it.

2.2 new way to use collaboration in ViewModel

It is in this case that Google created ViewModelScope for us. It makes it convenient for us to use the collaboration process by adding extended properties to the ViewModel class, and will automatically cancel its child collaboration process when the ViewModel is destroyed.

class JetpackCoroutineViewModel : ViewModel() {
    fun launchData() {
        viewModelScope.launch {
            //Execute in the background
            val result = getNetData()
            //Modify UI
            log(result)
        }
    }

    //Switch time-consuming tasks to IO threads for execution
    private suspend fun getNetData() = withContext(Dispatchers.IO) {
        //Analog network time
        delay(1000)
        //Simulation return results
        "{}"
    }

}

All the initialization and cancellation of CoroutineScope have been completed for us. We only need to use viewModelScope in the code to start a new collaboration, and we don't have to worry about forgetting to cancel.

Let's take a look at how Google implements it.

2.3 underlying implementation of viewmodelscope

Click to see the source code and know the root. In case of any strange bug in the future, you can think of a solution faster when you know the principle.

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

/**
 * [CoroutineScope] tied to this [ViewModel].
 * 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() {
        //Take values from the cache first, and return directly if any
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        //Create a CloseableCoroutineScope without cache
        return setTagIfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }

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

    override fun close() {
        coroutineContext.cancel()
    }
}

The source code first introduces what viewModelScope is. It is actually an extended attribute of ViewModel. Its actual type is CloseableCoroutineScope. This name looks like a collaboration that can be canceled. Sure enough, it implements Closeable and cancels it in the close method.

Each time you use viewModelScope, you will get it from the cache first. If not, you will create a CloseableCoroutineScope. It should be noted that the CloseableCoroutineScope is executed in the main thread.

What we need to know now is how the cache is stored and retrieved.

//ViewModel.java

// Can't use ConcurrentHashMap, because it can lose values on old apis (see b/37042460)
@Nullable
private final Map<String, Object> mBagOfTags = new HashMap<>();
/**
 * Returns the tag associated with this viewmodel and the specified key.
 */
@SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"})
<T> T getTag(String key) {
    if (mBagOfTags == null) {
        return null;
    }
    synchronized (mBagOfTags) {
        return (T) mBagOfTags.get(key);
    }
}

/**
 * Sets a tag associated with this viewmodel and a key.
 * If the given {@code newValue} is {@link Closeable},
 * it will be closed once {@link #clear()}.
 * <p>
 * If a value was already set for the given key, this calls do nothing and
 * returns currently associated value, the given {@code newValue} would be ignored
 * <p>
 * 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) {
    T previous;
    synchronized (mBagOfTags) {
        previous = (T) mBagOfTags.get(key);
        if (previous == null) {
            mBagOfTags.put(key, newValue);
        }
    }
    T result = previous == null ? newValue : previous;
    if (mCleared) {
        // 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)
        closeWithRuntimeException(result);
    }
    return result;
}

Now we know that it exists in mBagOfTags of ViewModel, which is a HashMap.

You know how to save it. When did you use it?

@MainThread
final void clear() {
    mCleared = true;
    // Since clear() is final, this method is still called on mock objects
    // and in those cases, mBagOfTags is null. It'll always be empty though
    // because setTagIfAbsent and getTag are not final so we can skip
    // clearing it
    if (mBagOfTags != null) {
        synchronized (mBagOfTags) {
            for (Object value : mBagOfTags.values()) {
                // see comment for the similar call in setTagIfAbsent
                closeWithRuntimeException(value);
            }
        }
    }
    onCleared();
}

private static void closeWithRuntimeException(Object obj) {
    if (obj instanceof Closeable) {
        try {
            ((Closeable) obj).close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

I searched the ViewModel for mBagOfTags and found that there is a clear method in which mBagOfTags is traversed and all value s that are closed are closed. In the above source code, when using viewModelScope for the first time, a CloseableCoroutineScope will be created, which implements the Closeable interface and the close method, just for canceling.

After seeing this, we know that the workflow built by viewModelScope is canceled when the clear method of ViewModel is called back.

Moreover, there is the familiar onCleared method call in the clear method. We know what onCleared does. This method will be called back when the ViewModel is no longer used. Generally, we need to do some finishing work in this method, such as canceling the observer subscription and closing resources.

Well, let's make a bold guess. This clear() method should also be called when the ViewModel is about to end its life.

After searching, I found that the clear method was called in the ViewModelStore.

public class ViewModelStore {

    private final HashMap<String, ViewModel> mMap = new HashMap<>();

    final void put(String key, ViewModel viewModel) {
        ViewModel oldViewModel = mMap.put(key, viewModel);
        if (oldViewModel != null) {
            oldViewModel.onCleared();
        }
    }

    final ViewModel get(String key) {
        return mMap.get(key);
    }

    Set<String> keys() {
        return new HashSet<>(mMap.keySet());
    }

    /**
     *  Clears internal storage and notifies ViewModels that they are no longer used.
     */
    public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.clear();
        }
        mMap.clear();
    }
}

ViewModelStore is a container for holding viewmodels. The clear method of all viewmodels in the ViewModelStore is called in the clear method of the ViewModelStore. Where is the clear of ViewModelStore called? I followed the trace and found that it was in the construction method of ComponentActivity.

public ComponentActivity() {
    Lifecycle lifecycle = getLifecycle();
    getLifecycle().addObserver(new LifecycleEventObserver() {
        @Override
        public void onStateChanged(@NonNull LifecycleOwner source,
                @NonNull Lifecycle.Event event) {
            if (event == Lifecycle.Event.ON_DESTROY) {
                if (!isChangingConfigurations()) {
                    getViewModelStore().clear();
                }
            }
        }
    });
}

When the Activity lifecycle reaches onDestroy, call the ViewModelStore clear to finish the work. However, please note that there is a premise for this call. This time, onDestroy will not call the clear method because of configuration changes.

OK, so far, we have figured out how the collaboration of viewModelScope is automatically cancelled (mBagOfTags of ViewModel) and when it is cancelled (clear() of ViewModel).

3. lifecycleScope

For Lifecycle, Google provides LifecycleScope. We can create Coroutine directly through launch.

3.1 use

For example, in onCreate of Activity, the TextView text is updated every 100 milliseconds.

lifecycleScope.launch {
    repeat(100000) {
        delay(100)
        tvText.text = "$it"
    }
}

Because LifeCycle can sense the LifeCycle of components, once the Activity is onDestroy, the lifecycleScope above will be used accordingly. The call to the launch closure is also canceled.

In addition, lifecycleScope also provides the launchWhenCreated, launchWhenStarted, and launchWhenResumed methods. The closures of these methods contain the scope of the collaboration. They are executed when CREATED, STARTED, and RESUMED respectively.

//Mode 1
lifecycleScope.launchWhenStarted {
    repeat(100000) {
        delay(100)
        tvText.text = "$it"
    }
}
//Mode 2
lifecycleScope.launch {
    whenStarted { 
        repeat(100000) {
            delay(100)
            tvText.text = "$it"
        }
    }
}

Either calling launchWhenStarted directly or calling whenStarted in the launch can achieve the same effect.

3.2 underlying implementation of lifecyclescope

Let's take a look at lifecyclescope How launch does it

/**
 * [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle].
 *
 * This scope will be cancelled when the [Lifecycle] is destroyed.
 *
 * This scope is bound to
 * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
 */
val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope

Good guy, extended attribute again. This time, the LifecycleOwner is extended and a LifecycleCoroutineScope is returned. Each time you get, the returned lifecycle Coroutinescope, see what this is.

/**
 * [CoroutineScope] tied to this [Lifecycle].
 *
 * This scope will be cancelled when the [Lifecycle] is destroyed.
 *
 * This scope is bound to
 * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
 */
val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
        while (true) {
            val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
            if (existing != null) {
                return existing
            }
            val newScope = LifecycleCoroutineScopeImpl(
                this,
                SupervisorJob() + Dispatchers.Main.immediate
            )
            if (mInternalScopeRef.compareAndSet(null, newScope)) {
                newScope.register()
                return newScope
            }
        }
    }

The coroutineScope of Lifecycle is also an extended attribute. It is a LifecycleCoroutineScope. It can be seen from the comments that after the Lifecycle is destroyed, the collaboration will be cancelled. Here, the cache stored before will be fetched from mInternalScopeRef. If it is not regenerated, it will be put into a LifecycleCoroutineScopeImpl, and the register function of LifecycleCoroutineScopeImpl will be called. mInternalScopeRef here is an attribute in the Lifecycle class: AtomicReference<object> mInternalScopeRef = new AtomicReference<> (); AtomicReference enables an object to guarantee atomicity. AtomicReference is used here, of course, for thread safety.

Since the LifecycleCoroutineScopeImpl is generated, let's take a look at what it is

internal class LifecycleCoroutineScopeImpl(
    override val lifecycle: Lifecycle,
    override val coroutineContext: CoroutineContext
) : LifecycleCoroutineScope(), LifecycleEventObserver {
    init {
        // in case we are initialized on a non-main thread, make a best effort check before
        // we return the scope. This is not sync but if developer is launching on a non-main
        // dispatcher, they cannot be 100% sure anyways.
        if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
            coroutineContext.cancel()
        }
    }

    fun register() {
        //Start a collaboration. If the current Lifecycle state is greater than or equal to INITIALIZED, register a Lifecycle observer to observe the Lifecycle
        launch(Dispatchers.Main.immediate) {
            if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) {
                lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)
            } else {
                coroutineContext.cancel()
            }
        }
    }

    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        //If it is observed that the current life cycle is less than or equal to DESTROYED, the current observer is removed and the collaboration is cancelled
        if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
            lifecycle.removeObserver(this)
            coroutineContext.cancel()
        }
    }
}

In the above code, there are two important functions: register and onStateChanged. The register function is called when initializing LifecycleCoroutineScopeImpl. First, add an observer in the register function to observe the change of the life cycle, and then remove the observer and cancel the collaboration when the life cycle is determined to be DESTROYED in the onStateChanged function.

There is a small detail. Why can the register function directly start a coroutine? Because LifecycleCoroutineScopeImpl inherits LifecycleCoroutineScope, and LifecycleCoroutineScope implements the CoroutineScope interface (in fact, it is implemented in LifecycleCoroutineScopeImpl).

public abstract class LifecycleCoroutineScope internal constructor() : CoroutineScope {
    internal abstract val lifecycle: Lifecycle
    ......
}

Now we have clarified the process. When lifecycleScope is used, it will build a collaboration process, observe the component life cycle, and cancel the collaboration process at the appropriate time (DESTROYED).

In the above example, we have seen a code:

//Mode 1
lifecycleScope.launchWhenStarted {
    repeat(100000) {
        delay(100)
        tvText.text = "$it"
    }
}
//Mode 2
lifecycleScope.launch {
    whenStarted { 
        repeat(100000) {
            delay(100)
            tvText.text = "$it"
        }
    }
}

You can directly use the launchWhenCreated, launchWhenStarted, and launchWhenResumed provided by the lifecycleScope to execute the collaboration process in the corresponding life cycle.

Click inside to have a look

abstract class LifecycleCoroutineScope internal constructor() : CoroutineScope {
    internal abstract val lifecycle: Lifecycle

    /**
     * Launches and runs the given block when the [Lifecycle] controlling this
     * [LifecycleCoroutineScope] is at least in [Lifecycle.State.CREATED] state.
     *
     * The returned [Job] will be cancelled when the [Lifecycle] is destroyed.
     * @see Lifecycle.whenCreated
     * @see Lifecycle.coroutineScope
     */
    fun launchWhenCreated(block: suspend CoroutineScope.() -> Unit): Job = launch {
        lifecycle.whenCreated(block)
    }

    /**
     * Launches and runs the given block when the [Lifecycle] controlling this
     * [LifecycleCoroutineScope] is at least in [Lifecycle.State.STARTED] state.
     *
     * The returned [Job] will be cancelled when the [Lifecycle] is destroyed.
     * @see Lifecycle.whenStarted
     * @see Lifecycle.coroutineScope
     */

    fun launchWhenStarted(block: suspend CoroutineScope.() -> Unit): Job = launch {
        lifecycle.whenStarted(block)
    }

    /**
     * Launches and runs the given block when the [Lifecycle] controlling this
     * [LifecycleCoroutineScope] is at least in [Lifecycle.State.RESUMED] state.
     *
     * The returned [Job] will be cancelled when the [Lifecycle] is destroyed.
     * @see Lifecycle.whenResumed
     * @see Lifecycle.coroutineScope
     */
    fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch {
        lifecycle.whenResumed(block)
    }
}

It turns out that these functions are the functions in the LifecycleCoroutineScope class returned by the lifecycleScope extended attribute of LifecycleOwner. These functions do nothing but directly call the functions corresponding to the lifecycle

/**
 * Runs the given block when the [Lifecycle] is at least in [Lifecycle.State.CREATED] state.
 *
 * @see Lifecycle.whenStateAtLeast for details
 */
suspend fun <T> Lifecycle.whenCreated(block: suspend CoroutineScope.() -> T): T {
    return whenStateAtLeast(Lifecycle.State.CREATED, block)
}

/**
 * Runs the given block when the [Lifecycle] is at least in [Lifecycle.State.STARTED] state.
 *
 * @see Lifecycle.whenStateAtLeast for details
 */
suspend fun <T> Lifecycle.whenStarted(block: suspend CoroutineScope.() -> T): T {
    return whenStateAtLeast(Lifecycle.State.STARTED, block)
}

/**
 * Runs the given block when the [Lifecycle] is at least in [Lifecycle.State.RESUMED] state.
 *
 * @see Lifecycle.whenStateAtLeast for details
 */
suspend fun <T> Lifecycle.whenResumed(block: suspend CoroutineScope.() -> T): T {
    return whenStateAtLeast(Lifecycle.State.RESUMED, block)
}

These functions were originally suspend functions and extended Lifecycle functions. They finally call the whenStateAtLeast function and pass in the minimum Lifecycle state flag (minState) of the execution collaboration.

suspend fun <T> Lifecycle.whenStateAtLeast(
    minState: Lifecycle.State,
    block: suspend CoroutineScope.() -> T
) = withContext(Dispatchers.Main.immediate) {
    val job = coroutineContext[Job] ?: error("when[State] methods should have a parent job")
    val dispatcher = PausingDispatcher()
    val controller =
        LifecycleController(this@whenStateAtLeast, minState, dispatcher.dispatchQueue, job)
    try {
        //Execution coordination
        withContext(dispatcher, block)
    } finally {
        //Close out work remove Life Cycle Observations
        controller.finish()
    }
}

@MainThread
internal class LifecycleController(
    private val lifecycle: Lifecycle,
    private val minState: Lifecycle.State,
    private val dispatchQueue: DispatchQueue,
    parentJob: Job
) {
    private val observer = LifecycleEventObserver { source, _ ->
        if (source.lifecycle.currentState == Lifecycle.State.DESTROYED) {
            //Destroyed-> cancel collaboration
            handleDestroy(parentJob)
        } else if (source.lifecycle.currentState < minState) {
            dispatchQueue.pause()
        } else {
            //implement
            dispatchQueue.resume()
        }
    }

    init {
        // If Lifecycle is already destroyed (e.g. developer leaked the lifecycle), we won't get
        // an event callback so we need to check for it before registering
        // see: b/128749497 for details.
        if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
            handleDestroy(parentJob)
        } else {
            //Observe life cycle changes
            lifecycle.addObserver(observer)
        }
    }

    @Suppress("NOTHING_TO_INLINE") // avoid unnecessary method
    private inline fun handleDestroy(parentJob: Job) {
        parentJob.cancel()
        finish()
    }

    /**
     * Removes the observer and also marks the [DispatchQueue] as finished so that any remaining
     * runnables can be executed.
     */
    @MainThread
    fun finish() {
        //Remove lifecycle watcher
        lifecycle.removeObserver(observer)
        //Mark completed and execute the remaining executable Runnable
        dispatchQueue.finish()
    }
}

whenStateAtLeast is also an extension function of Lifecycle. The core logic is to add LifecycleObserver to LifecycleController to monitor the Lifecycle status, and determine whether to suspend execution, resume execution, or cancel execution through the status. After the execution is completed, that is, finally, finish the work by executing LifecycleController's finish: remove the Lifecycle listener and start to execute the remaining tasks.

Once the execution is completed, the lifecycle Watcher will be removed, which is equivalent to that the closures written to functions such as launchWhenResumed will only be executed once. After the execution is completed, it will not be executed again even after onpause->onresume.

4. liveData

During our normal use of livedata, we may be involved in this scenario: requesting the network to get the results, then transferring the data through livedata, receiving the notification in the Activity, and then updating the UI. Very common scenarios. In this case, we can simplify the above scenario code through the official livedata constructor function.

4.1 use

val netData: LiveData<String> = liveData {
    //If the observation is within the life cycle, it will be executed immediately
    val data = getNetData()
    emit(data)
}

//Switch time-consuming tasks to IO threads for execution
private suspend fun getNetData() = withContext(Dispatchers.IO) {
    //Analog network time
    delay(5000)
    //Simulation return results
    "{}"
}

In the above example, getNetData() is a suspend function. Use the livedata constructor function to asynchronously call getNetData(), and then use emit() to submit the results. On the Activity side, if the netData is observed and active, the result will be received. As we know, the suspend function needs to be called in the scope of the collaboration, so the closure of livedata also has the scope of the collaboration.

There is a small detail. If the component is just in the active state when observing this netData, the code in the liveData closure will be executed immediately.

In addition to the above usage, you can also emit multiple values in liveData.

val netData2: LiveData<String> = liveData {
    delay(3000)
    val source = MutableLiveData<String>().apply {
        value = "11111"
    }
    val disposableHandle = emitSource(source)

    delay(3000)
    disposableHandle.dispose()
    val source2 = MutableLiveData<String>().apply {
        value = "22222"
    }
    val disposableHandle2 = emitSource(source2)
}

It should be noted that when the latter one calls emitSource, it needs to call the dispose function to cut off the return value of the previous emitSource.

4.2 underlying implementation of livedata

Old rule, Ctrl+ click the left mouse button to see the source code

@UseExperimental(ExperimentalTypeInference::class)
fun <T> liveData(
    context: CoroutineContext = EmptyCoroutineContext,
    timeoutInMs: Long = DEFAULT_TIMEOUT,
    @BuilderInference block: suspend LiveDataScope<T>.() -> Unit
): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)

//The code we wrote in the closure behind liveData is passed to the block here. It is a suspend function with the context of LiveDataScope

First of all, the liveData function is actually a global function, which means that you can use it anywhere, not just in the Activity or ViewModel.

Secondly, the liveData function returns a CoroutineLiveData object? What is returned is an object, and no code is executed here. Where does my code execute?

This depends on the code of the CoroutineLiveData class

internal class CoroutineLiveData<T>(
    context: CoroutineContext = EmptyCoroutineContext,
    timeoutInMs: Long = DEFAULT_TIMEOUT,
    block: Block<T>
) : MediatorLiveData<T>() {
    private var blockRunner: BlockRunner<T>?
    private var emittedSource: EmittedSource? = null

    init {
        // use an intermediate supervisor job so that if we cancel individual block runs due to losing
        // observers, it won't cancel the given context as we only cancel w/ the intention of possibly
        // relaunching using the same parent context.
        val supervisorJob = SupervisorJob(context[Job])

        // The scope for this LiveData where we launch every block Job.
        // We default to Main dispatcher but developer can override it.
        // The supervisor job is added last to isolate block runs.
        val scope = CoroutineScope(Dispatchers.Main.immediate + context + supervisorJob)
        blockRunner = BlockRunner(
            liveData = this,
            block = block,
            timeoutInMs = timeoutInMs,
            scope = scope
        ) {
            blockRunner = null
        }
    }

    internal suspend fun emitSource(source: LiveData<T>): DisposableHandle {
        clearSource()
        val newSource = addDisposableSource(source)
        emittedSource = newSource
        return newSource
    }

    internal suspend fun clearSource() {
        emittedSource?.disposeNow()
        emittedSource = null
    }

    override fun onActive() {
        super.onActive()
        blockRunner?.maybeRun()
    }

    override fun onInactive() {
        super.onInactive()
        blockRunner?.cancel()
    }
}

There is little code in it. It mainly inherits MediatorLiveData, and then executes the maybeRun function of BlockRunner when onActive. What is executed in the maybeRun of BlockRunner is actually the code block we wrote in livedata, while the onActive method is actually inherited from livedata and will be called when an active observer listens to livedata.

This makes sense. In the above case, I observed netData in onCreate (active) of Activity, so the code in liveData will be executed immediately.

//typealias type alias
//This will be used in the BlockRunner below to carry the code in the closure behind liveData
internal typealias Block<T> = suspend LiveDataScope<T>.() -> Unit

/**
 * Handles running a block at most once to completion.
 */
internal class BlockRunner<T>(
    private val liveData: CoroutineLiveData<T>,
    private val block: Block<T>,
    private val timeoutInMs: Long,
    private val scope: CoroutineScope,
    private val onDone: () -> Unit
) {
    @MainThread
    fun maybeRun() {
       ...
        //The scope here is CoroutineScope(Dispatchers.Main.immediate + context + supervisorJob)
        runningJob = scope.launch {
            val liveDataScope = LiveDataScopeImpl(liveData, coroutineContext)
            //The block here executes the code we wrote in liveData. When executing the block, the liveDataScope instance is passed in, and the liveDataScope context is
            block(liveDataScope)
            //complete
            onDone()
        }
    }
    ...
}

internal class LiveDataScopeImpl<T>(
    internal var target: CoroutineLiveData<T>,
    context: CoroutineContext
) : LiveDataScope<T> {

    ...
    // use `liveData` provided context + main dispatcher to communicate with the target
    // LiveData. This gives us main thread safety as well as cancellation cooperation
    private val coroutineContext = context + Dispatchers.Main.immediate
    
    //Because there is a livedatascope impl context when the liveData closure is executed, you can use the emit function
    override suspend fun emit(value: T) = withContext(coroutineContext) {
        target.clearSource()
        //Set value for the livedata of the target, and the livedata returns the target. Observe the target in the component, and you will receive the value data here
        target.value = value
    }
}

A coroutine is started in the maybeRun function of BlockRunner. This scope is initialized in CoroutineLiveData: CoroutineScope(Dispatchers.Main.immediate + context + supervisorJob). Then, the code written in the closure behind liveData is executed in this scope, and there is the context of livedatascope impl. With the context of livedatascope impl, we can use the emit method in livedatascope impl. The emit method is actually very simple, that is, to give a data to a liveData object, and the liveData is the one returned by liveData{}. At this time, because the data of liveData has changed, if a component observes the liveData and the component is active, the component will receive a callback for the data change.

The general process of the whole process is to build a collaboration in LiveData{} and return a LiveData. Then the code in the closure we wrote is actually executed in a collaboration. When we call the emit method, we are updating the value in the LiveData. Since it is the returned LiveData, it is naturally associated with the component life cycle. The results can be obtained only when the component is active, and some memory leakage problems are avoided.

5. summary

It has to be said that the official offer is convenience, which can greatly facilitate our use of the cooperative process. The partners who are using the collaboration and have not yet used it with the architecture components should use it quickly. Sizzling~

reference material

  • https://developer.android.com/kotlin/coroutines
  • https://developer.android.com/topic/libraries/architecture/coroutines?hl=zh-cn

Tags: kotlin

Posted by blawson7 on Fri, 03 Jun 2022 05:24:25 +0530