Android Kotlin 基礎課程 05.3:與 ViewModel 和 LiveData 的資料繫結

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

簡介

在上一堂課中,您已針對 GuessTheWord 應用程式改善了程式碼。此應用程式現在使用 ViewModel 物件,讓應用程式資料能夠繼續保留裝置設定變更 (例如畫面旋轉和鍵盤變更)。你還新增了可觀測的LiveData,因此如果觀測到的資料有變化,就會自動通知。

在這個程式碼研究室中,您將繼續與 GuessTheWord 應用程式合作。您可以將檢視繫結至應用程式中的 ViewModel 類別,讓版面配置中的檢視直接與 ViewModel 物件進行通訊。(在您應用程式中以前,資料檢視會透過應用程式的片段,與「ViewModel直接通訊)。將資料繫結與 ViewModel 物件整合後,您不再需要在應用程式片段中使用點擊處理常式,就可以移除這些處理常式。

您也可以將 GuessTheWord 應用程式變更為使用 LiveData 做為資料繫結來源,這樣即使不使用 LiveData 檢視器方法,也能通知 UI 資料有異動。

須知事項

  • 如何在 Kotlin 中建立基本 Android 應用程式。
  • 活動和片段生命週期的運作方式。
  • 如何在您的應用程式中使用 ViewModel 物件。
  • 如何在 ViewModel 中使用 LiveData 儲存資料。
  • 如何新增觀測器方法來觀察 LiveData 資料中的變化。

課程內容

  • 如何使用資料繫結資料庫
  • 如何將 ViewModel 與資料繫結整合。
  • 如何將 LiveData 與資料繫結整合。
  • 如何使用監聽器繫結來取代片段中的點擊接聽器。
  • 如何將資料格式設定新增至資料繫結運算式。

執行步驟

  • GuessTheWord 版面配置中的檢視畫面會與 ViewModel 物件間接通訊,並透過使用者介面控制器 (片段) 來轉發資訊。在這個程式碼研究室中,您會將應用程式的檢視繫結至 ViewModel 物件,讓檢視直接ViewModel 物件進行通訊。
  • 您將應用程式變更為使用 LiveData 做為資料繫結來源。完成此變更後,LiveData 物件會通知 UI 資料異動,不再需要 LiveData 觀測器方法。

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

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

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

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

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

在這個程式碼研究室中,您可以將資料繫結與 ViewModel 物件中的 LiveData 整合,以改善 GuessTheWord 應用程式。這會讓版面配置與 ViewModel 物件中的視圖自動相互通訊,並可讓您使用 LiveData 簡化程式碼。

標題畫面

遊戲畫面

分數畫面

在這項工作中,您會找到並執行這個程式碼研究室的範例程式碼。您可以使用在舊版程式碼研究室中建立的 GuessTheWord 應用程式做為範例程式碼,也可以下載入門應用程式。

  1. (選用) 如果您並未使用前一個程式碼研究室提供的程式碼,請下載這個程式碼研究室的範例程式碼。將程式碼解壓縮,然後在 Android Studio 中開啟專案。
  2. 執行應用程式並玩遊戲。
  3. 請注意,[我知道了] 按鈕會顯示下一個字詞,並將分數提高一個,而 [略過] 按鈕則會顯示下一個字詞,並將分數降低 1 個。[結束遊戲] 按鈕可結束遊戲。
  4. 瀏覽所有字詞,注意應用程式會自動瀏覽至分數畫面。

在先前的程式碼研究室中,您使用資料繫結做為類型安全的方法,以便存取 GuessTheWord 應用程式中的檢視區塊。不過資料繫結的實際功能,就是要提供名稱建議,也就是將資料「直接」繫結至應用程式中的檢視物件。

目前的應用程式架構

應用程式中的 XML 定義會定義為 XML 版面配置,且這些資料檢視的資料會儲存在 ViewModel 物件中。每個資料檢視及其對應的 ViewModel 之間都是 UI 控制器,它是兩者之間的轉發。

例如:

  • Got It 按鈕在 game_fragment.xml 版面配置檔案中定義為 Button 視圖。
  • 當使用者輕觸 [我知道了] 按鈕時,GameFragment 片段中的點擊接聽器會呼叫 GameViewModel 中的對應點擊事件監聽器。
  • 分數會在 GameViewModel後更新。

Button 資料檢視和 GameViewModel 不會直接通訊,因此需要在 GameFragment 的點擊監聽器。

傳遞至資料繫結的 ViewModel

如果版面配置中的視圖會直接與 ViewModel 物件中的資料進行通訊,而不需依賴 UI 控制器做為媒介,就會比較簡單。

ViewModel 物件會保留 GuessTheWord 應用程式中的所有 UI 資料。將 ViewModel 物件傳遞至資料繫結中,就可以自動切換檢視和 ViewModel 物件之間的部分通訊。

在這項工作中,您將 GameViewModelScoreViewModel 類別與相應的 XML 版面配置建立關聯。您也可以設定事件監聽器繫結來處理點擊事件。

步驟 1:新增 GameViewModel 的資料繫結

在這個步驟中,您將 GameViewModel 與對應的版面配置檔案 game_fragment.xml 建立關聯。

  1. game_fragment.xml 檔案中,新增 GameViewModel 類型的資料繫結變數。如果 Android Studio 發生錯誤,請清理及重建專案。
<layout ...>

   <data>

       <variable
           name="gameViewModel"
           type="com.example.android.guesstheword.screens.game.GameViewModel" />
   </data>
  
   <androidx.constraintlayout...
  1. GameFragment 檔案中,將 GameViewModel 傳入資料繫結。

    如要這麼做,請將 viewModel 指派給您在前一個步驟中宣告的 binding.gameViewModel 變數。初始化 viewModel 後,請將這段程式碼置於 onCreateView() 中。如果 Android Studio 發生錯誤,請清理及重建專案。
// Set the viewmodel for databinding - this allows the bound layout access 
// to all the data in the ViewModel
binding.gameViewModel = viewModel

步驟 2:使用事件繫結處理事件

聽眾繫結是觸發運算式時,為觸發 onClick()onZoomIn()onZoomOut() 等事件所得出的繫結運算式。接聽器繫結是以 lambda 運算式的形式編寫。

資料繫結會建立事件監聽器,並在檢視畫面中設定事件監聽器。監聽事件發生時,事件監聽器會評估 lambda 運算式。接聽器繫結適用於 Android Gradle 外掛程式 2.0 以上版本。詳情請參閱版面配置和繫結運算式一文。

在這個步驟中,您會將 GameFragment 的點擊接聽器替換成 game_fragment.xml 檔案中的接聽程式繫結。

  1. game_fragment.xml 中,將 onClick 屬性新增至 skip_button。定義繫結運算式並呼叫 GameViewModel 中的 onSkip() 方法。這個繫結運算式稱為監聽器繫結
<Button
   android:id="@+id/skip_button"
   ...
   android:onClick="@{() -> gameViewModel.onSkip()}"
   ... />
  1. 同樣地,將 correct_button 點擊事件繫結至 GameViewModel 中的 onCorrect() 方法。
<Button
   android:id="@+id/correct_button"
   ...
   android:onClick="@{() -> gameViewModel.onCorrect()}"
   ... />
  1. end_game_button 點擊事件繫結至 GameViewModel 中的 onGameFinish() 方法。
<Button
   android:id="@+id/end_game_button"
   ...
   android:onClick="@{() -> gameViewModel.onGameFinish()}"
   ... />
  1. GameFragment 中,移除設定點擊接聽器的陳述式,然後移除點擊接聽器呼叫的函式。不再需要這類廣告。

要移除的程式碼:

binding.correctButton.setOnClickListener { onCorrect() }
binding.skipButton.setOnClickListener { onSkip() }
binding.endGameButton.setOnClickListener { onEndGame() }

/** Methods for buttons presses **/
private fun onSkip() {
   viewModel.onSkip()
}
private fun onCorrect() {
   viewModel.onCorrect()
}
private fun onEndGame() {
   gameFinished()
}

步驟 3:為 ScoreViewModel 新增資料繫結

在這個步驟中,您將 ScoreViewModel 與對應的版面配置檔案 score_fragment.xml 建立關聯。

  1. score_fragment.xml 檔案中,新增 ScoreViewModel 類型的繫結變數。這個步驟與您在 GameViewModel 執行的步驟類似。
<layout ...>
   <data>
       <variable
           name="scoreViewModel"
           type="com.example.android.guesstheword.screens.score.ScoreViewModel" />
   </data>
   <androidx.constraintlayout.widget.ConstraintLayout
  1. score_fragment.xml 中,將 onClick 屬性新增至 play_again_button。定義事件繫結繫結,並在 ScoreViewModel 中呼叫 onPlayAgain() 方法。
<Button
   android:id="@+id/play_again_button"
   ...
   android:onClick="@{() -> scoreViewModel.onPlayAgain()}"
   ... />
  1. ScoreFragment 中,在 onCreateView() 中初始化 viewModel。然後初始化 binding.scoreViewModel 繫結變數。
viewModel = ...
binding.scoreViewModel = viewModel
  1. ScoreFragment 中,移除用來設定 playAgainButton 點擊接聽器的程式碼。如果 Android Studio 顯示錯誤,請清理專案並重新建構。

要移除的程式碼:

binding.playAgainButton.setOnClickListener {  viewModel.onPlayAgain()  }
  1. 執行您的應用程式。應用程式應能照常運作,但現在按鈕檢視可與 ViewModel 物件直接通訊。檢視表不會再透過 ScoreFragment 中的按鈕點擊處理常式進行通訊。

排解資料繫結錯誤訊息

當應用程式使用資料繫結時,編譯程序會產生用於資料繫結的中繼類別。如果應用程式在嘗試編譯應用程式時發生 Android Studio 無法偵測的錯誤,您就不會看到警告或紅色程式碼,因此正在編寫程式碼。但在編譯時,您會收到所產生的中階類別產生的加密錯誤。

如果收到隱密的錯誤訊息:

  1. 請仔細查看 Android Studio「建置」窗格中的訊息。如果出現位置結尾為 databinding 的位置,表示資料繫結發生錯誤。
  2. 在版面配置 XML 檔案中,檢查是否有 onClick 屬性使用了資料繫結。尋找 lambda 運算式所呼叫的函式,並確認該函式確實存在。
  3. 在 XML 的 <data> 部分中,檢查資料繫結變數的拼字。

舉例來說,下列屬性值中函式名稱 onCorrect() 的拼字錯誤:

android:onClick="@{() -> gameViewModel.onCorrectx()}"

另請注意,XML 檔案「<data>」區段中的 gameViewModel 拼字錯誤:

<data>
   <variable
       name="gameViewModelx"
       type="com.example.android.guesstheword.screens.game.GameViewModel" />
</data>

您必須先編譯應用程式,Android Studio 才能偵測這類錯誤,且編譯器會顯示以下錯誤訊息:

error: cannot find symbol
import com.example.android.guesstheword.databinding.GameFragmentBindingImpl"

symbol:   class GameFragmentBindingImpl
location: package com.example.android.guesstheword.databinding

資料繫結可與 LiveData 搭配使用,以便與 ViewModel 物件搭配使用。既然您已經添加到 ViewModel 對象的數據綁定,您為了。 LiveData

在這項工作中,您會把 GuessTheWord 應用程式變更為使用 LiveData 做為資料繫結來源,這樣即使不使用 LiveData 檢視器方法,也能通知 UI 資料有異動。

步驟 1:在 Game_fragment.xml 檔案中加入 LiveData 字詞

在這個步驟中,您會將目前字詞文字檢視直接繫結至 ViewModel 中的 LiveData 物件。

  1. game_fragment.xml 中,將 android:text 屬性新增至 word_text 文字檢視。

使用繫結變數 gameViewModel,將它設為 GameViewModel 中的 LiveData 物件 word

<TextView
   android:id="@+id/word_text"
   ...
   android:text="@{gameViewModel.word}"
   ... />

請注意,您不一定要使用 word.value。而是改用實際的 LiveData 物件。LiveData 物件會顯示 word 的目前值。如果 word 的值為空值,LiveData 物件會顯示空的字串。

  1. GameFragment 中,在 onCreateView() 初始化 gameViewModel 之後,將目前的活動設為 binding 變數的生命週期擁有者。這會定義上述 LiveData 物件的範圍,讓物件自動更新版面配置 game_fragment.xml 中的視圖。
binding.gameViewModel = ...
// Specify the current activity as the lifecycle owner of the binding.
// This is used so that the binding can observe LiveData updates
binding.lifecycleOwner = this
  1. GameFragment 中,移除 LiveData word 的觀察器。

要移除的程式碼:

/** Setting up LiveData observation relationship **/
viewModel.word.observe(this, Observer { newWord ->
   binding.wordText.text = newWord
})
  1. 執行應用程式並玩遊戲。現在,當您在使用者介面控制項中更新目前的字詞時,並不會使用觀測器方法。

步驟 2:在 Score_fragment.xml 檔案中新增分數 LiveData

在這個步驟中,您會將 LiveData score 繫結至分數片段的分數文字檢視。

  1. score_fragment.xml 中,將 android:text 屬性新增至分數文字檢視。將「scoreViewModel.score指派給「text屬性。由於 score 是整數,因此請使用 String.valueOf() 將其轉換為字串。
<TextView
   android:id="@+id/score_text"
   ...
   android:text="@{String.valueOf(scoreViewModel.score)}"
   ... />
  1. ScoreFragment 中,在初始化 scoreViewModel 之後,請將目前的活動設為 binding 變數的生命週期擁有者。
binding.scoreViewModel = ...
// Specify the current activity as the lifecycle owner of the binding.
// This is used so that the binding can observe LiveData updates
binding.lifecycleOwner = this
  1. ScoreFragment 中,移除 score 物件的觀察器。

要移除的程式碼:

// Add observer for score
viewModel.score.observe(this, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})
  1. 執行應用程式並玩遊戲。請注意,分數片段的分數正確顯示,但分數片段沒有觀察項目。

步驟 3:使用資料繫結新增字串格式

在版面配置中,您可以新增字串格式和資料繫結。在這項工作中,您會設定目前文字格式,在前後加上引號。您還可以將分數字串的格式設定成「目前分數」,如下圖所示。

  1. string.xml 中,加入以下字串,以設定 wordscore 文字檢視的格式。%s%d 是目前字詞和目前分數的預留位置。
<string name="quote_format">\"%s\"</string>
<string name="score_format">Current Score: %d</string>
  1. game_fragment.xml 中,更新 word_text 文字檢視的 text 屬性以使用 quote_format 字串資源。通過「gameViewModel.word」。這會將目前的字詞以引數的形式傳送至格式字串。
<TextView
   android:id="@+id/word_text"
   ...
   android:text="@{@string/quote_format(gameViewModel.word)}"
   ... />
  1. score 文字檢視的格式與 word_text 類似。在 game_fragment.xml 中,將 text 屬性新增至 score_text 文字檢視。使用字串資源 score_format,這個字串會使用一個數字引數,由 %d 預留位置表示。傳入 LiveData 物件 score,做為此格式設定字串的引數。
<TextView
   android:id="@+id/score_text"
   ...
   android:text="@{@string/score_format(gameViewModel.score)}"
   ... />
  1. GameFragment 類別的 onCreateView() 方法中,移除 score 觀測器程式碼。

要移除的程式碼:

viewModel.score.observe(this, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})
  1. 清理、重建並執行應用程式,然後開始玩遊戲。請注意,目前的畫面和分數會在遊戲畫面中設定格式。

恭喜!您已將 LiveDataViewModel 整合至應用程式中的資料繫結。如此一來,版面配置中的視圖就能與 ViewModel 直接通訊,無需在片段中使用點擊處理常式。您也可以使用 LiveData 物件做為資料繫結來源,以便在沒有 LiveData 檢視器方法的情況下,自動告知 UI 資料異動。

Android Studio 專案:GuessTheWord

  • 資料繫結程式庫可與 ViewModelLiveData 等 Android 架構元件完美搭配運作。
  • 應用程式的版面配置可以繫結至架構元件的資料,該架構已協助您管理 UI 控制器的生命週期,並通知資料變更。

ViewModel 資料繫結

  • 您可以使用資料繫結,將 ViewModel 與版面配置建立關聯。
  • ViewModel 物件會保留 UI 資料。將 ViewModel 物件傳遞至資料繫結,就可以自動切換檢視和 ViewModel 物件之間的部分通訊。

如何將 ViewModel 與版面配置建立關聯:

  • 在版面配置檔案中,新增 ViewModel 類型的資料繫結變數。
   <data>

       <variable
           name="gameViewModel"
           type="com.example.android.guesstheword.screens.game.GameViewModel" />
   </data>
  • GameFragment 檔案中,將 GameViewModel 傳入資料繫結。
binding.gameViewModel = viewModel

事件監聽器繫結

  • Listener bindings 是版面配置中的繫結運算式,會在觸發 onClick() 這類點擊事件時執行。
  • 事件監聽器繫結都會寫成 lambda 運算式。
  • 透過事件繫結,您可以將 UI 控制器的點擊事件監聽器替換成版面配置檔案中的事件繫結繫結。
  • 資料繫結會建立事件監聽器,並在檢視畫面中設定事件監聽器。
 android:onClick="@{() -> gameViewModel.onSkip()}"

將 LiveData 加入資料繫結

  • LiveData 物件可做為資料繫結來源,以便在資料有異動時自動通知 UI。
  • 您可以將視圖直接繫結至 ViewModel 中的 LiveData 物件。當 ViewModel 中的 LiveData 發生變更時,版面配置中的視圖可能會自動更新,而不會使用 UI 控制器中的觀測器方法。
android:text="@{gameViewModel.word}"
  • 為了讓 LiveData 資料繫結順利運作,請將目前活動 (UI 控制器) 設為使用者介面控制器中 binding 變數的生命週期擁有者。
binding.lifecycleOwner = this

使用資料繫結的字串格式

  • 您可以使用資料繫結,為字串資源設定預留位置,例如 %s 字串,以及 %d 整數。
  • 如要更新資料檢視的 text 屬性,請以 LiveData 物件的形式傳送格式設定字串做為引數。
 android:text="@{@string/quote_format(gameViewModel.word)}"

Udacity 課程:

Android 開發人員說明文件:

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

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

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

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

回答這些問題

第 1 題

關於聽眾繫結的敘述,下列何者「不」正確

  • 事件監聽器繫結指的是會在事件發生時執行的繫結運算式。
  • 事件監聽器繫結適用於所有版本的 Android Gradle 外掛程式。
  • 事件監聽器繫結都會寫成 lambda 運算式。
  • 事件監聽器繫結類似於方法的參照,但可讓你執行任意的資料繫結運算式。

第 2 題

假設您的應用程式含有以下字串資源:
<string name="generic_name">Hello %s</string>

下列何者為使用資料繫結運算式設定字串格式的正確語法?

  • android:text= "@{@string/generic_name(user.name)}"
  • android:text= "@{string/generic_name(user.name)}"
  • android:text= "@{@generic_name(user.name)}"
  • android:text= "@{@string/generic_name,user.name}"

第 3 題

何時會評估並執行事件監聽器繫結運算式?

  • LiveData 保留的資料有所變更時
  • 當活動藉由變更設定而重新建立時
  • onClick()」這類事件發生時
  • 當活動進入背景時

開始下一門課程:5.4:LiveData 轉換

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