Android Kotlin 基礎知識 05.3:將資料繫結與 ViewModel 和 LiveData 互相整合

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

簡介

在本課程的先前程式碼研究室中,您已改善 GuessTheWord 應用程式的程式碼。應用程式現在使用 ViewModel 物件,因此應用程式資料在裝置設定變更時 (例如螢幕旋轉和鍵盤可用性變更) 仍會保持有效。您也新增了可觀察的 LiveData,因此當觀察到的資料變更時,檢視區塊會自動收到通知。

在本程式碼研究室中,您將繼續使用 GuessTheWord 應用程式。您會將檢視區塊繫結至應用程式中的 ViewModel 類別,讓版面配置中的檢視區塊直接與 ViewModel 物件通訊。(到目前為止,應用程式中的檢視畫面都是透過應用程式的片段,與 ViewModel 間接通訊。)將資料繫結與 ViewModel 物件整合後,您就不再需要應用程式片段中的點擊處理常式,因此請移除這些處理常式。

您也會變更 GuessTheWord 應用程式,使用 LiveData 做為資料繫結來源,通知 UI 資料變更,而不使用 LiveData 觀察器方法。

必備知識

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

課程內容

  • 如何使用資料繫結程式庫的元素。
  • 如何透過資料繫結整合 ViewModel
  • 如何透過資料繫結整合 LiveData
  • 如何使用事件監聽器繫結,取代片段中的點擊事件監聽器。
  • 如何將字串格式新增至資料繫結運算式。

學習內容

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

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

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

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

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

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

在本程式碼研究室中,您將在 ViewModel 物件中整合資料繫結與 LiveData,藉此改善 GuessTheWord 應用程式。這項功能會自動處理版面配置中的檢視區塊與 ViewModel 物件之間的通訊,並讓您使用 LiveData 簡化程式碼。

標題畫面

遊戲畫面

分數畫面

在這項工作中,您將找出並執行本程式碼研究室的範例程式碼。您可以使用先前程式碼研究室中建構的 GuessTheWord 應用程式做為範例程式碼,也可以下載範例應用程式。

  1. (選用) 如果您未使用上一個程式碼研究室的程式碼,請下載本程式碼研究室的範例程式碼。解壓縮程式碼,並在 Android Studio 中開啟專案。
  2. 執行應用程式並玩遊戲。
  3. 請注意,「Got It」按鈕會顯示下一個字詞,並將分數增加 1 分;「Skip」按鈕會顯示下一個字詞,並將分數減少 1 分。結束遊戲按鈕會結束遊戲。
  4. 逐一輸入所有單字,你會發現應用程式會自動前往分數畫面。

在先前的程式碼研究室中,您已使用資料繫結,以型別安全的方式存取 GuessTheWord 應用程式中的檢視區塊。但資料繫結的真正強大之處,在於直接將資料繫結至應用程式中的檢視區塊物件。

目前的應用程式架構

在應用程式中,檢視畫面是在 XML 版面配置中定義,而這些檢視畫面的資料則保存在 ViewModel 物件中。每個檢視區塊和對應的 ViewModel 之間都有 UI 控制器,做為兩者之間的中繼。

例如:

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

Button 檢視區塊和 GameViewModel 不會直接通訊,而是需要 GameFragment 中的點擊事件監聽器。

傳遞至資料繫結的 ViewModel

如果版面配置中的檢視畫面直接與 ViewModel 物件中的資料通訊,而不必依賴 UI 控制器做為中介,就會簡單許多。

ViewModel 物件會保存「猜單字」應用程式中的所有 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 變數。將這段程式碼放在 onCreateView() 內,並在 viewModel 初始化後執行。如果 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. ScoreFragmentonCreateView() 內,初始化 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「Build」窗格中的訊息。如果看到結尾為 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

資料繫結可與搭配 ViewModel 物件使用的 LiveData 完美搭配。現在您已將資料繫結新增至 ViewModel 物件,可以開始加入 LiveData

在這項工作中,您會變更 GuessTheWord 應用程式,使用 LiveData 做為資料繫結來源,通知 UI 資料變更,而不使用 LiveData 觀察器方法。

步驟 1:將 word LiveData 新增至 game_fragment.xml 檔案

在這個步驟中,您要將目前的字詞文字檢視區塊直接繫結至 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. GameFragmentonCreateView() 中,初始化 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. 執行應用程式,然後進行遊戲。現在,系統會更新目前字詞,而 UI 控制器中沒有觀察器方法。

步驟 2:將分數 LiveData 新增至 score_fragment.xml 檔案

在這個步驟中,您要將 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:使用資料繫結新增字串格式

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

  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 物件做為資料繫結來源,自動向 UI 通知資料異動情形,而不需要 LiveData 觀察器方法。

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

事件監聽器繫結

  • 事件監聽器繫結是版面配置中的繫結運算式,會在觸發點擊事件 (例如 onClick()) 時執行。
  • 事件監聽器繫結都會寫成 lambda 運算式。
  • 使用事件監聽器繫結,將 UI 控制器中的點按事件監聽器替換為版面配置檔案中的事件監聽器繫結。
  • 資料繫結會建立監聽器,並在檢視區塊中設定監聽器。
 android:onClick="@{() -> gameViewModel.onSkip()}"

將 LiveData 新增至資料繫結

  • LiveData 物件可做為資料繫結來源,自動向 UI 通知資料異動情形。
  • 您可以將檢視區塊直接繫結至 ViewModel 中的 LiveData 物件。當 ViewModel 中的 LiveData 變更時,版面配置中的檢視區塊會自動更新,不需要 UI 控制器中的觀察器方法。
android:text="@{gameViewModel.word}"
  • 如要讓 LiveData 資料繫結正常運作,請將目前活動 (UI 控制器) 設為 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 基礎知識程式碼研究室登陸頁面