這個程式碼研究室是 Android Kotlin 基礎知識課程的一部分。如果您按部就班完成程式碼研究室,就能充分體驗到本課程的價值。所有課程程式碼研究室都列在 Android Kotlin 基礎知識程式碼研究室到達網頁。
簡介
本程式碼研究室會回顧如何搭配使用 ViewModel
和片段來實作導覽功能。請注意,我們的目標是將導覽「時機」的邏輯放入 ViewModel
,但路徑定義在片段和導覽檔案中。如要達成這個目標,您可以使用檢視畫面模型、片段、LiveData
和觀察器。
本程式碼研究室的最後一個部分,會說明如何以最少的程式碼追蹤按鈕狀態,確保使用者只有在適當的時機才能啟用及點選按鈕。
必備知識
您必須已經熟悉下列項目:
- 使用活動、片段和檢視區塊建構基本使用者介面 (UI)。
- 在片段之間導覽,並使用
safeArgs
在片段之間傳遞資料。 - 查看模型、模型工廠、轉換和
LiveData
及其觀察者。 - 如何建立
Room
資料庫、建立資料存取物件 (DAO) 及定義實體。 - 如何使用協同程式進行資料庫互動,以及處理其他長時間執行的工作。
課程內容
- 如何更新資料庫中現有的睡眠品質記錄。
- 如何使用
LiveData
追蹤按鈕狀態。 - 如何顯示回應事件的訊息列。
學習內容
- 擴充 TrackMySleepQuality 應用程式來取得品質評分、將評分新增到資料庫並顯示結果。
- 使用
LiveData
觸發顯示 Snackbar。 - 使用
LiveData
啟用及停用按鈕。
在本程式碼研究室中,您將建構 TrackMySleepQuality 應用程式的睡眠品質記錄功能,並完成 UI。
如圖所示,這個應用程式有兩個畫面,分別以片段表示。
如左側所示,第一個畫面會顯示開始和停止追蹤的按鈕。畫面上會顯示使用者的所有睡眠資料。「清除」按鈕會永久刪除應用程式為使用者收集的所有資料。
右側的第二個畫面用於選取睡眠品質評分。應用程式會以數字表示評分。為了方便開發,應用程式會同時顯示臉部圖示和對應的數字。
使用者流程如下:
- 使用者開啟應用程式,並看到睡眠追蹤畫面。
- 使用者輕觸「開始」按鈕。系統會記錄開始時間並顯示。「開始」按鈕已停用,「停止」按鈕已啟用。
- 使用者輕觸「停止」按鈕。系統會記錄結束時間,並開啟睡眠品質畫面。
- 使用者選取睡眠品質圖示。螢幕會關閉,追蹤畫面則會顯示睡眠結束時間和睡眠品質。「停止」按鈕已停用,「開始」按鈕已啟用。應用程式已準備好迎接下一個夜晚。
- 只要資料庫中有資料,「清除」按鈕就會啟用。使用者輕觸「清除」按鈕後,所有資料都會遭到清除,且無法復原,系統也不會顯示「確定要清除嗎?」訊息。
這個應用程式採用簡化架構,如下圖所示 (完整架構的背景資訊)。應用程式只會使用下列元件:
- UI 控制器
- 查看模型和
LiveData
- Room 資料庫
本程式碼研究室假設您瞭解如何使用片段和導覽檔案實作導覽功能。為節省您的時間,我們提供許多程式碼。
步驟 1:檢查程式碼
- 如要開始,請繼續使用上一個程式碼研究室結尾的程式碼,或下載範例程式碼。
- 在範例程式碼中檢查
SleepQualityFragment
。這個類別會擴充版面配置、取得應用程式,並傳回binding.root
。 - 在設計編輯器中開啟 navigation.xml。您會看到從
SleepTrackerFragment
到SleepQualityFragment
的導覽路徑,以及從SleepQualityFragment
返回SleepTrackerFragment
的路徑。 - 檢查 navigation.xml 的程式碼。請特別留意名為
sleepNightKey
的<argument>
。
使用者從SleepTrackerFragment
前往SleepQualityFragment,
時,應用程式會將sleepNightKey
傳遞至SleepQualityFragment
,以更新當晚的睡眠資料。
步驟 2:新增睡眠品質追蹤功能的導覽
導覽圖已包含從 SleepTrackerFragment
到 SleepQualityFragment
的路徑,以及返回路徑。不過,實作從一個片段導覽至下一個片段的點擊處理常式尚未編碼。您現在要在 ViewModel
中新增該程式碼。
在點擊事件處理常式中,您可以設定 LiveData
,在您希望應用程式前往其他目的地時變更該值。片段會觀察這個 LiveData
。資料變更時,片段會前往目的地,並告知檢視模型已完成作業,這會重設狀態變數。
- 開啟
SleepTrackerViewModel
。您需要新增導覽功能,讓使用者輕觸「停止」按鈕時,應用程式會導覽至SleepQualityFragment
收集品質評分。 - 在
SleepTrackerViewModel
中,建立LiveData
,在您希望應用程式前往SleepQualityFragment
時變更。使用封裝功能,只向ViewModel
公開可取得的LiveData
版本。
您可以將這段程式碼放在類別主體頂層的任何位置。
private val _navigateToSleepQuality = MutableLiveData<SleepNight>()
val navigateToSleepQuality: LiveData<SleepNight>
get() = _navigateToSleepQuality
- 新增
doneNavigating()
函式,重設觸發導覽的變數。
fun doneNavigating() {
_navigateToSleepQuality.value = null
}
- 在「Stop」(停止) 按鈕的點擊處理常式中,
onStopTracking()
觸發導覽至SleepQualityFragment
。在函式結尾,將 _navigateToSleepQuality
變數設為launch{}
區塊內的最後一個項目。請注意,這個變數會設為night
。如果這個變數有值,應用程式會前往SleepQualityFragment
,並傳遞 night。
_navigateToSleepQuality.value = oldNight
SleepTrackerFragment
需要觀察 _navigateToSleepQuality
,應用程式才能知道何時要導覽。在SleepTrackerFragment
的onCreateView()
中,為navigateToSleepQuality()
新增觀察器。請注意,這項匯入作業不明確,您需要匯入androidx.lifecycle.Observer
。
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer {
})
- 在觀察器區塊中,導覽並傳遞目前晚上的 ID,然後呼叫
doneNavigating()
。如果匯入作業不明確,請匯入androidx.navigation.fragment.findNavController
。
night ->
night?.let {
this.findNavController().navigate(
SleepTrackerFragmentDirections
.actionSleepTrackerFragmentToSleepQualityFragment(night.nightId))
sleepTrackerViewModel.doneNavigating()
}
- 建構並執行應用程式。依序輕觸「開始」和「停止」,即可前往
SleepQualityFragment
畫面。如要返回,請使用系統返回按鈕。
在這項工作中,你會記錄睡眠品質,然後返回睡眠追蹤器片段。顯示畫面應自動更新,向使用者顯示更新後的值。您需要建立 ViewModel
和 ViewModelFactory
,並更新 SleepQualityFragment
。
步驟 1:建立 ViewModel 和 ViewModelFactory
- 在
sleepquality
套件中,建立或開啟 SleepQualityViewModel.kt。 - 建立
SleepQualityViewModel
類別,並將sleepNightKey
和資料庫做為引數。如同SleepTrackerViewModel
,您需要從工廠傳入database
。您也需要從導覽中傳入sleepNightKey
。
class SleepQualityViewModel(
private val sleepNightKey: Long = 0L,
val database: SleepDatabaseDao) : ViewModel() {
}
- 在
SleepQualityViewModel
類別中,定義Job
和uiScope
,並覆寫onCleared()
。
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
- 如要使用與上述相同的模式返回
SleepTrackerFragment
,請宣告_navigateToSleepTracker
。實作navigateToSleepTracker
和doneNavigating()
。
private val _navigateToSleepTracker = MutableLiveData<Boolean?>()
val navigateToSleepTracker: LiveData<Boolean?>
get() = _navigateToSleepTracker
fun doneNavigating() {
_navigateToSleepTracker.value = null
}
- 為所有睡眠品質圖片建立一個點擊處理常式
onSetSleepQuality()
。
使用與先前程式碼研究室相同的協同程式模式:
- 在
uiScope
中啟動協同程式,並切換至 I/O 調度器。 - 使用
sleepNightKey
取得tonight
。 - 設定睡眠品質。
- 更新資料庫。
- 觸發導覽。
請注意,以下程式碼範例會在點擊事件處理常式中完成所有工作,而不是在不同環境中分解資料庫作業。
fun onSetSleepQuality(quality: Int) {
uiScope.launch {
// IO is a thread pool for running operations that access the disk, such as
// our Room database.
withContext(Dispatchers.IO) {
val tonight = database.get(sleepNightKey) ?: return@withContext
tonight.sleepQuality = quality
database.update(tonight)
}
// Setting this state variable to true will alert the observer and trigger navigation.
_navigateToSleepTracker.value = true
}
}
- 在
sleepquality
套件中,建立或開啟SleepQualityViewModelFactory.kt
,然後新增SleepQualityViewModelFactory
類別,如下所示。這個類別使用的樣板程式碼版本與您先前看過的相同。請先檢查程式碼,再繼續操作。
class SleepQualityViewModelFactory(
private val sleepNightKey: Long,
private val dataSource: SleepDatabaseDao) : ViewModelProvider.Factory {
@Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(SleepQualityViewModel::class.java)) {
return SleepQualityViewModel(sleepNightKey, dataSource) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
步驟 2:更新 SleepQualityFragment
- 開啟
SleepQualityFragment.kt
。 - 在
onCreateView()
中取得application
後,您需要取得導覽隨附的arguments
。這些引數位於SleepQualityFragmentArgs
中。您必須從套件中擷取這些檔案。
val arguments = SleepQualityFragmentArgs.fromBundle(arguments!!)
- 接著取得
dataSource
。
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
- 建立工廠,並傳入
dataSource
和sleepNightKey
。
val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource)
- 取得
ViewModel
參照。
val sleepQualityViewModel =
ViewModelProviders.of(
this, viewModelFactory).get(SleepQualityViewModel::class.java)
- 將
ViewModel
新增至繫結物件。(如果看到繫結物件發生錯誤,請暫時忽略。)
binding.sleepQualityViewModel = sleepQualityViewModel
- 新增觀察者。系統顯示提示訊息時,請匯入
androidx.lifecycle.Observer
。
sleepQualityViewModel.navigateToSleepTracker.observe(this, Observer {
if (it == true) { // Observed state is true.
this.findNavController().navigate(
SleepQualityFragmentDirections.actionSleepQualityFragmentToSleepTrackerFragment())
sleepQualityViewModel.doneNavigating()
}
})
步驟 3:更新版面配置檔案並執行應用程式
- 開啟
fragment_sleep_quality.xml
版面配置檔案。在<data>
區塊中,新增SleepQualityViewModel
的變數。
<data>
<variable
name="sleepQualityViewModel"
type="com.example.android.trackmysleepquality.sleepquality.SleepQualityViewModel" />
</data>
- 為六張睡眠品質圖片各新增一個點擊處理常式,如下所示。為圖片選擇合適的品質評分。
android:onClick="@{() -> sleepQualityViewModel.onSetSleepQuality(5)}"
- 清除並重建專案。這應該可以解決繫結物件的任何錯誤。否則,請清除快取 (「File」>「Invalidate Caches / Restart」),然後重建應用程式。
恭喜!您剛才使用協同程式建構了完整的 Room
資料庫應用程式。
現在應用程式運作正常。使用者可以視需要多次輕觸「開始」和「停止」。使用者輕觸「停止」後,即可輸入睡眠品質。使用者輕觸「清除」後,系統會在背景中清除所有資料,不過,所有按鈕一律都會啟用並可點選,雖然不會導致應用程式故障,但使用者可能會建立不完整的睡眠夜間記錄。
在最後一項工作中,您將瞭解如何使用轉換對應管理按鈕顯示狀態,確保使用者只能做出正確選擇。您可以使用類似方法,在清除所有資料後顯示友善訊息。
步驟 1:更新按鈕狀態
概念是設定按鈕狀態,一開始只啟用「Start」按鈕,也就是可點選。
使用者輕觸「開始」後,「停止」按鈕會啟用,「開始」則會停用。只有在資料庫中有資料時,「清除」按鈕才會啟用。
- 開啟
fragment_sleep_tracker.xml
版面配置檔案。 - 為每個按鈕新增
android:enabled
屬性。android:enabled
屬性是布林值,表示按鈕是否已啟用。(已啟用的按鈕可以輕觸,已停用的按鈕則無法。) 為該屬性指派狀態變數的值,您稍後會定義該變數。
start_button
:
android:enabled="@{sleepTrackerViewModel.startButtonVisible}"
stop_button
:
android:enabled="@{sleepTrackerViewModel.stopButtonVisible}"
clear_button
:
android:enabled="@{sleepTrackerViewModel.clearButtonVisible}"
- 開啟
SleepTrackerViewModel
並建立三個對應變數。為每個變數指派測試該變數的轉換。
- 當
tonight
為null
時,「開始」按鈕應為啟用狀態。 - 當
tonight
不是null
時,「停止」按鈕應處於啟用狀態。 - 只有在
nights
(也就是資料庫) 包含睡眠夜數時,才應啟用「清除」按鈕。
val startButtonVisible = Transformations.map(tonight) {
it == null
}
val stopButtonVisible = Transformations.map(tonight) {
it != null
}
val clearButtonVisible = Transformations.map(nights) {
it?.isNotEmpty()
}
- 執行應用程式,然後試用按鈕。
步驟 2:使用 Snackbar 通知使用者
使用者清除資料庫後,請使用 Snackbar
小工具向使用者顯示確認訊息。Snackbar 會在畫面底部顯示訊息,提供簡短的作業回饋內容。如果逾時、使用者與螢幕上其他位置互動,或使用者將資訊列滑出螢幕,資訊列就會消失。
顯示零食列是 UI 工作,應在片段中進行。決定是否顯示 Snackbar 的動作會在 ViewModel
中進行。如要在清除資料時設定及觸發 snackbar,可以採用與觸發導覽相同的技巧。
- 在
SleepTrackerViewModel
中,建立封裝事件。
private var _showSnackbarEvent = MutableLiveData<Boolean>()
val showSnackBarEvent: LiveData<Boolean>
get() = _showSnackbarEvent
- 然後導入
doneShowingSnackbar()
。
fun doneShowingSnackbar() {
_showSnackbarEvent.value = false
}
- 在
SleepTrackerFragment
中,於onCreateView()
新增觀察器:
sleepTrackerViewModel.showSnackBarEvent.observe(this, Observer { })
- 在觀察程式區塊內,顯示 snackbar 並立即重設事件。
if (it == true) { // Observed state is true.
Snackbar.make(
activity!!.findViewById(android.R.id.content),
getString(R.string.cleared_message),
Snackbar.LENGTH_SHORT // How long to display the message.
).show()
sleepTrackerViewModel.doneShowingSnackbar()
}
- 在
SleepTrackerViewModel
中,於onClear()
方法中觸發事件。如要這麼做,請在launch
區塊中將事件值設為true
:
_showSnackbarEvent.value = true
- 建構並執行應用程式!
Android Studio 專案:TrackMySleepQualityFinal
在這款應用程式中導入睡眠品質追蹤功能,就像用新的調性演奏熟悉的樂曲。雖然詳細資料會變更,但您在本課程中先前程式碼研究室所做的基礎模式仍維持不變。瞭解這些模式後,您就能重複使用現有應用程式的程式碼,大幅加快編碼速度。以下是本課程目前為止使用的部分模式:
- 建立
ViewModel
和ViewModelFactory
,並設定資料來源。 - 觸發導覽。為區分關注事項,請將點擊事件處理常式放在檢視區塊模型中,並將導覽放在片段中。
- 使用
LiveData
封裝來追蹤及回應狀態變更。 - 搭配
LiveData
使用轉換。 - 建立單例資料庫。
- 為資料庫作業設定協同程式。
觸發導覽
您可以在導覽檔案中定義片段之間可能的導覽路徑。您可以透過幾種不同的方式,從一個片段觸發導覽至下一個片段。包括:
- 定義
onClick
處理常式,觸發導覽至目的地片段。 - 或者,如要啟用從一個片段到下一個片段的導覽功能,請按照下列步驟操作:
- 定義
LiveData
值,記錄是否應發生導覽。 - 將觀察器附加至該
LiveData
值。 - 每當需要觸發或完成導覽時,程式碼就會變更該值。
設定 android:enabled 屬性
android:enabled
屬性定義於TextView
中,並由所有子類別 (包括Button
) 繼承。android:enabled
屬性會決定是否啟用View
。「啟用」的意義因子類別而異。舉例來說,如果EditText
未啟用,使用者就無法編輯內含文字;如果Button
未啟用,使用者就無法輕觸按鈕。enabled
屬性與visibility
屬性不同。- 您可以根據其他物件或變數的狀態,使用轉換對應設定按鈕
enabled
屬性的值。
本程式碼研究室涵蓋的其他主題:
- 如要觸發通知給使用者,可以採用與觸發導覽相同的技巧。
- 您可以使用
Snackbar
通知使用者。
Udacity 課程:
Android 開發人員說明文件:
本節列出的作業可由課程講師指派給學習本程式碼研究室的學員。講師可自由採取以下行動:
- 視需要指派作業。
- 告知學員如何繳交作業。
- 為作業評分。
講師可以視需求使用全部或部分建議內容,也可以自由指派任何其他合適的作業。
如果您是自行學習本程式碼研究室,不妨利用這些作業驗收學習成果。
回答問題
第 1 題
如要讓應用程式從一個片段觸發導覽至下一個片段,其中一種方法是使用 LiveData
值,指出是否要觸發導覽。
使用名為 gotoBlueFragment
的 LiveData
值,從紅色片段觸發導覽至藍色片段的步驟為何?(可複選):
- 在
ViewModel
中,定義LiveData
值gotoBlueFragment
。 - 在
RedFragment
中觀察gotoBlueFragment
值。實作observe{}
程式碼,在適當時間導覽至BlueFragment
,然後重設gotoBlueFragment
的值,表示導覽完成。 - 請確保程式碼會在應用程式需要從
RedFragment
前往BlueFragment
時,將gotoBlueFragment
變數設為觸發導覽的值。 - 請確保程式碼為使用者點選的
View
定義onClick
處理常式,以便導覽至BlueFragment
,而onClick
處理常式會觀察goToBlueFragment
值。
第 2 題
您可以使用 LiveData
變更 Button
是否啟用 (可點選)。如何確保應用程式變更 UpdateNumber
按鈕,以便:
- 如果
myNumber
的值大於 5,按鈕就會啟用。 - 如果
myNumber
等於或小於 5,按鈕就不會啟用。
假設包含 UpdateNumber
按鈕的版面配置包含 NumbersViewModel
的 <data>
變數 (如下所示):
<data> <variable name="NumbersViewModel" type="com.example.android.numbersapp.NumbersViewModel" /> </data>
假設版面配置檔案中的按鈕 ID 如下:
android:id="@+id/update_number_button"
您還需要做些什麼?請選取所有適用選項。
- 在
NumbersViewModel
類別中,定義代表數字的LiveData
變數myNumber
。此外,請定義一個變數,並透過對myNumber
變數呼叫Transform.map()
來設定變數值,這會傳回布林值,指出數字是否大於 5。
具體來說,請在ViewModel
中新增下列程式碼:
val myNumber: LiveData<Int>
val enableUpdateNumberButton = Transformations.map(myNumber) {
myNumber > 5
}
- 在 XML 版面配置中,將
update_number_button button
的android:enabled
屬性設為NumberViewModel.enableUpdateNumbersButton
。
android:enabled="@{NumbersViewModel.enableUpdateNumberButton}"
- 在採用
NumbersViewModel
類別的Fragment
中,為按鈕的enabled
屬性新增觀察器。
具體來說,請在Fragment
中新增下列程式碼:
// Observer for the enabled attribute
viewModel.enabled.observe(this, Observer<Boolean> { isEnabled ->
myNumber > 5
})
- 在版面配置檔案中,將
update_number_button button
的android:enabled
屬性設為"Observable"
。
開始下一個課程:
如要查看本課程其他程式碼研究室的連結,請參閱 Android Kotlin 基礎知識程式碼研究室登陸頁面。