Easy ViewModel & ViewModelScope
A ViewModelScope
is defined for each ViewModel
in your app. Any coroutine launched in this scope is automatically cancelled if the ViewModel
is cleared. Coroutines are useful here for when you have work that needs to be done only if the ViewModel
is active. If your ViewModel
is destroyed, all the asynchronous task might be doing must be stopped, to avoid unnecessary resource consume.
Scopes in ViewModel
You need to create a coroutine scope by creating SupervisorJob()
. you need to cancel it in the onCleared
method. This coroutine scope will live as long as ViewModel
is being used. Here is the code...
class UpdatePersonalDataViewModel @Inject
internal constructor(private val mRepository: Repository) : BaseViewModel<ProfileNavigator>() {
private val viewModelJob = SupervisorJob()
private val viewModelScope = CoroutineScope(Dispatchers.Main + viewModelJob)
fun launchGetProfile(token : String){
viewModelScope.launch {
getProfileData(token)
}
}
private suspend fun getProfileData(token : String){
mRepository.callDriverProfile(token)
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
Dispatchers.Main
is a natural fit here, as ViewModel
is often related to UI which if often involved in updating it. So launching on another dispatcher will cost at least two extra thread Switch.
Any heavy work occurring in the background will terminate immediately after the ViewModel
gets destroyed. This is not a ton of code to write for achieving this behaviour. But Imagine you have lots of ViewModel
inside your project. Then surely there are lots of code to write every time and that also the same code. So to rescue this here comes ViewModelScope
ViewModelScope
AndroidX lifecycle v2.1.0 introduced the extension property of ViewModelScope
. It works the same way we demonstrate above. The new code looks like this
class UpdatePersonalDataViewModel @Inject
internal constructor(private val mRepository: Repository) : BaseViewModel<ProfileNavigator>() {
fun launchGetProfile(token : String){
viewModelScope.launch {
getProfileData(token)
}
}
private suspend fun getProfileData(token : String){
mRepository.callDriverProfile(token)
}
}
ViewModelScope means less code
To get this we need to implement below dependency in your Gradle file
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-rc02'
Digging deep into ViewModelScope
Let’s take a look into ViewModelScope
below…
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]
*/
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.immediate))
}
internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}
ViewModel
has a concurrentHashSet
which can save any object. The scopes are saved here. Scopes are stored here. If we take a look atgetTag(JOB_KEY)
, this method tries to retrieve the coroutineScope from here. If there is no scope it creates a new scope the way we created earlier and stores the TAG into the bag.
Before destroying ViewModel
it calls clear()
before onCleared()
. In clear()
ViewModel
cancel the job of ViewModelScope
.
@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();
}
Another thing is to notice that, The method goes through all the items in the bag and apply closeWithRuntimeException
which checks if the object is of type of Closeable
if so, it closes it. So the ViewModelScope
is the type of CloseableCoroutineScope
which extends Coroutinecope