一點點入坑JetPack:ViewModel篇

MDove發表於2019-02-03

前言

費了很多腦細胞,把Lifecycle單拆出來整了一篇文章。那麼接下來自然而然的就到了ViewModel,為了讓系列像系列的樣子,所以這裡仍然是單獨把ViewModel拿出來。

你別說單獨抽出來,還真有點乾乾巴巴,麻麻賴賴,一點都不圓潤。那還說啥呢?盤它…

正文

一、ViewModel

新官上任三把火,強敵面前秀三波。對於ViewModel來說,它算是JetPack框架中堪當中樞的角色,說實話它實在不好單獨去聊,更多的是和LiveData共進退。這裡必須安利一下,ViewModel+LiveData的確很好用,甚至可能加上Room簡直…飄了,拽了,感覺自己個頭都不矮了;瘋了,狂了,敢在宇宙之間稱王了….

礙於篇幅的原因,這裡單獨聊ViewModel,後邊會綜合介紹展現其強大的戰鬥力…

關於ViewModel來說,其實還是蠻簡單的。從ViewModel官方的描述來看ViewModel的存在,解決了倆大問題:

1.1、解決問題1

我們都知道,當我們的Activity/Fragment因為某些因素被銷燬重建時,我們的成員變數便失去了意義。因此我們常常需要通過 onSaveInstanceState()和onCreate()/onSaveInstanceState(Bundle)完成對資料的恢復(通常還要保證其正確的序列化)。並且對於大型資料來書,便有些乏力,比如:List、Bitmap…

而ViewModel就是解決此問題。

1.2、解決問題2

另一個問題是Activity/Fragment經常需要進行一些非同步操作。一旦涉及到非同步,我們都明白這裡存在記憶體洩漏的可能性。因此我們保證Activity/Fragment在銷燬後及時清理非同步操作,以避免潛在的記憶體洩漏。

ViewModel並沒有自動幫我們解決這個問題,而是通過onCleared()交給我們業務自己重寫去處理。

1.3、使用方法

關於ViewModel的使用,實在沒啥好說的。實在是太簡單了,一個簡單的demo:

class MyViewModel : ViewModel() {
    var name: String = "MDove"
}

// Activity中呼叫
class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        val model = ViewModelProviders.of(this).get(MyViewModel::class.java)
        // TODO model.name
    }
}
複製程式碼

我們只需要將想要被儲存、被管理的變數,宣告在ViewModel的實現類中即可。然後通過ViewModelProviders.of()/get()拿到這個例項。就可能像往常一樣自由的使用,而不需要擔心Activity/Fragment重建所帶來的一系列問題。

注意警告!

文件在此處,有一個大大的警告:Caution: A ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity context.

為啥?從上述解決的問題來看,ViewModel很明顯生命週期會比Activity要長,因此如果持有Activity相關例項,必然會帶來記憶體洩漏。(那如果的確有業務需要咋整?使用AndroidViewModel(application)即可。)

1.4、Fragment共享

值得注意的一點:of方法需要傳遞一個Activity/Fragment。因為ViewModel需要與其生命週期繫結。既然可以傳遞一個Activity,那麼我們就能夠猜到:是不是對於此Activity下的Fragment這個ViewModel也是可見的?

沒錯,正是如此。官方也作出瞭解讀:Activity中的兩個或多個Framgent需要相互通訊是很常見的,這個常見的痛點可以通過使用ViewModel物件來解決,這些Fragment可以共享ViewModel來處理通訊問題。

所以我們在同Activity下,不同的Fragment例項,可以直接通過傳入activity,拿到同樣的ViewModel例項,進而實現資料通訊。

真的很方便…

二、原始碼

如果我們開啟ViewModel的原始碼,我們會發現…

public abstract class ViewModel {
    @SuppressWarnings("WeakerAccess")
    protected void onCleared() {
    }
}
複製程式碼

就是一個抽象類,沒錯,整個ViewModel的設計就是很簡潔,我們往ViewModelProviders中繼續看:

@NonNull
@MainThread
public static ViewModelProvider of(@NonNull FragmentActivity activity,
        @Nullable Factory factory) {
    Application application = checkApplication(activity);
    if (factory == null) {
        factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
    }
    return new ViewModelProvider(activity.getViewModelStore(), factory);
}
複製程式碼

我們可以看到在例項化ViewModelProvider中,需要傳一個ViewModelStore,而這個ViewModelStore直接通過傳入的FragmentActivity中拿,讓我們走進去看一看:

@NonNull
@Override
public ViewModelStore getViewModelStore() {
    if (getApplication() == null) {
        throw new IllegalStateException("Your activity is not yet attached to the "
                + "Application instance. You can`t request ViewModel before onCreate call.");
    }
    if (mViewModelStore == null) {
        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();
        if (nc != null) {
            // Restore the ViewModelStore from NonConfigurationInstances
            mViewModelStore = nc.viewModelStore;
        }
        if (mViewModelStore == null) {
            mViewModelStore = new ViewModelStore();
        }
    }
    return mViewModelStore;
}
複製程式碼

我們可以看到,這個ViewModelStore是在FragmentActivity中是一個mViewModelStore的變數。這個ViewModelStore是什麼?從名字可以看出它是一個ViewModel的Store。

ViewModelStore的很簡單,就是一個Map在後文中會展開。

最開始我看到這時,很懵。ViewModel是保證我們重建後例項的唯一,可是這竟然是一個成員變數,很明顯重建後變數就沒了?!…(PS:當然有這種疑問,是因為我自己蠢…)

怎麼肥死,小老弟??…其實這裡是沒問題的,我們仔細看一看,這個mViewModelStore賦值是通過這一行程式碼:

NonConfigurationInstances nc = (NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
    mViewModelStore = nc.viewModelStore;
}
複製程式碼

沒錯,就是這行程式碼,保證了我們重建後恢復原來的mViewModelStore,進而保證了我們的ViewModel的唯一性。

@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
    ViewModel viewModel = mViewModelStore.get(key);

    if (modelClass.isInstance(viewModel)) {
        //noinspection unchecked
        return (T) viewModel;
    } else {
        //noinspection StatementWithEmptyBody
        if (viewModel != null) {
           // TODO: log a warning.
        }
    }

    viewModel = mFactory.create(modelClass);
    mViewModelStore.put(key, viewModel);
    return (T) viewModel;
}
複製程式碼

mViewModelStore原始碼 -> ViewModelStore原始碼,很常見的Map儲存操作

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

    public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.onCleared();
        }
        mMap.clear();
    }
}
複製程式碼

三、小總結

從ViewModel的使用上來說,似乎並沒有什麼“船新的版本”…更多的是幫我們搞定了一些現存的坑。的確是如此,但其實ViewModel更多是帶來了一種思想:資料驅動,也就是MVVM。

ViewModel作為中樞,擔任了從資料來源拿資料,交由LiveData通知UI層更新UI。用一張圖來解釋這種變革:

一點點入坑JetPack:ViewModel篇

Google Sample為Repository的編寫,提供了一個很巧妙的設計:NetworkBoundResource。全類一共有120+的程式碼,卻基於LiveData+ViewModel幫我們約束了:從伺服器取從資料庫取網路獲取失敗,從資料庫取…等等一系列網路請求、本地請求約數。

關於這個類的設計與用法,會在後續的實戰篇一點點展開。沒錯,當你用上它們,你會愛上這款“遊戲”。

尾聲

今天的文章想聊的內容就到此結束了,更多的是ViewModel的一個引子。畢竟對於我們來說,我tm不需要知道這些,只需要告訴我怎麼寫就行。老夫寫程式碼就是ctrl+c/v!

不著急,一點點來。後邊我會把業務中正在執行的程式碼拿出來,做實戰操作分析。飯要一口口的吃,文章要一篇篇的寫…

我是一個應屆生,最近和朋友們維護了一個公眾號,內容是我們在從應屆生過渡到開發這一路所踩過的坑,以及我們一步步學習的記錄,如果感興趣的朋友可以關注一下,一同加油~

個人公眾號:鹹魚正翻身

相關文章