Android Kotlin 基本概念 06.3:使用 LiveData 控制按鈕狀態

本程式碼研究室是 Android Kotlin 基礎課程的一部分。使用程式碼研究室逐步完成程式碼課程後,您將能充分發揮本課程的潛能。所有課程程式碼研究室清單均列於 Android Kotlin 基礎程式碼研究室到達網頁

簡介

這個程式碼研究室將示範如何使用 ViewModel 和片段來導入導覽功能。請記住,目標是將 when 邏輯加到 ViewModel,但要定義片段和導覽檔案中的路徑。為了達成這個目標,您可以使用檢視模型、片段、LiveData 和觀察器。

程式碼研究室的顯示,是以簡潔的方式追蹤基本程式碼的按鈕狀態。這樣一來,每個按鈕只會在使用者輕觸按鈕時才啟用及點擊。

須知事項

您應該很熟悉:

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

課程內容

  • 如何更新資料庫中現有的睡眠品質記錄。
  • 如何使用 LiveData 追蹤按鈕狀態。
  • 如何在活動中顯示 Snackbar。

執行步驟

  • 擴充 TrackMySleepquality 應用程式以收集品質評分、將評分新增到資料庫,並顯示結果。
  • 使用 LiveData 觸發 Snackbar 的顯示。
  • 使用 LiveData 即可啟用及停用按鈕。

在這個程式碼研究室中,您需要為「MyMySleepquality」應用程式建立睡眠品質的錄音和敲定的使用者介面。

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

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

第二個螢幕則是根據選取睡眠品質評分的方式。在應用程式中,評分是以數字表示。在開發方面,應用程式會同時顯示臉部圖示和對應的數值。

使用者流程如下:

  • 使用者開啟應用程式並顯示睡眠追蹤畫面。
  • 使用者輕觸 [開始] 按鈕。這會記錄開始時間,並顯示。[開始] 按鈕已停用,[停止] 按鈕也已啟用。
  • 輕觸 [停止] 按鈕。這會記錄結束時間,並開啟睡眠品質畫面。
  • 使用者選取睡眠品質圖示。螢幕會關閉,追蹤畫面會顯示睡眠的結束時間和睡眠品質。[停用] 按鈕已停用,且 [開始] 按鈕已啟用。應用程式已可繼續進行其他夜晚,
  • 如果資料庫中有資料,[清除] 按鈕就會啟用。使用者輕觸 [清除] 按鈕時,系統會將他們的所有資料清除,而不會經過清除,不會出現「你同意升級」的訊息。

此應用程式使用簡易架構,如下圖所示,在完整架構的情境中。應用程式僅使用下列元件:

  • UI 控制器
  • 檢視模型及LiveData
  • 會議室資料庫

本程式碼研究室假設您已知道如何使用片段和導覽檔案進行瀏覽。為節省您的工作時間,我們會妥善提供這組代碼。

步驟 1:檢查程式碼

  1. 如要開始,請在上個程式碼研究室結尾使用您自己的程式碼,或下載範例程式碼
  2. 在範例程式碼中,檢查 SleepQualityFragment。此類別會翻轉版面配置、取得應用程式並傳回 binding.root
  3. 在設計編輯器中開啟 navigation.xml。我們發現從SleepTrackerFragmentSleepQualityFragment有一項導航路徑,從SleepQualityFragmentSleepTrackerFragment又回到了一次。



  4. 檢查 navigation.xml 的程式碼。請特別注意,尋找名稱為 sleepNightKey<argument>

    當使用者從 SleepTrackerFragment 進入 SleepQualityFragment, 應用程式後,系統會將 sleepNightKey 傳送到 SleepQualityFragment,讓需要更新的夜晚睡覺。

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

導覽圖中已經包含從 SleepTrackerFragmentSleepQualityFragment 以及再次檢視的路徑。不過,將實作從特定片段移到下一個片段的點擊處理常式尚未編寫程式碼。您現在已經將這段程式碼加到 ViewModel 中。

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

  1. 開啟 SleepTrackerViewModel。您必須新增導覽功能,當使用者輕觸 [停止] 按鈕時,應用程式就會瀏覽至 SleepQualityFragment 以收集品質評分。
  2. SleepTrackerViewModel 中建立一個LiveData,讓應用程式回到SleepQualityFragment的時間點。使用封裝將 LiveData 的可獲取版本只暴露在 ViewModel 上。

    您可以將這段程式碼置於課程內文頂層。
private val _navigateToSleepQuality = MutableLiveData<SleepNight>()

val navigateToSleepQuality: LiveData<SleepNight>
   get() = _navigateToSleepQuality
  1. 新增會重設導覽變數的 doneNavigating() 函式。
fun doneNavigating() {
   _navigateToSleepQuality.value = null
}
  1. 停止按鈕onStopTracking()的 click click 中,觸見到SleepQualityFragment。請將函式結尾的 _navigateToSleepQuality 變數設為 launch{} 區塊內的最後一項。請注意,這個變數已設為 night。當這個變數有值時,應用程式會瀏覽至 SleepQualityFragment,並直到晚上顯示
_navigateToSleepQuality.value = oldNight
  1. SleepTrackerFragment 需要觀察 _navigateToSleepQuality,才能讓應用程式知道何時要導航。在 SleepTrackerFragment 中,為 onCreateView() 新增 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. 清理及重建專案。這樣做應可解決繫結物件的所有錯誤。否則請清除快取 (檔案 > 撤銷快取 / 重新啟動),然後重新建構應用程式。

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

您的應用程式現在成效良好。使用者可以輕觸 [開始] 和 [停止],次數不限。使用者輕觸 [停止] 後,即可輸入睡眠品質。使用者輕觸 [清除] 後,系統會在背景自動清除所有資料。不過,所有按鈕一律已啟用且可點選,而不會中斷應用程式,但可讓使用者建立不完整的睡眠之夜。

在上一項工作中,您將瞭解如何使用轉換地圖來管理按鈕顯示設定,讓使用者只能做出正確的選擇。在所有的資料都清除完畢後,您可以使用類似的方法顯示友善訊息。

步驟 1:更新按鈕狀態

建議設定按鈕狀態,這樣一開始就會只啟用 [開始] 按鈕,這表示使用者點選了按鈕。

使用者輕觸 [開始] 後,[停止] 按鈕就會啟用,[開始] 按鈕則不會啟用。[資料庫] 按鈕只有在資料庫有資料時才會啟用。

  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] 按鈕只有在已啟用 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 透過螢幕底部的訊息提供關於作業的簡短意見回饋。Snackbar 會在逾時、使用者與畫面其他畫面互動,或是使用者從 Snackbar 滑出畫面之後消失。

顯示 Snackbar 是使用者介面工作,應該會出現在片段中。決定顯示 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 使用轉換。
  • 建立單一 ton 資料庫。
  • 設定資料庫作業的協同程式。

觸發導航

您可在導覽檔中定義片段之間的可能導覽路徑。有幾種不同的方式可以觸發不同片段的瀏覽動作。包括:

  • 定義 onClick 處理常式以觸發前往目的地片段的導覽作業。
  • 或者,如要啟用不同片段的瀏覽功能:
  • 定義 LiveData 值,以便在系統產生導航時記錄。
  • 將觀察器附加至該 LiveData 值。
  • 這樣一來,每當需要觸發或完成導覽時,程式碼就會變更這個值。

設定 android:enabled 屬性

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

此程式碼研究室涵蓋的其他要點:

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

Udacity 課程:

Android 開發人員說明文件:

這個部分會列出在代碼研究室中,受老師主導的課程作業的可能學生作業。由老師自行決定要執行下列動作:

  • 視需要指派家庭作業。
  • 告知學生如何提交家庭作業。
  • 批改家庭作業。

老師可視需要使用這些建議,並視情況指派其他合適的家庭作業。

如果您是自行操作本程式碼研究室,歡迎透過這些家庭作業來測試自己的知識。

回答這些問題

第 1 題

如要讓應用程式觸發導覽的瀏覽方式,請使用 LiveData 值來表示是否要觸發導覽。

使用 LiveData 值 (gotoBlueFragment) 觸發從紅色片段到藍色片段的導覽的步驟為何?(可複選):

  • ViewModel 中定義 LiveDatagotoBlueFragment
  • RedFragment 中觀察 gotoBlueFragment 值。請實作 observe{} 程式碼,以便在適當情況下前往 BlueFragment,然後重設 gotoBlueFragment 的值,表示導覽已完成。
  • 請確認您的程式碼會將 gotoBlueFragment 變數設為該值,在應用程式需要從 RedFragment 改為 BlueFragment 時觸發導航。
  • 請確認您的程式碼定義了 onClickView 處理常式,以便使用者點擊 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 基礎程式碼程式碼到達網頁