瞭解如何使用
ViewModel
架構元件來儲存應用程式資料。如果在設定變更或其他事件期間,刪除架構並重新建立 activity 和 fragment,儲存的資料不會遺失。
學習目標
- Android 應用程式架構基本概念簡介。
- 如何在應用程式中使用
ViewModel
類別。 - 如何使用
ViewModel
,透過裝置設定變更保留 UI 資料。 - Kotlin 的幕後屬性。
- 如何使用質感設計元件庫中的
MaterialAlertDialog
。 - 建立
Unscramble
遊戲應用程式,可讓使用者猜測打散的字詞。
範例應用程式總覽
遊戲總覽
Unscramble
應用程式為單人字詞重組遊戲。本應用程式一次會顯示一個打散的字詞,且玩家必須使用打散的所有字母猜出這個字詞。只要字詞正確無誤,玩家即可得分,否則玩家可任意進行嘗試。應用程式也具備略過目前字詞的選項。應用程式左上角會顯示字詞計數,也就是目前遊戲中已遊玩過的字詞數。每場遊戲共有 10 字。



下載範例程式碼
如果您使用 GitHub 中的範例程式碼,請注意資料夾名稱是 android-basics-kotlin-unscramble-app-starter
。在 Android Studio 中開啟專案時,請選取這個資料夾。
範例程式碼總覽
- 在 Android Studio 中開啟含有範例程式碼的專案。
- 在 Android 裝置或模擬器上執行應用程式。
- 透過數個字詞進行遊戲,請輕觸「提交」和「略過」按鈕。 請注意,輕觸按鈕時會顯示下一個字詞,並增加字詞計數。
- 請留意,分數只會在輕觸「Submit」按鈕時提升。
範例程式碼相關問題
玩遊戲時,您可能已注意到下列錯誤:
- 按一下「Submit」按鈕時,應用程式不會檢查玩家的字詞。玩家總是可以得分。
- 無法結束遊戲。應用程式可讓您遊玩超過 10 個字詞。
- 遊戲畫面會顯示打散的字詞、玩家分數和字詞計數。旋轉裝置或模擬器變更螢幕方向。請注意,目前的字詞、分數和字詞計數都會消失,遊戲也會重新開始。
應用程式的主要問題
設定變更時 (例如裝置螢幕方向變更),範例應用程式不會儲存及還原應用程式狀態和資料。
您可以使用 onSaveInstanceState()
callback 解決此問題。不過,使用 onSaveInstanceState()
方法時,您必須編寫額外的程式碼將狀態儲存在套件中,並實作邏輯以擷取該狀態。此外,可儲存的資料量極少。
您可以使用在本課程所學到的 Android 架構元件來解決這些問題。
範例程式碼逐步操作說明
您下載的範例程式碼包含已為您預先設計的遊戲畫面版面配置。本課程重點為實作遊戲邏輯。您需要使用架構元件來實作建議的應用程式架構,並解決上述問題。以下是部分檔案的簡要逐步操作說明,可協助您快速上手。
game_fragment.xml
- 在「設計」檢視畫面中開啟
res/layout/game_fragment.xml
。 - 這包含應用程式中唯一畫面的版面配置,也就是遊戲畫面。
- 此版面配置包含玩家字詞的文字欄位,以及顯示分數和字詞計數的
TextViews
。另外還提供說明、「Submit」按鈕和「Skip」按鈕,方便玩遊戲。
main_activity.xml
以單一遊戲片段定義主要 activity 版面配置。
res/values
資料夾
colors.xml
包含應用程式中使用的主題色彩strings.xml
包含應用程式所需的所有字串themes
和styles
資料夾內含應用程式的 UI 自訂項目
MainActivity.kt
包含預設範本產生的程式碼,可將 activity 的內容檢視畫面設為 main_activity.xml
。
ListOfWords.kt
此檔案內含遊戲中使用的字詞清單、每場遊戲字詞數量上限,以及玩家針對每個正確字詞所得分數的常數。
GameFragment.kt
這是應用程式中唯一的 fragment,也是大部分遊戲動作發生處:
- 變數是根據目前打散的字詞 (
currentScrambledWord
)、字詞計數 (currentWordCount
) 和分數 (score
) 所定義。 - 已定義可存取名為
binding
之game_fragment
檢視畫面的 binding 物件 instance。 onCreateView()
函式會使用 binding 物件加載game_fragment
版面配置 XML。onViewCreated()
函式會設定按鈕點選監聽器,並更新 UI。onSubmitWord()
是「提交」按鈕的點擊事件監聽器,此函式會顯示下一個打散的字詞、清除文字欄位,並在未驗證玩家字詞的情況下增加分數和字詞計數。onSkipWord()
是「略過」按鈕的點擊事件監聽器,此函式會更新與onSubmitWord()
類似的 UI (分數除外)。getNextScrambledWord()
是一項輔助函式,其可從字詞清單中挑選隨機字詞,並隨機排序這些字母。- 系統會分別使用
restartGame()
和exitGame()
函式重新啟動及結束遊戲,您稍後將會使用這些函式。 setErrorTextField()
可清除文字欄位內容,並重設錯誤狀態。updateNextWordOnScreen()
函式可顯示新的打散字詞。
瞭解應用程式架構
架構可提供相關規範,協助您在應用程式內分配類別間的責任。設計良好的應用程式架構可協助您擴大應用程式,並於日後擴充其他功能。此外,也能讓團隊更輕鬆進行協作。
最常見的架構原則為:關注點分離,以及透過模型使用 UI。
關注點分離
關注點分離的設計原則為,應用程式應區分成不同類別,每個類別具有不同責任。
透過模型使用 UI
另一個重要原則是,您應透過模型 (建議為持續性模型) 使用 UI。模型是負責處理應用程式資料的元件。模型與應用程式中的 Views
和應用程式元件無關,因此不受應用程式的生命週期和相關關注點影響。
Android 架構中的主要類別或元件包括 UI controller (activity/fragment)、ViewModel
、LiveData
和 Room
。這些元件負責生命週期的部分複雜度,且可避免發生生命週期相關問題。您將在後續的程式碼研究室中學習 LiveData
和 Room
。
下圖為架構的基本部分:
UI Controller (activity/fragment)
activity 和 fragment 為 UI controller。UI controller 可控制 UI,方法包括在畫面產生 view、擷取使用者事件,以及與使用者互動之 UI 相關的任何其他內容。App 中的資料或與這些資料相關的決策邏輯不應屬於 UI controller 類別。
Android 系統可能會因為特定使用者互動或記憶體不足等系統情況,而隨時刪除 UI controller。由於這些事件不在您的控管之下,您不應在 UI controller 中儲存任何應用程式資料或狀態。反之,應該在 ViewModel
中新增資料相關的決策邏輯。
舉例來說,Unscramble 應用程式中的打散字詞、分數和字詞計數會顯示於 fragment (UI controller) 中。決策程式碼應位於 ViewModel
中,例如判斷下一個打散的字詞,以及分數和字詞計數的計算。
ViewModel
ViewModel
是 view 中顯示的應用程式資料(data)模型。模型是負責處理應用程式資料的元件。其可讓您的應用程式遵循透過模型使用 UI 的架構原則。
activity 或 fragment 遭到 Android 架構刪除並重新建立時,未刪除的應用程式相關資料會由 ViewModel
進行儲存。在設定變更期間,系統會自動保留 ViewModel
物件 (不會像 activity 或 fragment instance一般遭到刪除),讓處於保留狀態的資料立即用於下一個 activity 或 fragment instance。
如要在應用程式中實作 ViewModel
,請擴充架構元件庫中的 ViewModel
類別,並將應用程式資料儲存在該類別中。
★總結:
fragment/activity (UI controller) 責任 | ViewModel 責任 |
---|---|
activity 和 fragment 應負責在畫面中產生 view 和資料(data) ,並回應使用者事件。 | ViewModel 負責保留及處理 UI 所需的所有資料。其不得存取 view 階層 (例如 view binding 物件),或保留 activity/fragment 的引用。 |
新增 ViewModel
在這項工作中,您需將 ViewModel
新增至應用程式,以儲存應用程式資料 (打散的字詞、字詞計數和分數)。
您的應用程式架構如下。MainActivity
包含 GameFragment
,而 GameFragment
會從 GameViewModel
存取遊戲的相關資訊。

在 Android Studio 中,「Android」 視窗的「Gradle Scripts」資料夾下,開啟
build.gradle(Module:Unscramble.app)
檔案。如要在應用程式中使用
ViewModel
,請確認dependencies
區塊中具有 ViewModel 程式庫依附元件。此步驟已經完成。視程式庫的最新版本而定,所產生程式碼中的程式庫版本編號可能有所不同。
1 | // ViewModel |
- 建立名為
GameViewModel
的新 Kotlin 類別檔案。在「Android」視窗中,於「ui.game」資料夾上按一下滑鼠右鍵。選取「New」>「Kotlin File/Class」。

輸入名稱
GameViewModel
,然後從清單中選取「Class」。將
GameViewModel
變更為ViewModel
的子類別。ViewModel
為抽象類別,因此您必須將其擴充,才能在應用程式中使用。請參閱下方的GameViewModel
類別定義。
1 | class GameViewModel : ViewModel() { |
將 ViewModel 附加至 fragment
如要建立 ViewModel
與 UI controller (activity/fragment) 的關聯,請在 UI controller 內建立 ViewModel
的引用 (物件)。
在這個步驟中,您會在對應的 UI controller (GameFragment) 中建立 GameViewModel
的 object instance。
- 在
GameFragment
類別頂部新增GameViewModel
類型的屬性。 - 使用
by viewModels()
Kotlin 屬性委派功能將GameViewModel
初始化。您將在下一節深入瞭解。
1 | private val viewModel: GameViewModel by viewModels() |
- 如果 Android Studio 顯示提示,請匯入
androidx.fragment.app.viewModels
。
Kotlin 屬性委派
在 Kotlin 中,每個可變動 (var) 屬性都會自動產生屬性的 getter
和 setter
函式。當您指派值或讀取屬性值時,系統將會呼叫 setter
和 getter
函式。
唯讀屬性 (val) 與可變動屬性稍有不同。根據預設,只會產生 getter 函式。讀取唯讀屬性的值時,系統會呼叫 getter 函式。
- Kotlin 中的屬性委派功能可協助您將
getter-setter
責任移交給其他類別。 - 此類別 (稱為「委派類別」) 可提供屬性的
getter
和setter
函式,並處理其變更。
委派屬性是使用 by
子句和委派類別 instance 來定義:
1 | // Syntax for property delegation |
在應用程式中,如使用預設的 GameViewModel
建構函式初始化 view model,則如下所示:
1 | private val viewModel = GameViewModel() |
裝置經過設定變更後,應用程式將失去 viewModel
引用的狀態。舉例來說,如果您旋轉裝置,系統就會刪除並重新建立 activity,而您將再次擁有具備初始狀態的新 view model。
請改用屬性委派方法,並將 viewModel
物件的責任委派給另一個名為 viewModels
的類別。這代表當您存取 viewModel
物件時,該物件會由委派類別 viewModels
於內部進行處理。委派類別會在第一次存取時為您建立 viewModel
物件,並透過設定變更保留其值,並在要求時傳回該值。
將資料移至 ViewModel
將應用程式的 UI data 與 UI controller (Activity / Fragment 類別) 分離,以便您充分遵循上述單一責任原則。您的 activity 和 fragment 負責在畫面中產生 view 和 data,ViewModel 則負責保留及處理 UI 所需的所有資料。
在這項工作中,您必須將資料變數從 GameFragment
移至 GameViewModel
類別。
- 將資料變數
score
、currentWordCount
、currentScrambledWord
移至GameViewModel
類別。
1 | class GameViewModel : ViewModel() { |
- 請注意未解決的參照錯誤。這是因為屬性僅供
ViewModel
使用,且無法由 UI 控制器進行存取。您將在下一個步驟修正這些錯誤。
如要解決這個問題,屬性的可見度修飾符不得為 public
,資料不可由其他類別編輯。此操作具有風險,因為外部類別可能會以非預期的方式,變更未遵循檢視模式中指定遊戲規則的資料。舉例來說,外部類別可以將 score
變更為負值。
ViewModel
內的資料應可編輯,因此應為 private
和 var
。在 ViewModel
外部,資料應可供讀取,但無法編輯,因此資料應以 public
和 val
的形式呈現。為了達成這個行為,Kotlin 提供名為幕後屬性(Backing properties)的功能。
幕後屬性
幕後屬性可讓您從 getter
傳回確切物件以外的項目。
您已瞭解 Kotlin 架構會為每個屬性產生 getter
和 setter
。getter
和 setter
方法可覆寫此類方法 (一或兩種),並提供您自訂的行為。如要實作幕後屬性,您將會覆寫 getter
方法,以傳回唯讀資料版本。幕後屬性範例:
1 | // Declare private mutable variable that can only be modified |
舉例來說,假設您想在應用程式內,將應用程式資料設為僅供 ViewModel
使用:
在 ViewModel
類別中:
_count
屬性為private
,且可變動。因此,只能在ViewModel
類別中進行存取及編輯。慣例是在private
屬性字首加上底線。
ViewModel
類別外:
- Kotlin 中的預設瀏覽權限修飾符為
public
,因此count
是公開狀態,且可從 UI controller 等其他類別存取。由於只有get()
方法遭到覆寫,因此這個屬性不可變動且為唯讀。外部類別存取這個屬性時,系統會傳回_count
的值,且該值無法修改。這種做法可確保ViewModel
中的應用程式資料不會受到外部類別非必要和不安全的變更,但可讓外部呼叫者安全地存取其值。
將幕後屬性新增至 currentScrambledWord
- 在
GameViewModel
中,變更currentScrambledWord
宣告以新增幕後屬性。目前您只能在GameViewModel
中存取及編輯_currentScrambledWord
。UI controllerGameFragment
可以使用唯讀屬性currentScrambledWord
讀取其值。
1 | private var _currentScrambledWord = "test" |
- 在
GameFragment
中,更新updateNextWordOnScreen()
方法,以使用唯讀的viewModel
屬性currentScrambledWord
。
1 | private fun updateNextWordOnScreen() { |
- 在
GameFragment
中,刪除onSubmitWord()
和onSkipWord()
方法中的程式碼。您將於稍後實作這些方法。您現在應該能夠正確編譯程式碼,而不會產生錯誤。
ViewModel 的生命週期
只要 activity 或 fragment 的範圍保持運作,該架構就會使 ViewModel
保持運作。如果 ViewModel
的擁有者因設定變更 (例如螢幕旋轉) 而遭到刪除,系統並不會將其刪除。擁有者的新 instance 會重新連線至現有的 ViewModel
instance,如下圖所示:

瞭解 ViewModel 生命週期
在 GameViewModel
和 GameFragment
中新增記錄功能,以進一步瞭解 ViewModel
的生命週期。
- 在
GameViewModel.kt
中,新增含有記錄陳述式的init
區塊。
1 | class GameViewModel : ViewModel() { |
- Kotlin 會提供初始化器區塊 (也稱為
init
區塊),做為物件 instance 初始化期間,所需初始設定程式碼的位置。初始化器區塊的前面會加上init
關鍵字,後為大括號 {}。這個程式碼區塊會在首次建立並初始化物件 instance 時執行。
在
GameViewModel
類別中,覆寫onCleared()
方法(Control+o)。在您卸離相關 fragment 或 activity 完成後,系統會將ViewModel
刪除。在ViewModel
刪除之前,系統會呼叫onCleared()
callback。在
onCleared()
中新增記錄(log)陳述式,以追蹤GameViewModel
生命週期。
1 | override fun onCleared() { |
- 在
GameFragment
的onCreateView()
中找到 binding 物件引用後,請新增 log 陳述式以記錄 fragment 的建立作業。初次建立及每次重新建立 (例如設定變更等事件) fragment 時,系統會觸發onCreateView()
callback。
1 | override fun onCreateView( |
- 在
GameFragment
中覆寫onDetach()
callback 方法,以在對應的 activity 和 fragment 刪除時呼叫此方法。
1 | override fun onDetach() { |
- 在 Android Studio 中執行應用程式,開啟「Logcat」視窗,然後篩選
GameFragment
。請注意,GameFragment
和GameViewModel
已建立。
1 | com.example.android.unscramble D/GameFragment: GameFragment created/re-created! |
- 在裝置或模擬器上啟用自動旋轉設定,並變更螢幕方向數次。每次都會刪除並重新建立
GameFragment
,但GameViewModel
只會建立一次,而且不會在每次呼叫時重新建立或刪除。
1 | com.example.android.unscramble D/GameFragment: GameFragment created/re-created! |
- 離開遊戲,或使用返回箭頭離開應用程式。
GameViewModel
已刪除,並呼叫onCleared()
callback。GameFragment
已刪除。
1 | com.example.android.unscramble D/GameFragment: GameViewModel destroyed! |
填入 ViewModel
在這項工作中,您將使用輔助方法進一步填入 GameViewModel
,以便取得下一個字詞、驗證玩家的字詞能否增加分數,並檢查字詞計數來結束遊戲。
延遲初始化
通常在宣告變數時,您必須先提供初始值。不過,如果您還沒準備好指派值,可以稍後再進行初始化。為了延遲在 Kotlin 中初始化屬性,您可以使用關鍵字 lateinit
,表示延遲初始化。如果您確保在使用前先初始化屬性,可以使用 lateinit
宣告屬性。記憶體必須先初始化,才能分配給變數。如果您在初始化之前就嘗試存取變數,應用程式將會異常終止。
取得下一個字詞
在 GameViewModel
類別中建立 getNextWord()
方法,且具備下列功能:
- 從
allWordsList
取得隨機字詞,並將其指派給currentWord
. - 將
currentWord
中的字母打散,以產生打散的字詞,並將其指派給currentScrambledWord
- 處理打散與未打散字詞相同的情形。
- 請確定您在遊戲期間不會重複出現相同的字詞。
請在 GameViewModel
類別中執行下列步驟:
- 於
GameViewModel
, 中,新增MutableList<String>
類型的新類別變數 (名為wordsList
),以保留遊戲中使用的字詞清單,避免重複出現。 - 新增另一個名為
currentWord
的類別變數,以保留玩家嘗試重組的字詞。由於您稍後會初始化此屬性,請使用lateinit
關鍵字。
1 | private var wordsList: MutableList<String> = mutableListOf() |
- 在
init
區塊上新增名為getNextWord()
的新private
方法,且無不會傳回任何內容的參數。 - 從
allWordsList
取得隨機字詞,並將其指派給currentWord
。
1 | private fun getNextWord() { |
- 在
getNextWord()
中,將currentWord
字串轉換為字元陣列,並將其指派給名為tempWord
的新val
。如要打散字詞,請使用 Kotlin 方法shuffle()
隨機變換此陣列中的字元。
1 | val tempWord = currentWord.toCharArray() |
Array
與MutableList
相似,但其初始化時有固定的大小。Array
無法展開或縮減大小 (您必須複製陣列才能調整大小),而MutableList
具有add()
和remove()
函式,因此可以調整大小。
- 有時,隨機變換後的字元順序會與原始字詞相同。在要隨機變換的呼叫周圍加上下列
while
迴圈,以在打散字詞不同於原始字詞前持續進行迴圈。
1 | while (String(tempWord).equals(currentWord, false)) { |
- 新增
if-else
區塊,以確認是否已使用字詞。如果wordsList
包含currentWord
,請呼叫getNextWord()
。如果沒有,請以剛打散的字詞更新_currentScrambledWord
值、增加字詞計數,並將新字詞新增至wordsList
。
1 | if (wordsList.contains(currentWord)) { |
- 以下是完整的
getNextWord()
方法,供您參考。
1 | /* |
延遲初始化 currentScrambledWord
現在您已建立 getNextWord()
方法,以取得下一個打散的字詞。初次初始化 GameViewModel
時,系統會呼叫此方法。使用 init
區塊初始化類別中的 lateinit
屬性 (例如目前字詞)。如此一來,畫面上顯示的第一個字詞會是打散的字詞,而不是「test」。
- 執行應用程式。請注意,第一個字詞一律為「test」。
- 如要在應用程式起始處顯示打散的字詞,請呼叫
getNextWord()
方法,藉此讓系統更新currentScrambledWord
。呼叫GameViewModel
init
區塊中的getNextWord()
方法。
1 | init { |
- 在
_currentScrambledWord
屬性中加入lateinit
修飾符。由於未提供初始值,請明確提及String
資料類型。
1 | private lateinit var _currentScrambledWord: String |
- 執行應用程式。請注意,應用程式啟動時會顯示新的打散字詞。太棒了!

新增 Helper 方法
接下來,請加入 Helper 方法來處理和修改 ViewModel
中的資料。您將在後續工作中使用此方法。
在 GameViewModel
類別中,新增另一個 nextWord()
. 方法。接著從清單中取得下一個字詞,並在字詞計數少於 MAX_NO_OF_WORDS
時傳回 true
。
1 | /* |
對話方塊
在範例程式碼中,即使已遊玩 10 個字詞,遊戲也不會結束。請修改應用程式,在使用者遊玩 10 個字詞後結束遊戲,並顯示含有最終分數的對話方塊。使用者還可以選擇重新遊玩或離開遊戲。

這是您初次在應用程式中新增對話方塊。對話方塊是一個小視窗 (畫面),可提示使用者做出決定或輸入額外資訊。一般而言,如果對話方塊未填滿整個畫面,則使用者必須執行操作才能繼續操作。Android 提供不同類型的對話方塊。在本程式碼研究室中,您將瞭解「快訊對話方塊」。
快訊對話方塊剖析
- 快訊對話方塊
- 標題 (選填)
- 訊息
- 文字按鈕
實作最終分數對話方塊
使用質感設計元件庫中的 MaterialAlertDialog
,在應用程式中加入符合質感設計指南的對話方塊。由於對話方塊與 UI 相關,因此 GameFragment
將負責建立並顯示最終分數對話方塊。
- 首先,在
score
變數中新增幕後屬性。在GameViewModel
中,將score
變數宣告變更為以下內容。
1 | private var _score = 0 |
- 在
GameFragment
中,新增名為showFinalScoreDialog()
的私人函式。如要建立MaterialAlertDialog
,請使用MaterialAlertDialogBuilder
類別逐步建立對話方塊的內容。使用 fragment 的requireContext()
方法呼叫傳遞內容的MaterialAlertDialogBuilder
建構函式。requireContext()
方法會傳回非空值的Context
。
1 | /* |
- 顧名思義,
Context
是指應用程式、activity 或 fragment 的結構定義或目前狀態。其包含與 activity、fragment 或應用程式相關的資訊。其通常用於存取資源、資料庫和其他系統服務。在這個步驟中,您必須傳遞 fragment 結構定義,以建立快訊對話方塊。 - 如果 Android Studio 顯示提示,請
import
com.google.android.material.dialog.MaterialAlertDialogBuilder
。
- 加入程式碼以設定快訊對話方塊的標題,請使用
strings.xml
的字串資源。
1 | MaterialAlertDialogBuilder(requireContext()) |
- 設定訊息以顯示最終分數,並使用先前新增的分數變數 (
viewModel.score
) 唯讀版本。
1 | .setMessage(getString(R.string.you_scored, viewModel.score)) |
使用
setCancelable()
方法並傳遞false
,使快訊對話方塊在按下返回鍵時無法取消。使用
setNegativeButton()
和setPositiveButton()
方法新增「離開」和「再玩一次」兩個文字按鈕。從 lambda 分別呼叫exitGame()
和restartGame()
。
1 | .setNegativeButton(getString(R.string.exit)) { _, _ -> |
- 這個語法對您來說可能較陌生,但其為
setNegativeButton(getString(R.string.exit), { _, _ -> exitGame()})
的簡寫,其中setNegativeButton()
方法會納入兩個參數:String
及可用 lambda 表示的DialogInterface.OnClickListener()
函式。如果傳入的最後一個引數是函式,您可以將 lambda 運算式放在括號外。這就是所謂的結尾 lambda 語法。系統接受這兩種程式碼編寫方式 (lambda 位於括號內或外)。這同樣適用於setPositiveButton
函式。
- 最後加入
show()
,即可建立並顯示快訊對話方塊。
1 | .show() |
- 以下是完整的
showFinalScoreDialog()
方法,供您參考。
1 | /* |
實作「Submit」按鈕的 OnClickListener
在這項工作中,您要使用 ViewModel
和新增的快訊對話方塊,實作「Submit」按鈕點擊事件監聽器的遊戲邏輯。
顯示打散的字詞
- 如果您尚未在
GameFragment
中刪除onSubmitWord()
內的程式碼 (輕觸「Submit」按鈕時會呼叫此程式碼),請先完成這項操作。 - 請在
viewModel.nextWord()
方法的傳回值新增檢查。如果為true
,則可以使用其他字詞,因此請使用updateNextWordOnScreen()
更新畫面上打散的字詞。否則遊戲將會結束,並顯示含有最終分數的快訊對話方塊。
1 | private fun onSubmitWord() { |
執行應用程式!使用一些字詞進行遊戲。別忘了,您尚未實作「Skip」按鈕,因此無法略過該字詞。
請注意,文字欄位不會更新,因此玩家必須手動刪除上一個字詞。快訊對話方塊中的最終分數永遠為零。您將在後續步驟中修正這些錯誤。


新增 Helper 方法以驗證玩家字詞
- 在
GameViewModel
中,新增名為increaseScore()
的新私人方法,且不含參數和傳回值。透過SCORE_INCREASE
將score
變數提高。
1 | private fun increaseScore() { |
在
GameViewModel
中,新增名為isUserWordCorrect()
的 Helper 方法,其會傳回Boolean
並將玩家字詞String
做為參數。在
isUserWordCorrect()
中驗證玩家的字詞,如果答案正確無誤,則增加分數。這會更新快訊對話方塊中的最終分數。
更新文字欄位
顯示文字欄位中的錯誤
針對 Material 文字欄位,TextInputLayout
內建能顯示錯誤訊息的功能。舉例來說,在下列文字欄位中,標籤顏色有所變更、顯示錯誤圖示、顯示錯誤訊息等。

如要在文字欄位中顯示錯誤,您可以在程式碼中以動態方式設定錯誤訊息,或在版面配置檔案中以靜態方式設定錯誤訊息。設定及重設程式碼中錯誤的範例如下:
1 | // Set error text |
在範例程式碼中,您會發現已定義 setErrorTextField(error: Boolean)
Helper 方法,以協助您設定及重設文字欄位中的錯誤。根據是否要在文字欄位中顯示錯誤,使用 true
或 false
做為輸入參數,呼叫此方法。
範例程式碼中的程式碼片段
1 | private fun setErrorTextField(error: Boolean) { |
在這項工作中,您會實作 onSubmitWord()
方法。使用者提交字詞時,您可透過檢查原始字詞,驗證使用者的答案。如果字詞正確無誤,請前往下一個字詞 (如果遊戲已結束,則顯示對話方塊)。如果字詞有誤,請在文字欄位中顯示錯誤,並繼續使用目前的字詞。
- 在
GameFragment
中(onSubmitWord()
開始的部分),建立名為playerWord
的val
。從binding
變數的文字欄位中擷取文字,將玩家的字詞儲存在其中。
1 | private fun onSubmitWord() { |
在
onSubmitWord()
中的playerWord
宣告下方,驗證玩家的字詞。新增 if 陳述式,以使用isUserWordCorrect()
方法檢查玩家的字詞,並傳入playerWord
。在 if 區塊中,重設文字欄位,呼叫
setErrorTextField
傳入false
。將現有程式碼移至 if 區塊中。
1 | private fun onSubmitWord() { |
- 如果使用者字詞不正確,請在文字欄位中顯示錯誤訊息。將 else 區塊新增至上述 if 區塊,並呼叫
setErrorTextField()
傳入true
。已完成的onSubmitWord()
方法應如下所示:
1 | private fun onSubmitWord() { |
- 執行應用程式,並透過一些字詞進行遊戲。如果玩家的字詞正確無誤,按一下「Submit」按鈕即可清除字詞,否則系統會顯示「Try again!」的訊息。請注意,「略過」按鈕目前仍未運作。您將在下一個工作中加入此實作。

實作略過按鈕
在這項工作中,您要新增 onSkipWord()
實作,用於處理使用者輕觸「Skip」按鈕時的情況。
- 與
onSubmitWord()
類似,請在onSkipWord()
方法中新增條件。如為true
,請在畫面上顯示文字並重設文字欄位。如為false
,且這回合沒有其他字詞,則顯示含有最終分數的快訊對話方塊。
1 | /* |
- 執行您的應用程式。遊玩遊戲。請注意,「略過」和「提交」按鈕可正常運作。非常好!
確認 ViewModel 將會保留資料
在這項工作中,於 GameFragment
中新增 log,以觀察在設定變更期間,您的應用程式資料是否會保留在 ViewModel
中。如要存取 GameFragment
中的 currentWordCount
,您必須使用幕後屬性公開唯讀版本。
在
GameViewModel
中,在currentWordCount
變數上按一下滑鼠右鍵,然後選取「Refactor」>「Rename…」。在新名稱前加上底線_currentWordCount
。新增支援欄位。
1 | private var _currentWordCount = 0 |
- 在
GameFragment
內的onCreateView()
中,在回傳敘述上方新增另一個 log,以列印應用程式資料、字詞、分數和字詞計數。
1 | Log.d("GameFragment", "Word: ${viewModel.currentScrambledWord} " + |
- 在 Android Studio 中,開啟「Logcat」,並篩選
GameFragment
。執行應用程式,使用一些字詞進行遊戲。變更裝置的螢幕方向。fragment (UI controller) 會刪除並重新建立。觀察記錄。您現在可以看到分數和字詞計數增加!
1 | com.example.android.unscramble D/GameFragment: GameFragment created/re-created! |
- 請注意,螢幕方向變更時,應用程式資料會保存在
ViewModel
中。您將在後續的程式碼研究室中使用LiveData
和 data biniding,更新 UI 上的分數值和字詞計數。
更新遊戲重新啟動邏輯
再次執行應用程式,使用所有字詞進行遊戲。在「Congratulations!」快訊對話方塊中,按一下「PLAY AGAIN」。由於字詞計數現已達到
MAX_NO_OF_WORDS
值,因此應用程式無法讓您再玩一次。您必須將字詞計數重設為 0,才能再次從頭開始遊戲。如要重設應用程式資料,請在
GameViewModel
中新增名為reinitializeData()
的方法。將分數和字詞計數設為0
。清除字詞清單並呼叫getNextWord()
方法。
1 | /* |
- 在
GameFragment
頂端的方法restartGame()
中,呼叫新建立的方法reinitializeData()
。
1 | private fun restartGame() { |
- 再次執行應用程式。開始遊戲。看到祝賀對話方塊時,請按一下「Play Again」。現在,您應該可以成功再次遊玩遊戲!
應用程式最終畫面應如下所示。這個遊戲會顯示十個隨機打散的字詞,讓玩家進行重組。您可選擇「略過」字詞,或猜測字詞,然後輕觸「提交」。如果答案正確,分數將會增加。答案不正確會在文字欄位中顯示錯誤狀態。隨著每個新字詞的進行,字詞計數也會增加。
請注意,畫面上顯示的分數和字詞計數尚未更新。但這些資訊仍會儲存在檢視模型中,並在設定變更 (例如裝置旋轉) 期間保留。您將在後續的程式碼研究室中,更新畫面上的分數和字詞計數。


遊戲將在 10 個字詞後結束,畫面上會出現快訊對話方塊,顯示最終分數和結束遊戲或再玩一次的選項。

恭喜!您已建立第一個 ViewModel
,並成功儲存資料!
總結
- Android 應用程式架構指南建議將具有不同責任的類別分離,並透過模型使用 UI。
- UI Controller 是一種 UI 類別,例如
Activity
或Fragment
。UI 控制器只能包含處理 UI 和作業系統互動的邏輯;其不應做為在 UI 中顯示的資料來源。將該資料和任何相關的邏輯存放在ViewModel
中。 ViewModel
類別會儲存和管理 UI 相關資料。ViewModel
類別可在螢幕旋轉等變更時保留資料。ViewModel
是建議使用的 Android 架構元件之一。