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:
- We need to manually cancel it. It’s 9102. It shouldn’t be
- 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:
- Added extended properties to ViewModel
viewModelScope
The advantage is that it is more convenient to use. - Then rewrite
viewModelScope
Property, according toJOB_KEY
Retrieve the CoroutineScope object, for nowJOB_KEY
It is fixed, and multiple keys may be added later. - Is created if the CoroutineScope object is empty
CloseableCoroutineScope
Object and passsetTagIfAbsent
Method to cache, which is a thread-safe operation according to the method name. CloseableCoroutineScope
Class 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:
- Provides a Map to store coroutine Scope objects and provides methods for set and GET
- in
onCleared
Iterating 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:
- To decouple, define interface intermodulation, tune to tune to bypass
- Sending messages using EventBus can be difficult to manage if the code is large and has hundreds of identifiers
- 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.