Android Kotlin 基礎知識 06.3:使用 LiveData 控制按鈕狀態

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

簡介

本程式碼研究室會回顧如何搭配使用 ViewModel 和片段來實作導覽功能。請注意,我們的目標是將導覽「時機」的邏輯放入 ViewModel,但路徑定義在片段和導覽檔案中。如要達成這個目標,您可以使用檢視畫面模型、片段、LiveData 和觀察器。

本程式碼研究室的最後一個部分,會說明如何以最少的程式碼追蹤按鈕狀態,確保使用者只有在適當的時機才能啟用及點選按鈕。

必備知識

您必須已經熟悉下列項目:

  • 使用活動、片段和檢視區塊建構基本使用者介面 (UI)。
  • 在片段之間導覽,並使用 safeArgs 在片段之間傳遞資料。
  • 查看模型、模型工廠、轉換和 LiveData 及其觀察者。
  • 如何建立 Room 資料庫、建立資料存取物件 (DAO) 及定義實體。
  • 如何使用協同程式進行資料庫互動,以及處理其他長時間執行的工作。

課程內容

  • 如何更新資料庫中現有的睡眠品質記錄。
  • 如何使用 LiveData 追蹤按鈕狀態。
  • 如何顯示回應事件的訊息列。

學習內容

  • 擴充 TrackMySleepQuality 應用程式來取得品質評分、將評分新增到資料庫並顯示結果。
  • 使用 LiveData 觸發顯示 Snackbar。
  • 使用 LiveData 啟用及停用按鈕。

在本程式碼研究室中,您將建構 TrackMySleepQuality 應用程式的睡眠品質記錄功能,並完成 UI。

如圖所示,這個應用程式有兩個畫面,分別以片段表示。

如左側所示,第一個畫面會顯示開始和停止追蹤的按鈕。畫面上會顯示使用者的所有睡眠資料。「清除」按鈕會永久刪除應用程式為使用者收集的所有資料。

右側的第二個畫面用於選取睡眠品質評分。應用程式會以數字表示評分。為了方便開發,應用程式會同時顯示臉部圖示和對應的數字。

使用者流程如下:

  • 使用者開啟應用程式,並看到睡眠追蹤畫面。
  • 使用者輕觸「開始」按鈕。系統會記錄開始時間並顯示。「開始」按鈕已停用,「停止」按鈕已啟用。
  • 使用者輕觸「停止」按鈕。系統會記錄結束時間,並開啟睡眠品質畫面。
  • 使用者選取睡眠品質圖示。螢幕會關閉,追蹤畫面則會顯示睡眠結束時間和睡眠品質。「停止」按鈕已停用,「開始」按鈕已啟用。應用程式已準備好迎接下一個夜晚。
  • 只要資料庫中有資料,「清除」按鈕就會啟用。使用者輕觸「清除」按鈕後,所有資料都會遭到清除,且無法復原,系統也不會顯示「確定要清除嗎?」訊息。

這個應用程式採用簡化架構,如下圖所示 (完整架構的背景資訊)。應用程式只會使用下列元件:

  • UI 控制器
  • 查看模型和 LiveData
  • Room 資料庫

本程式碼研究室假設您瞭解如何使用片段和導覽檔案實作導覽功能。為節省您的時間,我們提供許多程式碼。

步驟 1:檢查程式碼

  1. 如要開始,請繼續使用上一個程式碼研究室結尾的程式碼,或下載範例程式碼
  2. 在範例程式碼中檢查 SleepQualityFragment。這個類別會擴充版面配置、取得應用程式,並傳回 binding.root
  3. 在設計編輯器中開啟 navigation.xml。您會看到從 SleepTrackerFragmentSleepQualityFragment 的導覽路徑,以及從 SleepQualityFragment 返回 SleepTrackerFragment 的路徑。



  4. 檢查 navigation.xml 的程式碼。請特別留意名為 sleepNightKey<argument>

    使用者從 SleepTrackerFragment 前往 SleepQualityFragment, 時,應用程式會將 sleepNightKey 傳遞至 SleepQualityFragment,以更新當晚的睡眠資料。

步驟 2:新增睡眠品質追蹤功能的導覽

導覽圖已包含從 SleepTrackerFragmentSleepQualityFragment 的路徑,以及返回路徑。不過,實作從一個片段導覽至下一個片段的點擊處理常式尚未編碼。您現在要在 ViewModel 中新增該程式碼。

在點擊事件處理常式中,您可以設定 LiveData,在您希望應用程式前往其他目的地時變更該值。片段會觀察這個 LiveData。資料變更時,片段會前往目的地,並告知檢視模型已完成作業,這會重設狀態變數。

  1. 開啟 SleepTrackerViewModel。您需要新增導覽功能,讓使用者輕觸「停止」按鈕時,應用程式會導覽至 SleepQualityFragment 收集品質評分。
  2. SleepTrackerViewModel 中,建立 LiveData,在您希望應用程式前往 SleepQualityFragment 時變更。使用封裝功能,只向 ViewModel 公開可取得的 LiveData 版本。

    您可以將這段程式碼放在類別主體頂層的任何位置。
private val _navigateToSleepQuality = MutableLiveData<SleepNight>()

val navigateToSleepQuality: LiveData<SleepNight>
   get() = _navigateToSleepQuality
  1. 新增 doneNavigating() 函式,重設觸發導覽的變數。
fun doneNavigating() {
   _navigateToSleepQuality.value = null
}
  1. 在「Stop」(停止) 按鈕的點擊處理常式中,onStopTracking() 觸發導覽至 SleepQualityFragment在函式結尾,將 _navigateToSleepQuality 變數設為 launch{} 區塊內的最後一個項目。請注意,這個變數會設為 night。如果這個變數有值,應用程式會前往 SleepQualityFragment,並傳遞 night。
_navigateToSleepQuality.value = oldNight
  1. SleepTrackerFragment 需要觀察 _navigateToSleepQuality,應用程式才能知道何時要導覽。在 SleepTrackerFragmentonCreateView() 中,為 navigateToSleepQuality() 新增觀察器。請注意,這項匯入作業不明確,您需要匯入 androidx.lifecycle.Observer
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer {
})

  1. 在觀察器區塊中,導覽並傳遞目前晚上的 ID,然後呼叫 doneNavigating()。如果匯入作業不明確,請匯入 androidx.navigation.fragment.findNavController
night ->
night?.let {
   this.findNavController().navigate(
           SleepTrackerFragmentDirections
                   .actionSleepTrackerFragmentToSleepQualityFragment(night.nightId))
   sleepTrackerViewModel.doneNavigating()
}
  1. 建構並執行應用程式。依序輕觸「開始」和「停止」,即可前往 SleepQualityFragment 畫面。如要返回,請使用系統返回按鈕。

在這項工作中,你會記錄睡眠品質,然後返回睡眠追蹤器片段。顯示畫面應自動更新,向使用者顯示更新後的值。您需要建立 ViewModelViewModelFactory,並更新 SleepQualityFragment

步驟 1:建立 ViewModel 和 ViewModelFactory

  1. sleepquality 套件中,建立或開啟 SleepQualityViewModel.kt
  2. 建立 SleepQualityViewModel 類別,並將 sleepNightKey 和資料庫做為引數。如同 SleepTrackerViewModel,您需要從工廠傳入 database。您也需要從導覽中傳入 sleepNightKey
class SleepQualityViewModel(
       private val sleepNightKey: Long = 0L,
       val database: SleepDatabaseDao) : ViewModel() {
}
  1. SleepQualityViewModel 類別中,定義 JobuiScope,並覆寫 onCleared()
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}
  1. 如要使用與上述相同的模式返回 SleepTrackerFragment,請宣告 _navigateToSleepTracker。實作 navigateToSleepTrackerdoneNavigating()
private val _navigateToSleepTracker = MutableLiveData<Boolean?>()

val navigateToSleepTracker: LiveData<Boolean?>
   get() = _navigateToSleepTracker

fun doneNavigating() {
   _navigateToSleepTracker.value = null
}
  1. 為所有睡眠品質圖片建立一個點擊處理常式 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
        }
    }
  1. 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

  1. 開啟 SleepQualityFragment.kt
  2. onCreateView() 中取得 application 後,您需要取得導覽隨附的 arguments。這些引數位於 SleepQualityFragmentArgs 中。您必須從套件中擷取這些檔案。
val arguments = SleepQualityFragmentArgs.fromBundle(arguments!!)
  1. 接著取得 dataSource
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. 建立工廠,並傳入 dataSourcesleepNightKey
val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource)
  1. 取得 ViewModel 參照。
val sleepQualityViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepQualityViewModel::class.java)
  1. ViewModel 新增至繫結物件。(如果看到繫結物件發生錯誤,請暫時忽略。)
binding.sleepQualityViewModel = sleepQualityViewModel
  1. 新增觀察者。系統顯示提示訊息時,請匯入 androidx.lifecycle.Observer
sleepQualityViewModel.navigateToSleepTracker.observe(this, Observer {
   if (it == true) { // Observed state is true.
       this.findNavController().navigate(
               SleepQualityFragmentDirections.actionSleepQualityFragmentToSleepTrackerFragment())
       sleepQualityViewModel.doneNavigating()
   }
})

步驟 3:更新版面配置檔案並執行應用程式

  1. 開啟 fragment_sleep_quality.xml 版面配置檔案。在 <data> 區塊中,新增 SleepQualityViewModel 的變數。
 <data>
       <variable
           name="sleepQualityViewModel"
           type="com.example.android.trackmysleepquality.sleepquality.SleepQualityViewModel" />
   </data>
  1. 為六張睡眠品質圖片各新增一個點擊處理常式,如下所示。為圖片選擇合適的品質評分。
android:onClick="@{() -> sleepQualityViewModel.onSetSleepQuality(5)}"
  1. 清除並重建專案。這應該可以解決繫結物件的任何錯誤。否則,請清除快取 (「File」>「Invalidate Caches / Restart」),然後重建應用程式。

恭喜!您剛才使用協同程式建構了完整的 Room 資料庫應用程式。

現在應用程式運作正常。使用者可以視需要多次輕觸「開始」和「停止」。使用者輕觸「停止」後,即可輸入睡眠品質。使用者輕觸「清除」後,系統會在背景中清除所有資料,不過,所有按鈕一律都會啟用並可點選,雖然不會導致應用程式故障,但使用者可能會建立不完整的睡眠夜間記錄。

在最後一項工作中,您將瞭解如何使用轉換對應管理按鈕顯示狀態,確保使用者只能做出正確選擇。您可以使用類似方法,在清除所有資料後顯示友善訊息。

步驟 1:更新按鈕狀態

概念是設定按鈕狀態,一開始只啟用「Start」按鈕,也就是可點選。

使用者輕觸「開始」後,「停止」按鈕會啟用,「開始」則會停用。只有在資料庫中有資料時,「清除」按鈕才會啟用。

  1. 開啟 fragment_sleep_tracker.xml 版面配置檔案。
  2. 為每個按鈕新增 android:enabled 屬性。android:enabled 屬性是布林值,表示按鈕是否已啟用。(已啟用的按鈕可以輕觸,已停用的按鈕則無法。) 為該屬性指派狀態變數的值,您稍後會定義該變數。

start_button

android:enabled="@{sleepTrackerViewModel.startButtonVisible}"

stop_button:

android:enabled="@{sleepTrackerViewModel.stopButtonVisible}"

clear_button

android:enabled="@{sleepTrackerViewModel.clearButtonVisible}"
  1. 開啟 SleepTrackerViewModel 並建立三個對應變數。為每個變數指派測試該變數的轉換。
  • tonightnull 時,「開始」按鈕應為啟用狀態。
  • tonight 不是 null 時,「停止」按鈕應處於啟用狀態。
  • 只有在 nights (也就是資料庫) 包含睡眠夜數時,才應啟用「清除」按鈕。
val startButtonVisible = Transformations.map(tonight) {
   it == null
}
val stopButtonVisible = Transformations.map(tonight) {
   it != null
}
val clearButtonVisible = Transformations.map(nights) {
   it?.isNotEmpty()
}
  1. 執行應用程式,然後試用按鈕。

步驟 2:使用 Snackbar 通知使用者

使用者清除資料庫後,請使用 Snackbar 小工具向使用者顯示確認訊息。Snackbar 會在畫面底部顯示訊息,提供簡短的作業回饋內容。如果逾時、使用者與螢幕上其他位置互動,或使用者將資訊列滑出螢幕,資訊列就會消失。

顯示零食列是 UI 工作,應在片段中進行。決定是否顯示 Snackbar 的動作會在 ViewModel 中進行。如要在清除資料時設定及觸發 snackbar,可以採用與觸發導覽相同的技巧。

  1. SleepTrackerViewModel 中,建立封裝事件。
private var _showSnackbarEvent = MutableLiveData<Boolean>()

val showSnackBarEvent: LiveData<Boolean>
   get() = _showSnackbarEvent
  1. 然後導入 doneShowingSnackbar()
fun doneShowingSnackbar() {
   _showSnackbarEvent.value = false
}
  1. SleepTrackerFragment 中,於 onCreateView() 新增觀察器:
sleepTrackerViewModel.showSnackBarEvent.observe(this, Observer { })
  1. 在觀察程式區塊內,顯示 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()
   }
  1. SleepTrackerViewModel 中,於 onClear() 方法中觸發事件。如要這麼做,請在 launch 區塊中將事件值設為 true
_showSnackbarEvent.value = true
  1. 建構並執行應用程式!

Android Studio 專案:TrackMySleepQualityFinal

在這款應用程式中導入睡眠品質追蹤功能,就像用新的調性演奏熟悉的樂曲。雖然詳細資料會變更,但您在本課程中先前程式碼研究室所做的基礎模式仍維持不變。瞭解這些模式後,您就能重複使用現有應用程式的程式碼,大幅加快編碼速度。以下是本課程目前為止使用的部分模式:

  • 建立 ViewModelViewModelFactory,並設定資料來源。
  • 觸發導覽。為區分關注事項,請將點擊事件處理常式放在檢視區塊模型中,並將導覽放在片段中。
  • 使用 LiveData 封裝來追蹤及回應狀態變更。
  • 搭配 LiveData 使用轉換。
  • 建立單例資料庫。
  • 為資料庫作業設定協同程式。

觸發導覽

您可以在導覽檔案中定義片段之間可能的導覽路徑。您可以透過幾種不同的方式,從一個片段觸發導覽至下一個片段。包括:

  • 定義 onClick 處理常式,觸發導覽至目的地片段。
  • 或者,如要啟用從一個片段到下一個片段的導覽功能,請按照下列步驟操作:
  • 定義 LiveData 值,記錄是否應發生導覽。
  • 將觀察器附加至該 LiveData 值。
  • 每當需要觸發或完成導覽時,程式碼就會變更該值。

設定 android:enabled 屬性

  • android:enabled 屬性定義於 TextView 中,並由所有子類別 (包括 Button) 繼承。
  • android:enabled 屬性會決定是否啟用 View。「啟用」的意義因子類別而異。舉例來說,如果 EditText 未啟用,使用者就無法編輯內含文字;如果 Button 未啟用,使用者就無法輕觸按鈕。
  • enabled 屬性與 visibility 屬性不同。
  • 您可以根據其他物件或變數的狀態,使用轉換對應設定按鈕 enabled 屬性的值。

本程式碼研究室涵蓋的其他主題:

  • 如要觸發通知給使用者,可以採用與觸發導覽相同的技巧。
  • 您可以使用 Snackbar 通知使用者。

Udacity 課程:

Android 開發人員說明文件:

本節列出的作業可由課程講師指派給學習本程式碼研究室的學員。講師可自由採取以下行動:

  • 視需要指派作業。
  • 告知學員如何繳交作業。
  • 為作業評分。

講師可以視需求使用全部或部分建議內容,也可以自由指派任何其他合適的作業。

如果您是自行學習本程式碼研究室,不妨利用這些作業驗收學習成果。

回答問題

第 1 題

如要讓應用程式從一個片段觸發導覽至下一個片段,其中一種方法是使用 LiveData 值,指出是否要觸發導覽。

使用名為 gotoBlueFragmentLiveData 值,從紅色片段觸發導覽至藍色片段的步驟為何?(可複選):

  • ViewModel 中,定義 LiveDatagotoBlueFragment
  • 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 buttonandroid: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 buttonandroid:enabled 屬性設為 "Observable"

開始下一個課程:7.1 RecyclerView 基礎知識

如要查看本課程其他程式碼研究室的連結,請參閱 Android Kotlin 基礎知識程式碼研究室登陸頁面