Android Kotlin 基礎知識 05.1:ViewModel 和 ViewModelFactory

這個程式碼研究室是 Android Kotlin 基礎知識課程的一部分。如果您按部就班完成程式碼研究室,就能充分體驗到本課程的價值。所有課程程式碼研究室都列在 Android Kotlin 基礎知識程式碼研究室到達網頁

標題畫面

遊戲畫面

分數畫面

簡介

在本程式碼研究室中,您將瞭解 Android 架構元件之一的 ViewModel

  • 您可以使用 ViewModel 類別,以注重生命週期的方式儲存及管理 UI 相關資料。ViewModel 類別可在裝置設定變更時保留資料,例如螢幕旋轉和鍵盤可用性變更。
  • 您可以使用 ViewModelFactory 類別,例項化並傳回在設定變更後仍然有效的 ViewModel 物件。

必備知識

  • 如何使用 Kotlin 建構基本 Android 應用程式。
  • 如何使用導覽圖在應用程式中實作導覽功能。
  • 如何新增程式碼,在應用程式的目的地之間導覽,以及在導覽目的地之間傳遞資料。
  • 活動和片段生命週期的運作方式。
  • 如何將記錄資訊新增至應用程式,並在 Android Studio 中使用 Logcat 讀取記錄。

課程內容

學習內容

  • 在應用程式中新增 ViewModel,儲存應用程式資料,確保資料在設定變更後仍然有效。
  • 使用 ViewModelFactory 和工廠方法設計模式,以建構函式參數例項化 ViewModel 物件。

在第 5 課的程式碼研究室中,您將從範例程式碼開始開發 GuessTheWord 應用程式。GuessTheWord 是雙人比手畫腳遊戲,玩家要彼此合作,盡可能獲得最高分。

第一位玩家看著應用程式中的字詞,輪流表演每個字詞,但要確保第二位玩家看不到字詞。第二位玩家嘗試猜測字詞。

如要開始遊戲,第一位玩家要在裝置上開啟應用程式,並看到一個字詞,例如「吉他」,如下方螢幕截圖所示。

第一位玩家要表演這個字詞,但不能說出該字詞。

  • 第二位玩家猜對字詞後,第一位玩家會按下「Got It」按鈕,計數會增加一,並顯示下一個字詞。
  • 如果第二位玩家猜不出單字,第一位玩家可以按下「略過」按鈕,這樣一來,計數就會減少 1,並跳到下一個單字。
  • 如要結束遊戲,請按下「結束遊戲」按鈕。(這項功能不包含在系列第一個程式碼研究室的範例程式碼中)。

在這項工作中,您將下載並執行範例應用程式,然後檢查程式碼。

步驟 1:開始使用

  1. 下載 GuessTheWord 範例程式碼,並在 Android Studio 中開啟專案。
  2. 在 Android 裝置或模擬器上執行應用程式。
  3. 輕觸按鈕。請注意,「略過」按鈕會顯示下一個字詞,並將分數減少 1 分;「答對了」按鈕會顯示下一個字詞,並將分數增加 1 分。「End Game」按鈕尚未實作,因此輕觸該按鈕不會有任何反應。

步驟 2:逐步檢查程式碼

  1. 在 Android Studio 中瀏覽程式碼,瞭解應用程式的運作方式。
  2. 請務必查看下列檔案,這些檔案特別重要。

MainActivity.kt

這個檔案只包含範本產生的預設程式碼。

res/layout/main_activity.xml

這個檔案包含應用程式的主要版面配置。使用者瀏覽應用程式時,NavHostFragment 會代管其他片段。

UI 片段

範例程式碼在 com.example.android.guesstheword.screens 套件下有三個不同套件中的三個片段:

  • title/TitleFragment,適用於標題畫面
  • game/GameFragment 適用於遊戲畫面
  • score/ScoreFragment,用於顯示分數畫面

screens/title/TitleFragment.kt

應用程式啟動時,系統會顯示標題片段,點按處理常式會設為「Play」按鈕,以便導覽至遊戲畫面。

screens/game/GameFragment.kt

這是主要片段,也是大部分遊戲動作發生處:

  • 變數是根據目前字詞和目前分數所定義。
  • resetList() 方法中定義的 wordList 是遊戲中使用的字詞範例清單。
  • onSkip() 方法是「Skip」按鈕的點擊處理常式。將分數減 1,然後使用 nextWord() 方法顯示下一個字。
  • onCorrect() 方法是「我知道了」按鈕的點擊處理常式。這個方法的實作方式與 onSkip() 方法類似。唯一的差別在於這個方法會將分數加 1,而不是減 1。

screens/score/ScoreFragment.kt

ScoreFragment 是遊戲的最後一個畫面,會顯示玩家的最終分數。在本程式碼研究室中,您將新增實作項目,顯示這個畫面並顯示最終得分。

res/navigation/main_navigation.xml

導覽圖顯示片段如何透過導覽功能連結:

  • 使用者可以從標題片段前往遊戲片段。
  • 使用者可以從遊戲片段前往分數片段。
  • 使用者可以從分數片段返回遊戲片段。

在這項工作中,您會找出 GuessTheWord 範例應用程式的問題。

  1. 執行範例程式碼,透過幾個字詞進行遊戲,並在每個字詞後輕觸「略過」或「我知道了」。
  2. 遊戲畫面現在會顯示一個字詞和目前的分數。旋轉裝置或模擬器變更螢幕方向。請注意,目前的分數會消失。
  3. 使用更多字詞進行遊戲。當遊戲畫面顯示分數時,請關閉並重新開啟應用程式。你會發現遊戲會從頭開始,因為應用程式狀態未儲存。
  4. 透過數個字詞進行遊戲,然後輕觸「結束遊戲」按鈕。請注意,畫面不會有任何變化。

應用程式問題:

  • 設定變更時 (例如裝置螢幕方向變更,或應用程式關閉並重新啟動),範例應用程式不會儲存及還原應用程式狀態。
    您可以使用 onSaveInstanceState() 回呼解決這個問題。不過,使用 onSaveInstanceState() 方法時,您必須編寫額外的程式碼將狀態儲存在套件中,並實作邏輯以擷取該狀態。此外,可儲存的資料量極少。
  • 使用者輕觸「End Game」按鈕時,遊戲畫面不會導向分數畫面。

您可以使用在本程式碼研究室所學到的應用程式架構元件來解決這些問題。

應用程式架構

應用程式架構可協助您設計應用程式的類別,以及類別之間的關係,讓程式碼井然有序、在特定情境中運作良好,且易於使用。在本系列四個程式碼研究室中,您對 GuessTheWord 應用程式所做的改善,都遵循 Android 應用程式架構指南,並使用 Android 架構元件。Android 應用程式架構與 MVVM (模型-檢視區塊-檢視區塊模型) 架構模式類似。

GuessTheWord 應用程式遵循關注點分離設計原則,並劃分為多個類別,每個類別負責處理不同的關注點。在本課程的第一個程式碼研究室中,您會使用 UI 控制器、ViewModelViewModelFactory 類別。

UI 控制器

UI 控制器是以 UI 為基礎的類別,例如 ActivityFragment。UI 控制器只能包含處理 UI 和作業系統互動的邏輯,例如顯示檢視畫面和擷取使用者輸入內容。請勿將決策邏輯 (例如決定要顯示的文字) 放入 UI 控制器。

在 GuessTheWord 範例程式碼中,UI 控制器是三個片段:GameFragmentScoreFragment,TitleFragment。遵循「關注點分離」設計原則,GameFragment 只負責在畫面上繪製遊戲元素,以及瞭解使用者何時輕觸按鈕,除此之外一概不負責。使用者輕觸按鈕時,這項資訊會傳遞至 GameViewModel

ViewModel

ViewModel 會存放要顯示在 ViewModel 相關片段或活動中的資料。ViewModel 可以對資料執行簡單的計算和轉換,方便 UI 控制器顯示資料。在這個架構中,ViewModel 會執行決策。

GameViewModel 會保存分數值、字詞清單和目前字詞等資料,因為這些資料會顯示在畫面上。GameViewModel 也包含商業邏輯,可執行簡單的計算,決定資料的目前狀態。

ViewModelFactory

ViewModelFactory 會例項化 ViewModel 物件,可包含或不包含建構函式參數。

在後續的程式碼研究室中,您將瞭解與 UI 控制器和 ViewModel 相關的其他 Android 架構元件。

ViewModel 類別專為儲存及管理 UI 相關資料而設計。在這個應用程式中,每個 ViewModel 都與一個片段相關聯。

在這項工作中,您要在應用程式中新增第一個 ViewModel,也就是 GameFragmentGameViewModel。您也會瞭解 ViewModel 可感知生命週期的意義。

步驟 1:新增 GameViewModel 類別

  1. 開啟 build.gradle(module:app) 檔案,在 dependencies 區塊中,為 ViewModel 新增 Gradle 依附元件。

    如果您使用最新版程式庫,解決方案應用程式應可正常編譯。如果沒有,請嘗試解決問題,或還原至下方顯示的版本。
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
  1. screens/game/ 資料夾中,建立名為 GameViewModel 的新 Kotlin 類別。
  2. GameViewModel 類別擴充抽象類別 ViewModel
  3. 如要進一步瞭解 ViewModel 如何感知生命週期,請新增含有 log 陳述式的 init 區塊。
class GameViewModel : ViewModel() {
   init {
       Log.i("GameViewModel", "GameViewModel created!")
   }
}

步驟 2:覆寫 onCleared() 並新增記錄

在您卸離相關片段或活動完成後,系統會將 ViewModel 刪除。在 ViewModel 刪除之前,系統會呼叫 onCleared() 回呼來清除資源。

  1. GameViewModel 類別中,覆寫 onCleared() 方法。
  2. onCleared() 中新增記錄陳述式,以追蹤 GameViewModel 生命週期。
override fun onCleared() {
   super.onCleared()
   Log.i("GameViewModel", "GameViewModel destroyed!")
}

步驟 3:將 GameViewModel 與遊戲片段建立關聯

ViewModel 必須與 UI 控制器建立關聯。如要建立兩者之間的關聯,請在 UI 控制器內建立 ViewModel 的參照。

在這個步驟中,您會在對應的 UI 控制器 (GameFragment) 中建立 GameViewModel 的參照。

  1. GameFragment 類別中,於頂層新增 GameViewModel 類型的欄位做為類別變數。
private lateinit var viewModel: GameViewModel

步驟 4:初始化 ViewModel

在螢幕旋轉等設定變更期間,系統會重新建立片段等 UI 控制器。不過,ViewModel 例項會繼續存留。如果您使用 ViewModel 類別建立 ViewModel 執行個體,每次重新建立片段時,系統都會建立新物件。請改為使用 ViewModelProvider 建立 ViewModel 執行個體。

ViewModelProvider 的運作方式:

  • ViewModelProvider 會傳回現有的 ViewModel (如有),或建立新的 ViewModel (如尚不存在)。
  • ViewModelProvider 會建立與指定範圍 (活動或片段) 相關聯的 ViewModel 例項。
  • 只要範圍維持有效,系統就會保留建立的 ViewModel。舉例來說,如果範圍是片段,系統會保留 ViewModel,直到片段分離為止。

使用 ViewModelProviders.of() 方法建立 ViewModelProvider,藉此初始化 ViewModel

  1. GameFragment 類別中,初始化 viewModel 變數。將這段程式碼放在 onCreateView() 內,繫結變數定義之後。使用 ViewModelProviders.of() 方法,並傳入相關聯的 GameFragment 內容和 GameViewModel 類別。
  2. ViewModel 物件的初始化上方,新增記錄陳述式,記錄 ViewModelProviders.of() 方法呼叫。
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
  1. 執行應用程式。在 Android Studio 中開啟「Logcat」窗格,然後篩選 Game。輕觸裝置或模擬器上的「Play」按鈕。遊戲畫面隨即開啟。

    如 Logcat 所示,GameFragmentonCreateView() 方法會呼叫 ViewModelProviders.of() 方法來建立 GameViewModel。您新增至 GameFragmentGameViewModel 的記錄陳述式會顯示在 Logcat 中。

  1. 在裝置或模擬器上啟用自動旋轉設定,並變更螢幕方向數次。每次都會刪除並重新建立 GameFragment,因此每次都會呼叫 ViewModelProviders.of()。但 GameViewModel 只會建立一次,而且不會在每次呼叫時重新建立或刪除。
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
  1. 離開遊戲,或離開遊戲片段。GameFragment 已刪除。相關聯的 GameViewModel 也會遭到刪除,並呼叫 onCleared() 回呼。
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel destroyed!

ViewModel 會在設定變更時保留,因此適合存放需要在設定變更後繼續留存的資料:

  • 將要顯示在畫面上的資料,以及處理該資料的程式碼,放在 ViewModel 中。
  • ViewModel 不得包含片段、活動或檢視區塊的參照,因為活動、片段和檢視區塊在設定變更後會失效。

為了進行比較,以下說明在您新增 ViewModel 前後,啟動器應用程式如何處理 GameFragment UI 資料:ViewModel

  • 新增 ViewModel 前:
    應用程式進行設定變更 (例如螢幕旋轉) 時,遊戲片段會遭到刪除並重新建立。資料會遺失。
  • 新增 ViewModel 並將遊戲片段的 UI 資料移至 ViewModel 後:片段需要顯示的所有資料現在都是 ViewModel
    應用程式經過設定變更後,ViewModel 會保留下來,資料也會一併保留。

在這項工作中,您要將應用程式的 UI 資料移至 GameViewModel 類別,以及處理資料的方法。這樣做是為了在設定變更期間保留資料。

步驟 1:將資料欄位和資料處理作業移至 ViewModel

將下列資料欄位和方法從 GameFragment 移至 GameViewModel

  1. 移動 wordscorewordList 資料欄位。請確認 wordscore 不是 private

    請勿移動繫結變數 GameFragmentBinding,因為其中包含對檢視區塊的參照。這個變數用於擴充版面配置、設定點擊事件監聽器,以及在畫面上顯示資料,這些都是片段的責任。
  2. 移動 resetList()nextWord() 方法。這些方法會決定要在畫面上顯示的字詞。
  3. onCreateView() 方法中,將 resetList()nextWord() 的方法呼叫移至 GameViewModelinit 區塊。

    這些方法必須位於 init 區塊中,因為您應該在建立 ViewModel 時重設字詞清單,而不是每次建立片段時都重設。您可以刪除 GameFragmentinit 區塊中的記錄陳述式。

GameFragment 中的 onSkip()onCorrect() 點擊事件處理常式包含處理資料和更新 UI 的程式碼。更新 UI 的程式碼應保留在片段中,但處理資料的程式碼需要移至 ViewModel

目前請在兩個位置加入相同的方法:

  1. GameFragment 中的 onSkip()onCorrect() 方法複製到 GameViewModel
  2. GameViewModel 中,請確認 onSkip()onCorrect() 方法不是 private,因為您會從片段參照這些方法。

以下是重構後的 GameViewModel 類別程式碼:

class GameViewModel : ViewModel() {
   // The current word
   var word = ""
   // The current score
   var score = 0
   // The list of words - the front of the list is the next word to guess
   private lateinit var wordList: MutableList<String>

   /**
    * Resets the list of words and randomizes the order
    */
   private fun resetList() {
       wordList = mutableListOf(
               "queen",
               "hospital",
               "basketball",
               "cat",
               "change",
               "snail",
               "soup",
               "calendar",
               "sad",
               "desk",
               "guitar",
               "home",
               "railway",
               "zebra",
               "jelly",
               "car",
               "crow",
               "trade",
               "bag",
               "roll",
               "bubble"
       )
       wordList.shuffle()
   }

   init {
       resetList()
       nextWord()
       Log.i("GameViewModel", "GameViewModel created!")
   }
   /**
    * Moves to the next word in the list
    */
   private fun nextWord() {
       if (!wordList.isEmpty()) {
           //Select and remove a word from the list
           word = wordList.removeAt(0)
       }
       updateWordText()
       updateScoreText()
   }
 /** Methods for buttons presses **/
   fun onSkip() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   fun onCorrect() {
       if (!wordList.isEmpty()) {
           score++
       }
       nextWord()
   }

   override fun onCleared() {
       super.onCleared()
       Log.i("GameViewModel", "GameViewModel destroyed!")
   }
}

以下是重構後的 GameFragment 類別程式碼:

/**
* Fragment where the game is played
*/
class GameFragment : Fragment() {


   private lateinit var binding: GameFragmentBinding


   private lateinit var viewModel: GameViewModel


   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {

       // Inflate view and obtain an instance of the binding class
       binding = DataBindingUtil.inflate(
               inflater,
               R.layout.game_fragment,
               container,
               false
       )

       Log.i("GameFragment", "Called ViewModelProviders.of")
       viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)

       binding.correctButton.setOnClickListener { onCorrect() }
       binding.skipButton.setOnClickListener { onSkip() }
       updateScoreText()
       updateWordText()
       return binding.root

   }


   /** Methods for button click handlers **/

   private fun onSkip() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   private fun onCorrect() {
       if (!wordList.isEmpty()) {
           score++
       }
       nextWord()
   }


   /** Methods for updating the UI **/

   private fun updateWordText() {
       binding.wordText.text = word
   }

   private fun updateScoreText() {
       binding.scoreText.text = score.toString()
   }
}

步驟 2:更新 GameFragment 中對點擊事件處理常式和資料欄位的參照

  1. GameFragment 中,更新 onSkip()onCorrect() 方法。移除更新分數的程式碼,改為呼叫 viewModel 中對應的 onSkip()onCorrect() 方法。
  2. 由於您已將 nextWord() 方法移至 ViewModel,遊戲片段無法再存取該方法。

    GameFragmentonSkip()onCorrect() 方法中,將對 nextWord() 的呼叫替換為 updateScoreText()updateWordText()。這些方法會在畫面上顯示資料。
private fun onSkip() {
   viewModel.onSkip()
   updateWordText()
   updateScoreText()
}
private fun onCorrect() {
   viewModel.onCorrect()
   updateScoreText()
   updateWordText()
}
  1. GameFragment 中,將 scoreword 變數更新為使用 GameViewModel 變數,因為這些變數現在位於 GameViewModel 中。
private fun updateWordText() {
   binding.wordText.text = viewModel.word
}

private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.toString()
}
  1. GameViewModelnextWord() 方法中,移除對 updateWordText()updateScoreText() 方法的呼叫。這些方法現在會從 GameFragment 呼叫。
  2. 建構應用程式,確保沒有錯誤。如有錯誤,請清除專案再重新建構。
  3. 執行應用程式,使用一些字詞進行遊戲。在遊戲畫面中旋轉裝置,請注意,螢幕方向變更後,目前的分數和字詞會保留。

做得好!現在所有應用程式資料都會儲存在 ViewModel 中,因此在設定變更期間會保留下來。

在這項工作中,您要實作「End Game」按鈕的點擊事件監聽器。

  1. GameFragment 中,新增名為 onEndGame() 的方法。使用者輕觸「End Game」按鈕時,系統會呼叫 onEndGame() 方法。
private fun onEndGame() {
   }
  1. GameFragmentonCreateView() 方法中,找出為「Got It」和「Skip」按鈕設定點擊事件監聽器的程式碼。在這兩行下方,為「End Game」按鈕設定點選監聽器。使用繫結變數 binding。在點擊事件監聽器內,呼叫 onEndGame() 方法。
binding.endGameButton.setOnClickListener { onEndGame() }
  1. GameFragment 中,新增名為 gameFinished() 的方法,將應用程式導覽至分數畫面。使用 Safe Args 將分數當做引數傳遞。
/**
* Called when the game is finished
*/
private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score
   NavHostFragment.findNavController(this).navigate(action)
}
  1. onEndGame() 方法中,呼叫 gameFinished() 方法。
private fun onEndGame() {
   gameFinished()
}
  1. 執行應用程式並進行遊戲,然後循環瀏覽一些字詞。輕觸「結束遊戲」 按鈕。請注意,應用程式會導覽至分數畫面,但不會顯示最終分數。您將在下一個工作修正此問題。

使用者結束遊戲時,ScoreFragment 不會顯示分數。您希望 ViewModel 保留要由 ScoreFragment 顯示的分數。您將使用工廠方法模式,在 ViewModel 初始化期間傳遞分數值。

工廠方法模式是一種建立設計模式,會使用工廠方法建立物件。工廠方法會傳回相同類別的例項。

在這項工作中,您要建立 ViewModel,其中包含分數片段的參數化建構函式,以及用於例項化 ViewModel 的工廠方法。

  1. score 套件下,建立名為 ScoreViewModel 的新 Kotlin 類別。這個類別將做為分數片段的 ViewModel
  2. ViewModel. 擴充 ScoreViewModel 類別,並新增最終分數的建構函式參數。新增含有記錄陳述式的 init 區塊。
  3. ScoreViewModel 類別中,新增名為 score 的變數,以儲存最終分數。
class ScoreViewModel(finalScore: Int) : ViewModel() {
   // The final score
   var score = finalScore
   init {
       Log.i("ScoreViewModel", "Final score is $finalScore")
   }
}
  1. score 套件下,建立另一個名為 ScoreViewModelFactory 的 Kotlin 類別。這個類別負責例項化 ScoreViewModel 物件。
  2. ViewModelProvider.Factory 擴充 ScoreViewModelFactory 類別。新增最終分數的建構函式參數。
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
  1. ScoreViewModelFactory 中,Android Studio 會顯示有關未實作抽象成員的錯誤。如要解決錯誤,請覆寫 create() 方法。在 create() 方法中,傳回新建立的 ScoreViewModel 物件。
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) {
       return ScoreViewModel(finalScore) as T
   }
   throw IllegalArgumentException("Unknown ViewModel class")
}
  1. ScoreFragment 中,為 ScoreViewModelScoreViewModelFactory 建立類別變數。
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
  1. ScoreFragmentonCreateView() 中,初始化 binding 變數後,初始化 viewModelFactory。使用 ScoreViewModelFactory。將引數套件中的最終分數做為建構函式參數,傳遞至 ScoreViewModelFactory()
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
  1. onCreateView( 中,初始化 viewModelFactory 後,請初始化 viewModel 物件。呼叫 ViewModelProviders.of() 方法,傳入相關聯的分數片段內容和 viewModelFactory。這會使用 viewModelFactory 類別中定義的工廠方法建立 ScoreViewModel 物件.
viewModel = ViewModelProviders.of(this, viewModelFactory)
       .get(ScoreViewModel::class.java)
  1. onCreateView() 方法中,初始化 viewModel 後,將 scoreText 檢視區塊的文字設為 ScoreViewModel 中定義的最終分數。
binding.scoreText.text = viewModel.score.toString()
  1. 執行應用程式,然後進行遊戲。瀏覽部分或所有字詞,然後輕觸「結束遊戲」。請注意,分數片段現在會顯示最終分數。

  1. 選用:在 Logcat 中依 ScoreViewModel 篩選,檢查 ScoreViewModel 記錄。系統應會顯示分數值。
2019-02-07 10:50:18.328 com.example.android.guesstheword I/ScoreViewModel: Final score is 15

在這項工作中,您實作了 ScoreFragment,以便使用 ViewModel。您也學會如何使用 ViewModelFactory 介面,為 ViewModel 建立參數化建構函式。

恭喜!您已變更應用程式的架構,改用 Android 架構元件 ViewModel。您已解決應用程式的生命週期問題,現在遊戲資料在設定變更後仍可保留。您也學會如何使用 ViewModelFactory 介面,建立用於建立 ViewModel 的參數化建構函式。

Android Studio 專案:GuessTheWord

  • Android 應用程式架構指南建議將具有不同責任的類別分離。
  • UI 控制器是以 UI 為基礎的類別,例如 ActivityFragment。UI 控制器只能包含處理 UI 和作業系統互動的邏輯,不應包含要在 UI 中顯示的資料。將該資料存放在 ViewModel 中。
  • ViewModel 類別會儲存和管理 UI 相關資料。ViewModel 類別可在螢幕旋轉等變更時保留資料。
  • ViewModel 是建議使用的 Android 架構元件之一。
  • ViewModelProvider.Factory 是可用於建立 ViewModel 物件的介面。

下表比較 UI 控制器與為其保留資料的 ViewModel 例項:

UI 控制器

ViewModel

您在本程式碼研究室中建立的 ScoreFragment 就是 UI 控制器的範例。

您在本程式碼研究室中建立的 ScoreViewModel 就是 ViewModel 的範例。

不含任何要在使用者介面中顯示的資料。

內含 UI 控制器在 UI 中顯示的資料。

包含用於顯示資料的程式碼,以及使用者事件程式碼,例如點擊監聽器。

包含資料處理的程式碼。

每次設定變更時都會銷毀並重新建立。

只有在相關聯的 UI 控制器永久消失時才會遭到刪除,例如活動完成時 (如果是活動),或是片段卸離時 (如果是片段)。

包含檢視畫面。

不得包含活動、片段或檢視區塊的參照,因為這些項目不會在設定變更後保留,但 ViewModel 會。

包含相關聯 ViewModel 的參照。

不包含任何相關聯的 UI 控制器參照。

Udacity 課程:

Android 開發人員說明文件:

其他:

本節列出的作業可由課程講師指派給學習本程式碼研究室的學員。講師可自由採取以下行動:

  • 視需要指派作業。
  • 告知學員如何繳交作業。
  • 為作業評分。

講師可以視需求使用全部或部分建議內容,也可以自由指派任何其他合適的作業。

如果您是自行學習本程式碼研究室,不妨利用這些作業驗收學習成果。

回答問題

第 1 題

為了避免在裝置設定變更期間遺失資料,你應該在哪個類別之中儲存應用程式資料?

  • ViewModel
  • LiveData
  • Fragment
  • Activity

第 2 題

ViewModel 不可內含任何片段、活動或檢視的參照。下列敘述是否正確?

  • 錯誤

第 3 題

ViewModel 會在何時遭到刪除?

  • 相關聯的 UI 控制器在裝置螢幕方向變更時遭到刪除並重新建立時。
  • 螢幕方向改變時。
  • 做為活動或片段的相關聯 UI 控制器完成或卸離時。
  • 當使用者按下返回按鈕時。

第 4 題

ViewModelFactory 介面的用途為何?

  • 建立 ViewModel 物件的例項。
  • 在變更螢幕方向期間保留資料。
  • 重新整理顯示於螢幕上的資料。
  • 在應用程式資料變更時收到通知。

開始下一個課程:5.2:LiveData 與 LiveData 觀察者

如要查看本課程其他程式碼研究室的連結,請參閱 Android Kotlin 基礎知識程式碼研究室登陸頁面