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

引言

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

Court-Counter是一個很是簡單的應用程序,只有一個界面,裏面提供了一些按鈕用於修改籃球比賽的比分。學生們最終完成的應用程序有都存在一個bug: 若是旋轉手機的屏幕,應用程序界面上的當前比分將莫名其妙地丟失。java

這是怎麼回事?旋轉設備的屏幕是應用程序在其生命週期中可能經歷的一些configuration changes中的一種,其它還包括等改設備的語言等。 全部這些configuration changes都會致使Activity被銷燬並從新建立。 Android系統的這種機制可讓咱們作一些有趣的事情,好比在設備旋轉的時候使用橫向佈局,但它卻可能會讓Android開發新手頭疼不已。android

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

ViewModel 類旨在以一種可以感知生命週期的方式來保存和管理與UI相關的數據,這使得數據可以在configuration changes(如屏幕旋轉)的時候不會丟失。github

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

  • 解釋ViewModel所能知足的基本需求
  • 使用ViewModel來重構Court-Counter的代碼從而解決屏幕旋轉問題
  • 深刻研究ViewModel和UI組件之間的關係

根本的問題

問題的根本緣由在於Activity的生命週期有不少不一樣的狀態,而且因爲configuration changes,一個Activity可能會屢次經歷這些不一樣的狀態。編程

當一個Activity正在經歷全部的這些狀態時,您可能還須要在內存中保存一些UI的臨時數據。我將UI的臨時數據定義爲UI所需的數據。它包括用戶輸入的數據,應用在運行時生成的數據或者是從數據庫加載的數據。這些數據多是位圖圖像,RecyclerView所需的對象列表,或者是本文中提到的籃球得分。bash

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

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

從你第一次請求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的鏈接。

全部這些邏輯都是經過調用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之間的關聯關係。
相關文章
相關標籤/搜索