Android ViewModel 引入協程

孫鵬發表於2019-05-11

ViewModel 引入協程

當我們在 ViewModel 裡面引入協程,首先要在 ViewModel 中新建一個 CoroutineScope, 用來管理所有協程任務,同時需要 onCleared() 方法裡面取消協程任務,模板程式碼實現如下:

class MyViewModel : ViewModel() {

    private val viewModelJob = SupervisorJob()
    
    private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
    
    override fun onCleared() {
        super.onCleared()
        viewModelJob.cancel() // Cancel all coroutines
    }
    
    fun launchDataLoad() {
        uiScope.launch {
            sortList()
            // Modify UI
        }
    }
    
    suspend fun sortList() = withContext(Dispatchers.Default) {
        // Heavy work
    }
}
複製程式碼

然而,很多情況我們會經常忘記取消協程,導致出現記憶體洩漏等各種問題。 那怎麼處理比較好?使用 ViewModel 擴充套件屬性 viewModelScope 來優化程式碼。

viewModelScope 方式

AndroidX Lifecycle v2.1.0 在 ViewModel 中引入 viewModelScope,當 ViewModel 被銷燬時它會自動取消協程任務,這個特性真的好用。viewModelScope 管理協程的方式與我們在 ViewModel 引入協程的方式一樣,程式碼實現如下:

class MyViewModel : ViewModel() {
  
    fun launchDataLoad() {
        viewModelScope.launch {
            sortList()
            // Modify UI
        }
    }
  
    suspend fun sortList() = withContext(Dispatchers.Default) {
        // Heavy work
    }
}
複製程式碼

注意 lifecycle-viewmodel-ktx 版本號: 2.1.0-beta01

viewModelScope 內部實現

// androidx.lifecycle.ViewModel.viewModelScope

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() {
        coroutineContext.cancel()
    }
}
複製程式碼
  1. viewModelScope 預設使用 Dispatchers.Main, 方便 Activity 和 Fragment 更新 UI
  2. 注意使用 SupervisorJob 而不是用 Job
  3. ViewModel 取消協程,需要實現 Closeable 介面

ViewModel 內部取消協程

ViewModel 類通過 HashMap 儲存 CoroutineScope 物件。當使用 getTag(JOB_KEY) 方法獲取物件不存在時,建立一個新的 CoroutineScope 並呼叫 setTagIfAbsent(JOB_KEY, scope) 方法儲存新建的 CoroutineScope 物件。當 ViewModel 被銷燬時內部會執行 clear() 方法。在 clear() 方法中遍歷呼叫 closeWithRuntimeException 取消了 viewModelScope 的協程,實現流程非常清晰。相關程式碼如下:

// androidx.lifecycle.ViewModel

private final Map<String, Object> mBagOfTags = new HashMap<>();

<T> T getTag(String key) {
    //noinspection unchecked
    synchronized (mBagOfTags) {
        return (T) mBagOfTags.get(key);
    }
}

<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;
}

@MainThread
final void clear() {
    mCleared = true;
    if (mBagOfTags != null) {
        for (Object value : mBagOfTags.values()) {
            closeWithRuntimeException(value);
        }
    }
    onCleared();
}

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

結論

如果你也正在使用 MVVM 和協程,非常推薦在 ViewModel 中使用 viewModelScope 方式。不僅簡化 ViewModel 程式碼,而且還能管理協程生命週期。

相關文章