• Easy Coroutines in Android: viewModelScope
  • Originally written by Manuel Vivo
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: twang1727

Virginia Poltrack

Canceling coroutines that are no longer needed is a forgettable task that can be tedious and introduce a lot of template code. The viewModelScope’s contribution to structured concurrency is to add an extended property to the ViewModel class that automatically cancels the subcoroutine when the ViewModel is destroyed.

Announcement: viewModelScope will be introduced in AndroidX Lifecycle V2.1.0 which is still in alpha. Because in alpha, the API can change, it can be buggy. Click here to report an error.

Scope of the ViewModel

CoroutineScope keeps track of all the coroutines it creates. Therefore, when you cancel a scope, all coroutines created by it are also cancelled. This is especially important when you’re running coroutines in ViewModel. If your ViewModel is about to be destroyed, all its asynchronous work must also be stopped. Otherwise, you’re wasting resources and potentially leaking memory. If you feel that an asynchronous task should be preserved after the ViewModel is destroyed, that task should be placed at a lower level in the application architecture.

Creating a new scope and passing in a container that will be canceled in the onCleared() method is the SupervisorJob, and you’re adding a CoroutineScope to the ViewModel. Coroutines created in this scope will persist for as long as the ViewModel is used. The code is as follows:

class MyViewModel : ViewModel() {/** * This is the task for all coroutines that this ViewModel runs. * Terminating this task will terminate all coroutines started by the ViewModel. */ private val viewModelJob = SupervisorJob() /** this is the main scope for all the coroutines MainViewModel is running in. * Since we passed the viewModelJob, you can cancel all the coroutines uiScope starts by calling viewModelJob.cancel() *. */ private val uiScope = CoroutineScope(dispatchers.main + viewModelJob) /** * Cancel all coroutines when ViewModel is empty */ Override funonCleared() {super.oncleared () viewModeljob.cancel ()} /** * Heavy operations that cannot be completed in the main thread */ funlaunchDataLoad() {
        uiScope.launch {
            sortList()
            // 更新 UI
        }
    }
    
    suspendFun sortList() = withContext(dispatchers.default) {// Heavy tasks}}Copy the code

The onerous operations running in the background are cancelled when the ViewModel is destroyed because the corresponding coroutine is started by the uiScope.

But in each ViewModel we’re going to have to introduce this much code, right? We can actually simplify this with viewModelScope.

ViewModelScope can reduce template code

AndroidX Lifecycle v2.1.0 introduces the extended property viewModelScope in the ViewModel class. It manages coroutines in the same way as in the previous section. The code is reduced to:

class MyViewModel : ViewModel() {/** * heavy operations that cannot be completed on the main thread */ funlaunchDataLoad() {
        viewModelScope.launch {
            sortList()
            // 更新 UI
        }
    }
  
    suspendFun sortList() = withContext(dispatchers.default) {// Heavy tasks}}Copy the code

All the CoroutineScope create and cancel steps are ready for us. To use it, simply import the following dependencies in build.gradle:

Implementation "androidx. Lifecycle. Lifecycle - viewmodel - KTX$lifecycle_version"Copy the code

Let’s take a look at the underlying implementation.

In-depth viewModelScope

AOSP has shared code. ViewModelScope is implemented like this:

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))
    }
Copy the code

The ViewModel class has a ConcurrentHashSet property to store objects of any type. This is where the CoroutineScope is stored. If we look at the code, the getTag(JOB_KEY) method tries to fetch the scope from it. If the fetch value is empty, it creates a new CoroutineScope and labels it in the manner mentioned earlier.

When the ViewModel is cleared, it runs the clear() method and calls the onCleared() method that we would have had to override without the viewModelScope. In the clear() method, the ViewModel cancels the task in the viewModelScope. The full ViewModel code is here, but we’ll only discuss the parts you care about:

@MainThread
final void clear() {
    mCleared = true; Since clear() is final, this method is still called on mock objects, and in those cases mBagOfTags are null. But it will always be empty, // becausesetTagIfAbsent and getTag are not // final methods so we don't have to clear them.if(mBagOfTags ! = null) {for (Object value : mBagOfTags.values()) {
            // see comment for the similar call in setTagIfAbsent
            closeWithRuntimeException(value);
        }
    }
    onCleared();
}
Copy the code

Iterate through all of the objects and invoke closeWithRuntimeException, this method checks whether the object belongs to Closeable type, if it is closed it. In order for the scope to be closed by the ViewModel, it should implement the Closeable interface. This is why the viewModelScope type is CloseableCoroutineScope, which extends CoroutineScope, overwrites coroutineContext, and implements the Closeable interface.

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

Dispatchers.main is used by default

Dispatchers.Main is the default CoroutineDispatcher for viewModelScope.

val scope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main)
Copy the code

Dispatchers.Main is useful here because the ViewModel is associated with frequently updated UIs, whereas using other Dispatchers would introduce at least two thread switches. Given that the suspension method has its own thread-blocking mechanism, using another dispatcher is not appropriate because we don’t want to replace what the ViewModel already has.

Unit test viewModelScope

Dispatchers.main uses Android’s looper.getMainLooper () method to execute code in the UI thread. This method is available for Instrumented Android tests, but not for unit tests.

Borrow org. Jetbrains. Kotlinx: kotlinx – coroutines – test: $coroutines_version library, Call dispatchers. setMain and pass in a singleThreadExecutor to replace the main dispatcher. Do not use dispatchers. Unconfined, it breaks all assumptions and timelines of code that uses dispatchers. Main. Because unit tests should run perfectly in isolation without causing any side effects, you should call dispatchers.resetmain () to clean up the executor when the test is complete.

You can simplify your code with the following JUnitRule that embodies this logic.

@ExperimentalCoroutinesApi
class CoroutinesMainDispatcherRule : TestWatcher() {
  
  private val singleThreadExecutor = Executors.newSingleThreadExecutor()
  
  override fun starting(description: Description?) {
      super.starting(description)
      Dispatchers.setMain(singleThreadExecutor.asCoroutineDispatcher())
  }
  
  override fun finished(description: Description?) {
      super.finished(description)
      singleThreadExecutor.shutdownNow()
      Dispatchers.resetMain()
  }
}
Copy the code

Now you can add it to your unit tests.

class MainViewModelUnitTest {
  
    @get:Rule
    var coroutinesMainDispatcherRule = CoroutinesMainDispatcherRule()
  
    @Test
    fun test() {... }}Copy the code

Please note that this is subject to change. TestCoroutineContext integration with structured concurrency is in progress, see this issue for details.


If you use viewModels and coroutines, let the framework manage the lifecycle through viewModelScope. Don’t think twice!

Coroutines Codelab has been updated to use it. Learn how to use coroutines in Android applications.

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.