ViewModels 簡單入門

Android_開發者發表於2017-11-13

簡介

兩年前,我在做 給 Android 入門的課程,教零基礎學生開發 Android App。其中有一部分是教學生構建一個簡單 App 叫做 Court-Counter.

Court-Counter 是一個只有幾個按鈕來修改籃球比賽分數的 App。最終的App有一個bug,如果你旋轉手機,當前儲存的分數會莫名歸零。

ViewModels 簡單入門
這是什麼原因呢?因為旋轉裝置會導致 App 中一些 配置發生改變 ,比如鍵盤是否可用,變更裝置語言等。這些配置的改變都會導致 Activity 被銷燬重建。

這種表現可以讓我們在做一些特殊處理,比如裝置旋轉時變更為橫向特定佈局。 然而對於新手(有時候老鳥也是)工程師來說,這可能會讓他們頭疼。

在 Google I/O 2017,Android Framework團隊推出了一套 Architecture Components 的工具集,其中一個處理裝置旋轉的問題。

ViewModel 類旨在以有生命週期的方式儲存和管理與UI相關的資料。 這使得資料可以在螢幕旋轉等配置變化的情況下不丟失。

這篇文章是詳細探索ViewModel系列文章中的第一篇。 在這篇文章中,我會:

  • 解釋ViewModel滿足的基本需求
  • 通過更改 Court-Counter 程式碼以使用 ViewModel 解決旋轉問題
  • 仔細審視 ViewModel 和 UI 元件的關聯

潛在的問題

潛在的挑戰是 Android Activity 生命週期 中有很多狀態,並且由於配置更改,單個Activity可能會多次迴圈進入這些不同的狀態。

ViewModels 簡單入門
Activity 會經歷所有這些狀態,也可能需要把暫時的使用者介面資料儲存在記憶體中。這裡將把臨時UI資料定義為UI所需的資料。例子中包括使用者輸入的資料,執行時生成的資料或者是資料庫載入的資料。這些資料可以是bitmap, RecyclerView 所需的物件列表等等,在這個例子中,是指籃球得分。

以前你可能用過 onRetainNonConfigurationInstance 方法在配置更改期間儲存和恢復資料。但是,如果你的資料不需要知道或管理 Activity 所處的生命週期狀態,這樣寫會不會導致程式碼過於冗雜?如果 Activity 中有一個像scoreTeamA 這樣的變數,雖然與 Activity 生命週期緊密相連,但又儲存在Activity之外的地方呢?這就是 ViewModel 類的目的

在下面的圖表中,可以看到一個 Activity 的生命週期,該 Activity 經歷了一次旋轉,最後被 finish 掉。 ViewModel 的生命週期顯示在關聯的Activity生命週期旁邊。注意,ViewModels 可以很簡單的用與Fragments 和 Activities,,這裡稱他們為 UI 控制器。本示例著重於 Activities。

ViewModels 簡單入門
ViewModel從你首次請求建立ViewModel(通常在onCreate的Activity)時就存在,直到Activity完成並銷燬。Activity 的生命週期中,onCreate可能會被呼叫多次,比如當應用程式被旋轉時,但 ViewModel 會一直存在,不會被重建。

一個簡單的例子

分三步驟來設定和使用ViewModel:

  1. 通過建立一個擴充套件 ViewModel 類來從UI控制器中分離出你的資料
  2. 建立你的 ViewModel 和UI控制器之間的通訊
  3. 在 UI 控制器中使用你的 ViewModel

**第一步: 建立 ViewModel 類 **

一般來講,需要為每個介面都建立一個ViewModel類。這個ViewModel類將儲存與該屏相關的所有資料,提供 getter 和 setter。這樣就將資料與 UI 顯示邏輯分開了,UI邏輯在Activities 或 Fragments中,資料儲存在 ViewModel 中。好了,接下來為 Court-Counter 中的一個屏建立ViewModel類:

public class ScoreViewModel extends ViewModel {
// Tracks the score for Team A
public int scoreTeamA = 0;

// Tracks the score for Team B
public int scoreTeamB = 0;
}
複製程式碼

為了簡潔,這裡我採用了公共成員儲存在ScoreViewModel.java中,也可以選擇用 getter 和 setter 來更好地封裝資料。

第二步:關聯UI控制器和ViewModel

你的UI控制器(Activity或Fragment)需要訪問你的ViewModel。這樣,UI控制器就可以在UI互動發生時顯示和更新資料,例如按下按鈕以增加 Court-Counter 中的分數。

ViewModels不應該持有 Activities ,Fragments 或者 Context 的引用。

此外,ViewModels也不應包含包含對UI控制器(如Views)引用的元素,因為這將建立對Context的間接引用。

之所以不這樣做是因為,ViewModel 比 UI控制器生命週期長,比如你旋轉一個Activity三次,會得到三個不同的Activity例項,但ViewModel只有一個。

基於這一點,我們來建立 UI控制器/ ViewMode l的關聯。在UI控制器中將 ViewModel 建立為一個成員變數。然後在 onCreate中這樣呼叫:

ViewModelProviders.of(<Your UI controller>).get(<Your ViewModel>.class)
複製程式碼

在 Court-Counter 例子中,會是這樣:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mViewModel = ViewModelProviders.of(this).get(ScoreViewModel.class);
// Other setup code below...
}
複製程式碼

注意: 這裡對 “no contexts in ViewModels” 規則有個例外。有時候你可能會需要一個 Application context(as opposed to an Activity context) 呼叫系統服務。這種情況下在 ViewModel 中持有 Application context 是沒問題的,因為 Application context 是存在於 App 整個生命週期的,這點與 Activity context 不同, Activity context 只存在與 Activity 的生命週期。事實上,如果你需要 Application context,最好繼承 AndroidViewModel ,這是一個持有 Application 引用的 ViewModel。

第三步:在 UI 控制器中使用 ViewModel

要訪問或更改UI資料,可以使用ViewModel中的資料。下面是一個新的 onCreate 方法的示例,以及一個增加 team A 分數的方法:

// The finished onCreate method
@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);
   mViewModel = ViewModelProviders.of(this).get(ScoreViewModel.class);
   displayForTeamA(mViewModel.scoreTeamA);
   displayForTeamB(mViewModel.scoreTeamB);
複製程式碼

}

// An example of both reading and writing to the ViewModel
public void addOneForTeamA(View v) {
    mViewModel.scoreTeamA = mViewModel.scoreTeamA + 1;
    displayForTeamA(mViewModel.scoreTeamA);
}
複製程式碼

tips: ViewModel 也可以很好地與另一個架構元件 LiveData 一起工作,在這個系列中我不會深入探索。使用LiveData 的額外好處是它是可觀察的:它可以在資料改變時觸發UI更新。可以在這裡瞭解更多關於LiveData的資訊。

進一步審視 ViewModelsProviders.of

第一次呼叫 ViewModelProviders.of 方法是在 MainActivity 中,建立了一個新的 ViewModel 例項。每次呼叫 onCreate 方法都會再次呼叫這個方法。它會返回之前 Court-Counter MainActivity 中建立的 ViewModel。 這就是它持有資料的方式。

只有給 UI controller 提供正確的UI控制器作為引數才可以。切記不要在 ViewModel 記憶體儲 UI 控制器,ViewModel 會在後臺跟蹤 UI 控制器例項和 ViewModel 之間的關聯。

ViewModelProviders._of_(**<THIS ARGUMENT>**).get(ScoreViewModel.**class**);
複製程式碼

這可以讓你有一個應用程式,開啟同一個 Activity or Fragment 的不同例項,但具有顯示不同的 ViewModel 資訊。讓我們想象一下,如果我們擴充套件 Court-Counter 程式,使其可以支援不同的籃球比賽得分。比賽呈現在列表裡,然後點選列表中的比賽就會開啟一屏與 MainActivity 一樣的畫面,後面我就叫它 GameScoreActivity。

對於你開啟的每一個不同的比賽畫面,在 onCreate 中關聯ViewModel和GameScoreActivity 後,它將建立不同的 ViewModel 例項。旋轉其中一個螢幕,則保持與同一個ViewModel的連線。

ViewModels 簡單入門
所有這些邏輯都是通過調ViewModelProviders.of(<Your UI controller>).get(<Your ViewModel>.class) 實現的。 你只需要傳遞正確的UI 控制器例項就好。

最後的思考: ViewModel非常好的把你的UI控制器程式碼與UI的資料分離出來。 這就是說,它並不是能完成資料持久化和儲存App 狀態的工作。 在下一篇文章中,我將探討Activity生命週期與ViewModels之間的微妙互動,以及 ViewModel 與 onSaveInstanceState 進行比較。

結論和進一步的學習

在這篇文章中,我探索了新的ViewModel類的基礎知識。關鍵要點是:

  • ViewModel類旨在一個連續的生命週期中儲存和管理與UI相關的資料。這使得資料可以在螢幕旋轉等配置變化的情況下得以儲存。
  • ViewModels將UI實現與 App 資料分離開來。
  • 一般來說,如果某屏應用中有瞬態資料,則應該為該屏的資料建立一個單獨的ViewModel。
  • ViewModel的生命週期從關聯的UI控制器首次建立時開始,直到完全銷燬。
  • 不要將UI控制器或 Context 直接或間接儲存在ViewModel中。這包括在ViewModel中儲存 View。對UI控制器的直接或間接引用違背了從資料中分離UI的目的,並可能導致記憶體洩漏。
  • ViewModel物件通常會儲存LiveData物件,您可以在 這裡瞭解更多。
  • ViewModelProviders.of 方法通過作為引數傳入的 UI控制器與 ViewModel 進行關聯。

想要了解更多 ViewModel 化的好處? 可以進一步閱讀下面文章:

感謝 Mark Lu, Florina Muntenescu, 以及 Daniel Galpin.


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博知乎專欄