[譯]ViewModels:一個簡單的示例

ronaldong發表於2018-05-31

引言

大約兩年前,我在傳授Android for Beginners課程,這是一門讓零程式設計基礎的學生學習如何編寫第一個Android應用程式的課程。作為課程的一部分,學生們將開發一個非常簡單的名為Court-Counter的應用程式。

Court-Counter是一個非常簡單的應用程式,只有一個介面,裡面提供了一些按鈕用於修改籃球比賽的比分。學生們最終完成的應用程式有都存在一個bug: 如果旋轉手機的螢幕,應用程式介面上的當前比分將莫名其妙地丟失。

[譯]ViewModels:一個簡單的示例

這是怎麼回事?旋轉裝置的螢幕是應用程式在其生命週期中可能經歷的一些configuration changes中的一種,其它還包括等改裝置的語言等。 所有這些configuration changes都會導致Activity被銷燬並重新建立。 Android系統的這種機制可以讓我們做一些有趣的事情,比如在裝置旋轉的時候使用橫向佈局,但它卻可能會讓Android開發新手頭疼不已。

在2017年Google I / O大會上,Android Framework團隊推出了一套新的Architecture Components,其中的一個元件ViewModel就是用來處理這個螢幕的旋轉問題。

ViewModel 類旨在以一種能夠感知生命週期的方式來儲存和管理與UI相關的資料,這使得資料能夠在configuration changes(如螢幕旋轉)的時候不會丟失。

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

  • 解釋ViewModel所能滿足的基本需求
  • 使用ViewModel來重構Court-Counter的程式碼從而解決螢幕旋轉問題
  • 深入研究ViewModel和UI元件之間的關係

根本的問題

問題的根本原因在於Activity的生命週期有很多不同的狀態,並且由於configuration changes,一個Activity可能會多次經歷這些不同的狀態。

[譯]ViewModels:一個簡單的示例

當一個Activity正在經歷所有的這些狀態時,您可能還需要在記憶體中儲存一些UI的臨時資料。我將UI的臨時資料定義為UI所需的資料。它包括使用者輸入的資料,應用在執行時生成的資料或者是從資料庫載入的資料。這些資料可能是點陣圖影象,RecyclerView所需的物件列表,或者是本文中提到的籃球得分。

ViewModel出現之前,在configuration changes的時候您可能會使用onRetainNonConfigurationInstance方法來儲存此資料,並使用getLastNonConfigurationInstance方法來取出這些資料。但是如果你的資料不需要知道Activity正處於處生命週期的哪種狀態,它會不會無限膨脹?如果這些資料不是像Activity的變數scoreTeamA那樣,與Activity的生命週期緊密相,而是儲存在Activity之外的其他位置,該怎麼辦? 這正是ViewModel類存在的意義。

在下面的圖表中,您可以看到一個Activity的生命週期,該Activity經歷了一次螢幕旋轉,然後最終被finish。ViewModel的生命週期顯示在相對應的Activity生命週期的旁邊。 請注意,ViewModels可以很方便的用在Fragment和Activity裡,我將稱其為UI controllers。本文重點介紹的是在Activity裡如何使用ViewMode。

[譯]ViewModels:一個簡單的示例
從你第一次請求ViewModel(通常在onCreate Activity中)開始到Activity最終被銷燬,ViewModel會一直存在。 onCreate可能會在Activity的生命週期中多次呼叫,例如裝置的螢幕發生旋轉,但ViewModel還是同一個ViewModel。

一個很簡單的示例

ViewModel的使用可以分為一下三個步驟:

  1. 建立一個繼承ViewModel的類,將資料從UI controllers中分離出來。
  2. 將ViewModel和你的UI controllers關聯起來。
  3. 在您的UI controllers中使用ViewModel。

Step 1: 建立一個ViewModel類

一般來說,您需要為您應用中的每個介面建立一個ViewModel類。 這個ViewModel類將儲存與介面相關的所有資料,併為儲存的資料提供getter和setter方法。這樣就將使用者介面(在Activity和Fragment中實現)中需要顯示的資料從UI controllers中分離出來,現在該資料位於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 controllers和ViewModel關聯起來

你的UI controllers(這裡是指Activity或Fragment)需要知道你的ViewModel,因為使用者在與UI發生互動的時候需要顯示資料和更新資料,例如按下按鈕以增加Court-Counter計數器中的團隊得分。 ViewModels不應該包含Activity、Fragment或Context的引用。此外,ViewModels還不應包含對UI controllers中的變數(如Views)的引用,因為這將建立對Context的間接引用。

不要在ViewModels裡儲存這些物件的原因是ViewModels是獨立於你的UI controllers例項之外的- 如果你三次旋轉一個Activity的螢幕方向,那麼系統會建立三個不同的Activity例項,但你只有一個ViewModel。

考慮到這一點,我們需要實現這個UI controller 與 ViewModel之間關聯。 您需要在UI controller中為您的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...
}
複製程式碼

注意: “ViewModels中不應包含Context”這個原則有一個例外情況。 有時您可能需要Application context (而不是Activity context )以獲取諸如系統服務之類的東西。將Application context儲存在ViewModel中是可以的,因為Application context是與應用程式的生命週期相關聯的。這與Activity context不同,後者與Activity的生命週期相關聯。事實上,如果你需要一個Application context,你應該繼承AndroidViewModel類,它是一個包含Application context的ViewModel。

Step 3: 在你的UI Controller中使用ViewModel

現在你可以在ViewModel中獲取或更改UI中資料了。 下面是一個新的onCreate方法的示例:

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

複製程式碼

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

ViewModelProviders.of的原理

當MainActivity第一次呼叫ViewModelProviders.of方法的時候,它將建立一個新的ViewModel例項。 之後MainActivity裡的onCreate()方法每一次被呼叫的時候,ViewModelProviders.of也同樣會被呼叫,但是它將返回與MainActivity相關聯的預先存在的ViewModel,這就是ViewModel可以儲存資料的原因。

前提條件是你必須傳入正確的UI controller來作為ViewModelProviders.of的第一個引數。 雖然你不應該在ViewModel中儲存UI controller,但ViewModel類會使用您傳入的UI controller作為第一個引數來跟蹤ViewModel和UI controller之間的關聯關係。

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

ViewModelProviders.of使得你的應用可以開啟同一Activity或Fragment的不同例項,但ViewModel中卻儲存著不同的資訊。

我們可以把Court-Counter擴充套件一下,使它能記錄和顯示多場籃球比賽的分數。比賽以列表形式呈現,然後點選列表中的某一場比賽會開啟一個看起來像我們當前的MainActivity的介面,這裡我稱之為GameScoreActivity。

對於您開啟的每場比賽所對應的GameScoreActivity,如果在GameScoreActivity的onCreate方法將其與ViewModel關聯起來,它將建立一個不同的ViewModel例項。如果旋轉其中一個介面的螢幕,則保持與同一ViewModel的連線。

[譯]ViewModels:一個簡單的示例

所有這些邏輯都是通過呼叫ViewModelProviders.of(Your UI controller)get(Your ViewModel.class)方法來完成的。 所以只要你傳入一個UI控制器的正確例項,它就可以正常工作。

最後我想說:ViewModels真的很好,可以將填充資料到檢視的邏輯從UI controller分離出來。這意味著,它並不是一種資料持久化和儲存應用程式狀態的解決方案。在下一篇文章中,我將研究Activity生命週期與ViewModels之間的互動,並將ViewModel與onSaveInstanceState進行比較。

結束語

在這篇文章中,我介紹了一些關於ViewModel類的基礎知識。關鍵要點是:

  • ViewModel的目標是以一種能夠感知生命週期的方式來儲存和管理與UI相關的資料,這使得資料能夠在configuration changes(如螢幕旋轉)的時候不會丟失。
  • ViewModels實現了UI與資料的分離。
  • 一般來說,如果您應用中的介面有臨時資料,你應該為該介面上的資料建立一個單獨的ViewModel。
  • ViewModel的生命週期從首次建立UI controller與ViewModel的關聯開始,直至UI controller被完全銷燬。
  • 切勿將UI controller或Context直接或間接的儲存在ViewModel中,包括在ViewModel中儲存View。直接或間接的引用UI controller違背了將UI與資料分離的目的,並可能導致記憶體洩漏。
  • ViewModel物件通常會儲存LiveData物件,您可以在這裡瞭解更多資訊。
  • ViewModelProviders.of方法通過它的引數(傳入的UI controller)來跟蹤它與對應的UI controller之間的關聯關係。

相關文章