Android Kotlin 基礎課程 05.1:ViewModel 和 ViewModelFactory

本程式碼研究室是 Android Kotlin 基礎課程的一部分。使用程式碼研究室逐步完成程式碼課程後,您將能充分發揮本課程的潛能。所有課程程式碼研究室清單均列於 Android Kotlin 基礎程式碼研究室到達網頁

標題畫面

遊戲畫面

分數畫面

引言

在本程式碼研究室中,您可以瞭解其中一種 Android 架構元件 ViewModel

  • 只要使用 ViewModel 類別,就能以易於使用的方式儲存及管理 UI 相關資料。ViewModel 類別可讓資料繼續套用裝置設定變更,例如旋轉或調整鍵盤可用性。
  • 您可以使用 ViewModelFactory 類別執行個體化並傳回在設定變更後繼續運作的 ViewModel 物件。

須知事項

  • 如何在 Kotlin 中建立基本 Android 應用程式。
  • 如何使用導覽圖表在應用程式中導入導覽功能。
  • 如何新增程式碼在應用程式目的地之間瀏覽,並在導航目的地之間傳送資料。
  • 活動和片段生命週期的運作方式。
  • 如何在 Android Studio 中新增紀錄資訊,並使用 Logcat 來讀取紀錄。

課程內容

  • 如何使用建議的 Android 應用程式架構
  • 如何在您的應用程式中使用 LifecycleViewModelViewModelFactory 課程。
  • 如何透過裝置設定變更來保留 UI 資料。
  • 瞭解工廠方法的設計模式和使用方式。
  • 如何使用介面 ViewModelProvider.Factory 建立 ViewModel 物件。

執行步驟

  • 在應用程式中加入 ViewModel 以儲存應用程式的資料,以便資料在設定變更後繼續保留。
  • 使用 ViewModelFactory 和工廠方法設計模式,以建構函式參數為 ViewModel 物件執行個體化。

在第 5 課的程式碼研究室中,您開發了 GuessTheWord 應用程式,從入門程式碼開始著手。GuessTheWord 是一款雙人「角色」風格的遊戲,玩家透過協作的方式取得最高分。

第一位玩家會查看應用程式中的字詞,並逐一執行每個動作,這樣使用者就不會看到這個單字。第二位玩家會嘗試猜測字詞。

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

第一個玩家會說出字詞,請小心不要實際說出這個字。

  • 當第二位玩家正確猜出字時,第一位玩家按下 [我知道了] 按鈕,這會增加一個數字,並顯示下一個字詞。
  • 如果第二名玩家猜到字詞,第一個玩家按下 [略過] 按鈕可將音量減少 1,直接跳到下一個字詞。
  • 如要結束遊戲,請按下 [結束遊戲] 按鈕。(該系列中第一個程式碼研究室的範例程式碼並未加入這項功能)。

在這項工作中,您將下載並執行啟動應用程式,並檢查程式碼。

步驟 1:開始使用

  1. 下載 GuessTheWord 範例程式碼,然後在 Android Studio 中開啟專案。
  2. 在 Android 裝置或模擬器上執行應用程式。
  3. 輕觸按鈕。請注意,[略過] 按鈕可顯示下一個字詞並減少 1 個分數;「我知道了」按鈕則顯示下一個字詞並增加 1 個分數。系統並未提供 [結束遊戲] 按鈕,因此當您輕觸按鈕時,系統也不會顯示任何動作。

步驟 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

Screen/title/TitleFragment.kt

標題片段是指應用程式啟動時,第一個顯示的畫面。點擊處理常式設定為 [播放] 按鈕,即可前往遊戲畫面。

畫面/遊戲/GameFragment.kt

這是主要片段,其中大部分動作都是發生在遊戲中:

  • 變數是針對目前字詞和目前分數所定義。
  • resetList() 方法中定義的 wordList 是遊戲中所使用的字詞範例清單。
  • onSkip() 方法是 [略過] 按鈕的點擊處理常式。這會將分數降低 1 分,然後使用 nextWord() 方法顯示下一個字詞。
  • onCorrect() 方法是 Got It 按鈕的點擊處理常式。此方法與 onSkip() 方法類似。唯一的差別在於此方法會為分數加上 1,而非減去。

Screen/score/ScoreFragment.kt

ScoreFragment 是遊戲的最終畫面,會顯示玩家的最終得分。在這個程式碼研究室中,您實作了導入這個畫面並顯示最終分數。

res/navigation/main_navigation.xml

導覽圖表顯示透過導覽方式連結片段的方式:

  • 使用者可以透過標題片段前往遊戲片段。
  • 使用者可在遊戲片段中瀏覽至分數片段。
  • 使用者可以在分數片段中返回遊戲片段。

在這項工作中,您會找到關於 GuessTheWord 入門應用程式的問題。

  1. 執行範例程式碼,然後透過幾個字詞玩遊戲,接著輕觸每個字詞後輕觸 [略過] 或 [我知道了]
  2. 遊戲畫面現在會顯示字詞和目前的分數。旋轉裝置或模擬器變更螢幕方向。請注意,目前的分數遺失。
  3. 用幾個字來玩遊戲。如果遊戲畫面顯示分數,請關閉應用程式再重新開啟。請注意,由於應用程式的狀態尚未儲存,因此遊戲從頭重新開始。
  4. 透過幾個字玩遊戲,然後輕觸 [結束遊戲] 按鈕。請注意,這不會發生任何變化。

應用程式問題:

  • 此外,在設定變更期間 (例如裝置螢幕方向改變,或應用程式關閉及重新啟動時),啟動應用程式並不會儲存及還原應用程式狀態。
    如要解決這個問題,可以使用 onSaveInstanceState() 回呼。不過,使用 onSaveInstanceState() 方法時,您必須撰寫額外的程式碼來儲存組合中的狀態,並實作邏輯來擷取狀態。此外,可儲存的資料量最少。
  • 使用者輕觸 [結束遊戲] 按鈕時,遊戲畫面不會瀏覽至分數畫面。

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

應用程式架構

應用程式架構是一種設計應用程式的設計方法,並且讓各類別間的關係,讓程式碼有條不紊、在特定情況下運作良好,而且易於使用。在這組四個程式碼研究室中,您對 GuessTheWord 應用程式所做的改善,都遵循 Android 應用程式架構規範,而且您使用了 Android 架構元件。Android 應用程式架構與 MVVM (模型檢視模型) 架構模式類似。

GuessTheWord 應用程式按照疑慮區隔設計原則來分為不同類別,並分成兩類,分屬不同的類別。在本課程的第一堂程式碼研究室中,您參與的課程皆為 UI 控制器、ViewModelViewModelFactory

UI 控制器

UI 控制器是 UI 類別,例如 ActivityFragment。UI 控制器只能包含用來處理 UI 和作業系統互動的邏輯,例如顯示視圖及擷取使用者輸入內容。請勿在 UI 控制器中加入決策邏輯,例如決定顯示文字的邏輯。

在 GuessTheWord 範例程式碼中,使用者介面控制器分為三個片段:GameFragmentScoreFragment,TitleFragment。根據「疑慮分離」的原則,GameFragment 只負責將畫面元素繪製到螢幕,並且瞭解使用者輕觸按鈕的時機,完全不用擔心。使用者輕觸按鈕時,這項資訊會傳送至 GameViewModel

ViewModel

ViewModel 保存資料,以顯示在與 ViewModel 相關的片段或活動中。ViewModel 可對資料執行簡單的計算和轉換工作,讓 UI 控制器能夠顯示資料。在這個架構下,ViewModel 會執行決策。

GameViewModel 會保留分數值、字詞清單和目前字詞等資料,因為這是要在螢幕上顯示的資料。GameViewModel 也包含商業邏輯,能夠執行簡單的計算以決定資料目前的狀態。

ViewModelFactory

ViewModelFactoryViewModel 物件執行個體化 (無論是否有建構函式參數)。

在稍後的程式碼程式碼中,您將瞭解其他與 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 (如果沒有的話),則會建立新的現有金鑰。
  • 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。輕觸裝置或模擬器上的 [播放] 按鈕。遊戲畫面隨即開啟。

    如 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 不應包含片段、活動或檢視的參照,因為活動、片段和檢視不會改變配置的設定。

以下比較 GameFragment 使用者介面資料在加入 ViewModel 前及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. onSkip()onCorrect() 方法從 GameFragment 複製到 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 中,因此在設定變更時,這些資料仍會保留下來。

在這項工作中,您實作了 [結束遊戲] 按鈕的點擊事件監聽器。

  1. GameFragment 中,新增名為 onEndGame() 的方法。使用者輕觸 [結束遊戲] 按鈕時,系統會呼叫 onEndGame() 方法。
private fun onEndGame() {
   }
  1. GameFragmentonCreateView() 方法中,找出用來設定 [我知道了] 和 [略過] 按鈕點擊事件監聽器的程式碼。在這兩行的正下方,設定 [結束遊戲] 按鈕的點擊事件監聽器。使用繫結變數 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 套件下,建立名為另一個 Kotlin 類別並命名為 ScoreViewModelFactory。此類別將負責為 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. ScoreFragment 中,在 onCreateView() 中初始化 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. 選用:透過篩選 ScoreViewModel,檢查 Logcat 中的 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 和作業系統互動行為的邏輯;這類使用者介面不得包含要顯示在使用者介面中的資料。將資料放入 ViewModel
  • ViewModel 類別會儲存及管理 UI 相關資料。ViewModel 類別可讓資料繼續套用設定變更,例如畫面旋轉。
  • ViewModel 是建議的 Android 架構元件之一。
  • ViewModelProvider.Factory 是可用來建立 ViewModel 物件的介面。

下表比較了使用者介面控制器和用來保存資料的 ViewModel 例項:

UI 控制器

ViewModel

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

在這個程式碼研究室中建立的 ScoreViewModelViewModel 的範例。

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

包含 UI 控制器在使用者介面中顯示的資料。

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

包含資料處理的程式碼。

系統會在每項設定變更時刪除並重新建立。

只有在相關 UI 控制器永久消失時 (例如活動、活動結束或片段停止) 才會解除片段的刪除作業。

內含資料檢視。

不得包含活動、片段或視圖的參照,因為它們不會改變設定,但 ViewModel 仍是如此。

包含與關聯 ViewModel 的參照。

未包含任何相關聯的使用者介面控制器的參照。

Udacity 課程:

Android 開發人員說明文件:

其他:

這個部分會列出在代碼研究室中,受老師主導的課程作業的可能學生作業。由老師自行決定要執行下列動作:

  • 視需要指派家庭作業。
  • 告知學生如何提交家庭作業。
  • 批改家庭作業。

老師可視需要使用這些建議,並視情況指派其他合適的家庭作業。

如果您是自行操作本程式碼研究室,歡迎透過這些家庭作業來測試自己的知識。

回答這些問題

第 1 題

為避免在裝置設定變更期間遺失資料,請將應用程式資料儲存到哪個類別?

  • ViewModel
  • LiveData
  • Fragment
  • Activity

第 2 題

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

  • 正確
  • 不正確

第 3 題

何時會刪除 ViewModel

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

第 4 題

ViewModelFactory 介面的用途為何?

  • ViewModel 物件執行個體化。
  • 在螢幕變更時保留資料。
  • 重新整理畫面上顯示的資料。
  • 在應用程式資料變更時收到通知。

開始下一堂課:5.2:LiveData 和 LiveData 觀察員

如要瞭解本課程中其他程式碼研究室的連結,請參閱 Android Kotlin 基礎程式碼程式碼到達網頁