將
ViewModel
中的應用程式資料轉換為LiveData
,並觀察自動更新 UI 所出現的變化。
在先前的程式碼研究室中,您已瞭解如何使用 ViewModel
儲存應用程式資料。ViewModel
可在設定變更時保留應用程式資料。在本程式碼研究室中,您將瞭解如何整合 LiveData
與 ViewModel
中的資料。
LiveData
類別也屬於 Android 架構元件的一部分,為可觀察的資料容器類別。
學習目標
- 如何在應用程式中使用
LiveData
和MutableLiveData
。 - 如何使用
LiveData
來封裝儲存在ViewModel
中的資料。 - 如何新增觀察器(observer)方法以觀察
LiveData
中的變化。 - 如何在版面配置檔案中編寫 binding 表達式。
建構項目
- 針對
Unscramble
應用程式中的應用程式資料 (字詞、字詞計數和分數) 使用LiveData
。 - 新增觀察器(observer)方法,在資料變更時接收通知,並自動更新打散字詞的 text view。
- 在 layout 檔案中編寫 binding 表達式,在基礎
LiveData
變更時觸發。分數、字詞計數和打散字詞 text view 會自動更新。
範例應用程式總覽
本程式碼研究室會使用您在先前程式碼研究室中熟悉的 Unscramble
解決方案程式碼。應用程式會顯示打散的字詞,讓玩家進行重組。玩家可以嘗試任意次數來猜測正確字詞。目前字詞、玩家分數和字詞計數等應用程式資料會儲存在 ViewModel
中。不過,該應用程式的 UI 不會反映新的分數和字詞計數值。在本程式碼研究室中,您將使用 LiveData
實作缺少的功能。

什麼是 Livedata
LiveData
是可觀察的資料容器類別,可感知生命週期。
LiveData
特性如下:
LiveData
可保存資料;LiveData
包裝函式可與任何類型的資料搭配使用。LiveData
可觀察,這表示當LiveData
物件保存的資料變更時,observer
會接收通知。LiveData
可感知生命週期。將observer
附加至LiveData
時,observer
會與LifecycleOwner
建立關聯 (通常是 activity 或 fragment )。LiveData
只會更新處於有效生命週期狀態 (例如STARTED
或RESUMED
) 的observer
。如要進一步瞭解LiveData
與observer
,請參閱這篇文章。
範例程式碼中的 UI 更新
在範例程式碼中,每當您想在 UI 中顯示新的打散字詞時,系統都會明確呼叫 updateNextWordOnScreen()
方法。您可在遊戲初始化期間,以及玩家按下「提交」或「略過」按鈕時呼叫此方法。將從 onViewCreated()
、restartGame()
、onSkipWord()
和 onSubmitWord()
方法呼叫此方法。使用 Livedata
時,您不必從多個位置呼叫此方法來更新 UI。您只會在 observer 中執行一次。
將 LiveData 新增至 currentScrambledWord
在這項工作中,您會學習如何將 GameViewModel
中的目前字詞轉換為 LiveData
,以使用 LiveData
, 包裝任何資料。在後續工作中,您會將 observer 新增至此類 LiveData
物件,並瞭解如何觀察 LiveData
。
MutableLiveData
MutableLiveData
是 LiveData
的可變動版本,也就是可以變更儲存在其中的資料值。
在
GameViewModel
中,將變數_currentScrambledWord
的類型變更為MutableLiveData<String>
。LiveData
和MutableLiveData
屬於一般類別,因此您需要指定其保存的資料類型。將
_currentScrambledWord
的變數類型變更為val
,因為LiveData/MutableLiveData
物件的值會保持不變,只有儲存在物件中的資料會改變。
1 | // String改成 LiveData |
- 將支援欄位
currentScrambledWord
類型變更為LiveData<String>
,因為此欄位不可變動。Android Studio 會顯示某些錯誤,您將在後續步驟中進行修正。
1 | // String改成 LiveData |
- 如要存取
LiveData
物件中的資料,請使用value
屬性。在getNextWord()
方法的GameViewModel
中,於else
區塊內,將_currentScrambledWord
引用變更為_currentScrambledWord.value
。
1 | private fun getNextWord() { |
將 Observer 附加至 LiveData 物件
在這項工作中,您將會在應用程式元件 GameFragment
中設定觀察器(observer)。您新增的 observer 會觀察應用程式資料 currentScrambledWord
的變更。LiveData
具備生命週期感知功能,代表其只會更新處於有效生命週期狀態的 observer。因此,GameFragment
中的 observer 只會在 GameFragment
處於 STARTED
或 RESUMED
狀態時收到通知。
在
GameFragment
中,刪除updateNextWordOnScreen()
方法及其所有呼叫。您將會在LiveData
中附加 observer,因此不需要使用此方法。在
onSubmitWord()
中,按照下列步驟修改空的if-else
區塊。完整方法應如下所示。
1 | private fun onSubmitWord() { |
- 為
currentScrambledWord
LiveData
附加 observer。在GameFragment
的onViewCreated()
callback 結尾中,於currentScrambledWord
上呼叫observe()
方法。
1 | // 觀察 currentScrambledWord 的 LiveData |
- Android Studio 會顯示缺少參數的相關錯誤。您將在下一個步驟中修正錯誤。
將
viewLifecycleOwner
做為第一個參數傳遞至observe()
方法。viewLifecycleOwner
代表fragment 的 view 生命週期。此參數可協助LiveData
留意GameFragment
生命週期,且只有在GameFragment
處於有效狀態 (STARTED
或RESUMED
) 時,才通知 observer。使用
newWord
函式參數將lambda
新增為第二個參數。newWord
將包含新的打散字詞值。
1 | // 觀察 scrambledCharArray LiveData,傳入 LifecycleOwner 和 observer |
lambda
運算式是未宣告的匿名函式,但會立即以運算式的形式傳遞。lambda
運算式一律會加上大括號{ }
。
- 在
lambda
運算式的函式主體中,將newWord
指派至打散字詞 text view。
1 | // Observe the scrambledCharArray LiveData, passing in the LifecycleOwner and the observer. |
- 編譯並執行應用程式。您的遊戲應用程式應可照常運作,但現在會在
LiveData
observer 中 (而非在updateNextWordOnScreen()
方法中) 自動更新打散字詞 text view。
將 Observer 附加至分數和字詞計數
如同前一個工作,在這項工作中,您會將 LiveData
新增至應用程式的其他資料、分數和字詞計數中,使 UI 在遊戲期間能夠更新分數和字詞計數的正確值。
步驟 1:使用 LiveData 包裝分數和字詞計數
在
GameViewModel
中,將_score
和_currentWordCount
類別變數的類型變更為val
。將變數
_score
和_currentWordCount
的資料類型變更為MutableLiveData
,並將其初始化為0
。將支援欄位類型變更為
LiveData<Int>
。
1 | private val _score = MutableLiveData(0) |
- 在
reinitializeData()
方法開頭的GameViewModel
中,將_score
和_currentWordCount
的引用變更為_score.value
和_currentWordCount.value
。
1 | fun reinitializeData() { |
- 在
nextWord()
方法內的GameViewModel
中,將_currentWordCount
的參照變更為_currentWordCount.value!!
。
1 | fun nextWord(): Boolean { |
在
GameViewModel
的increaseScore()
和getNextWord()
方法中,分別將_score
和_currentWordCount
的參照變更為_score.value
和_currentWordCount.value
。Android Studio 會顯示錯誤,因為_score
已不再是整數,而是LiveData
,您將在後續步驟中修正此錯誤。使用
plus()
Kotlin 函式增加_score
值,如此即可使用空值安全性執行加法。
1 | private fun increaseScore() { |
- 同樣地,您也可以使用
inc()
Kotlin 函式,使用空值安全性將值增加一。
1 | private fun getNextWord() { |
- 在
GameFragment
中,使用value
屬性存取score
的值。在showFinalScoreDialog()
方法中,將viewModel.score
變更為viewModel.score.value
。
1 | private fun showFinalScoreDialog() { |
步驟 2:將 Observer 附加至分數和字詞計數
應用程式中的分數和字詞計數不會更新。在這項工作中,您將使用 LiveData
observer 進行更新。
- 在
GameFragment
的onViewCreated()
方法中 ,刪除更新分數和字詞計數 text view 的程式碼。
移除:
1 | binding.score.text = getString(R.string.score, 0) |
- 在
GameFragment
中onViewCreated()
方法的結尾,為score
附加 observer。將viewLifecycleOwner
做為第一個參數傳遞至 observer,並使用lambda
運算式做為第二個參數。在lambda
運算式中,將新的分數做為參數傳遞,並在函式主體中,將新分數設為 text view 的文字。
1 | viewModel.score.observe(viewLifecycleOwner, |
- 在
onViewCreated()
方法結尾,為currentWordCount
LiveData
附加 observer。將viewLifecycleOwner
做為第一個參數傳遞至 observer,並使用lambda
運算式做為第二個參數。在lambda
運算式中,將新字詞計數做為參數傳遞,並在函式主體中,將新字詞計數與MAX_NO_OF_WORDS
設為 text view 的文字。
1 | viewModel.currentWordCount.observe(viewLifecycleOwner, |
- 在生命週期擁有者的生命週期期間 (即
GameFragment
),當ViewModel
內的分數和字詞計數值發生變化時,就會觸發新的 observer。
- 執行應用程式即可見證其奧妙之處。使用一些字詞進行遊戲。畫面上的分數和字詞計數也會正確更新。請注意,這些 text view 並不會根據程式碼的部分條件進行更新。
score
和currentWordCount
為LiveData
,且基礎值變更時,系統會自動呼叫對應的 observer。

將 LiveData 搭配 Data Binding 使用
在先前的工作中,您的應用程式會監聽程式碼中的 data 變更。同樣地,應用程式也可以監聽 layout 中的 data 變更。透過 Data Binding,當可觀察的 LiveData 值變更時,系統也會通知其綁定(bind)的 layout 中的 UI 元素,且可從 layout 中更新 UI。
概念:Data Binding
在先前的程式碼研究室中,您已瞭解單向的 view binding。您可以將 view 綁定(bind)至程式碼,但無法將程式碼綁定(bind)至 view。
複習 View Binding:
view binding 是一項可讓您更輕鬆地在程式碼中訪問 view 的功能。它會為各 XML layout 檔案產生 binding class。凡是在對應 layout 中具有 ID 的 view,binding class 的例項都會包含指向這些 view 的直接引用。舉例來說,Unscramble 應用程式目前使用 view binding,因此使用產生的 binding class 可在程式碼中引用 view。
範例:
1 | binding.textViewUnscrambledWord.text = newWord |
- 如果使用 view binding,就無法引用 view 中的應用程式資料 (layout檔案)。您可以使用 data binding 完成這項操作。
Data Binding
Data Binding 程式庫也屬於 Android Jetpack 程式庫的一部分。Data Binding 使用宣告式格式將 layout 中的 UI 元件 bind 至應用程式中的資料(data)來源,稍後將在程式碼研究室中說明。
簡單來說,Data Binding 會將 data (從程式碼) bind 至 View + View Binding (將 view bind 至程式碼)︰
在 UI controller 中使用 view binding 的範例
1 | binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord |
在 layout 檔案中使用 data binding 的範例
1 | android:text="@{gameViewModel.currentScrambledWord}" |
- 以上範例說明如何使用 data binding 程式庫,直接將應用程式 data 指派至 layout 檔案中的 view/widget(小工具)。請注意,指派運算式中使用
@{}
語法。
使用 data binding 的主要優點在於,您可以移除 activity 中的許多 UI 架構呼叫,使其更加簡單且易於維護。這還可改善應用程式效能,避免發生記憶體流失及空值(null)指標例外狀況。
步驟 1:變更資料繫結的檢視繫結
- 在
build.gradle(Module)
檔案中,啟用buildFeatures
區段下的dataBinding
屬性。
將
1 | buildFeatures { |
取代為
1 | buildFeatures { |
- 如要在任何 Kotlin 專案中使用 data binding,請套用
kotlin-kapt
外掛程式。此步驟已在build.gradle(Module)
檔案中完成。
1 | plugins { |
- 上述步驟會自動為應用程式中的每個 layout XML 檔案產生 binding class,如果 layout 檔案名稱為
activity_main.xml
,則自動產生的 class 將會稱為ActivityMainBinding
。
步驟 2:將 layout 檔案轉換為 Data Binding layout
Data Binding layout 檔案略有不同,以 根(root) 標記 <layout>
為開頭,後面接著可選的 <data>
元素和 view
根(root) 元素。此 view 元素就是非綁定(bind) layout 檔案中的根(root)。
開啟
game_fragment.xml
,選取「Code」分頁標籤。如要將 layout 轉換成 data binding layout,請將根(root)元素納入
<layout>
標記中。您也必須將命名空間定義 (開頭為xmlns:
的屬性) 移至新的根(root)元素。在根(root)元素上方的<layout>
標記中加入<data></data>
標記。
- Android Studio 提供可自動執行此作業的便利方法:在根(root)元素
<ScrollView>
上按一下滑鼠右鍵,然後依序選取「Show Context Actions」>「Convert to data binding layout」。
- layout 應如下所示:
1 | <layout xmlns:android="http://schemas.android.com/apk/res/android" |
- 在
GameFragment
中,在onCreateView()
方法的一開始,變更binding
變數的 instance,以使用 data binding。
將
1 | binding = GameFragmentBinding.inflate(inflater, container, false) |
取代為
1 | binding = DataBindingUtil.inflate(inflater, R.layout.game_fragment, container, false) |
- 編譯程式碼;您應能夠順利編譯。您的應用程式現在會使用 data binding,layout 中的 view 也可存取應用程式的 data。
新增 Data Binding 變數
在這項工作中,您必須在 layout 檔案中加入屬性,以便存取 viewModel
中的應用程式 data。您將初始化程式碼中的 layout 變數。
- 在
game_fragment.xml
的<data>
標記中,新增名為<variable>
的子標記,宣告名為gameViewModel
且類型(type)為GameViewModel
的屬性。您會使用此方法將 ViewModel 中的 data 綁定(bind)至 layout。
1 | <data> |
- 請注意,
gameViewModel
的類型包含套件名稱(package name)。請確認此套件名稱與應用程式中的套件名稱相符。
- 在
gameViewModel
宣告下方,在<data>
標記中加入另一個變數,並將其命名為maxNoOfWords
,類型(type)為Integer
。您將使用此方法綁定(bind)至ViewModel
中的變數,以儲存每場遊戲的字詞數。
1 | <data> |
- 在
onViewCreated()
方法的開頭的GameFragment
中,初始化 layout 變數gameViewModel
和maxNoOfWords
。
1 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
LiveData
可觀察生命週期,因此您必須將生命週期擁有者傳遞給 layout。在onViewCreated()
方法內的GameFragment
中,在綁定(bind)變數的初始化下方新增下列程式碼。
1 | // 將 fragment view 指定為 binding 的 lifecycle owner,以觀察 LiveData 更新 |
- 提醒您,您在實作
LiveData
observer 時,也實作類似功能。您已將viewLifecycleOwner
做為其中一個參數傳遞給LiveData
observer。
使用 binding 運算式
binding 運算式會寫入屬性 (例如 android:text
) layout 內,並參照 layout 屬性。layout 屬性會透過 <variable>
標記在 data binding layout 檔案的頂部進行宣告。當任何相依變數有所變更時,「DB Library」將執行 binding 運算式 (進而更新 view)。使用 data binding library時,此變更偵測是無須付費的最佳化功能。
binding 運算式的語法
binding 運算式以 @
符號開頭,並加上大括號 {}
。在以下範例中,將TextView
文字設為 user
變數的 firstName
屬性:
範例:
1 | <TextView android:layout_width="wrap_content" |
步驟 1:將 binding 運算式新增至目前字詞
在此步驟中,您可以將目前的字詞 text view 綁定(bind)至 ViewModel
中的 LiveData
物件。
- 在
game_fragment.xml
中,請將text
屬性新增至textView_unscrambled_word
text view。使用新的 layout 變數gameViewModel
,並將@{gameViewModel.currentScrambledWord}
指派給text
屬性。
1 | <TextView |
- 在
GameFragment
中,移除currentScrambledWord
的LiveData
observer 程式碼: fragment 中不再需要使用 observer 程式碼。layout 會直接收到LiveData
的變更更新。
移除:
1 | viewModel.currentScrambledWord.observe(viewLifecycleOwner, |
- 執行您的應用程式,應用程式應可照常運作。不過,打散字詞 text view 目前使用 binding 運算式更新 UI,而非
LiveData
observer。
步驟 2:將 binding 運算式新增至分數和字詞計數
data binding 運算式的資源
data binding 運算式可透過下列語法引用應用程式資源。
範例:
1 | android:padding="@{@dimen/largePadding}" |
- 在上述範例中,系統會為
padding
屬性指派dimen.xml
資源檔案的largePadding
值。
您也可以傳遞 layout 屬性做為資源參數。
範例:
1 | android:text="@{@string/example_resource(user.lastName)}" |
strings.xml
1 | <string name="example_resource">Last Name: %s</string> |
- 在上述範例中,
example_resource
是具有%s
預留位置的字串資源。您會將user.lastName
做為資源參數傳入 binding 運算式,其中user
是 layout 變數。
在此步驟中,您會將 binding 運算式新增至分數和字詞計數 text view,並傳入資源參數。這個步驟與您為上述 textView_unscrambled_word
所做操作類似。
- 在
game_fragment.xml
中,使用以下 binidng 運算式更新word_count
text view 的text
屬性。使用word_count
字串資源,並將gameViewModel.currentWordCount
和maxNoOfWords
做為資源參數傳入。
1 | <TextView |
- 使用以下 binding 運算式更新
score
text view 的text
屬性。使用score
字串資源,並傳入gameViewModel.score
做為資源參數。
1 | <TextView |
- 從
GameFragment
中移除LiveData
observer。您已無需使用這些 observer,binding 運算式會在對應LiveData
變更時更新 UI。
移除:
1 | viewModel.score.observe(viewLifecycleOwner, |
- 執行應用程式,使用一些字詞進行遊戲。現在,您的程式碼會使用
LiveData
和 binding 運算式更新 UI。


恭喜!您已瞭解如何將 LiveData
observer 搭配 LiveData
使用,以及將 LiveData
搭配 binding 運算式使用。
總結
LiveData
可保存資料;LiveData
包裝函式可與任何資料搭配使用LiveData
可觀察,這表示當LiveData
物件保存的資料變更時,observer 會接收通知。LiveData
可感知生命週期。將 observer 附加至LiveData
時,observer 會與LifecycleOwner
建立關聯 (通常是 activity 或 fragment)。LiveData
只會更新處於有效生命週期狀態 (例如STARTED
或RESUMED
) 的 observer。- 應用程式可以透過 data binding 和 binding 運算式監聽 layout 中的
LiveData
變更。 - binding 運算式會寫入屬性 (例如
android:text
) layout 內,並引用 layout 屬性。