###### tags: `Android` # LiveData ## 前言 當我們需要在資料更新後顯示一個提示訊息可以怎麼做? 在 DataBinding 中有 addOnPropertyChangedCallback 方法可以處理這個問題,我們在 ==MainActivity== 加入以下程式碼。 ```kotlin= viewModel.mData.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() { override fun onPropertyChanged(sender: Observable?, propertyId: Int) { Toast.makeText(this@MainActivity, "Updated", Toast.LENGTH_SHORT).show() } }) ``` 雖然這的確完成了我們的需求,但是使用者如果在更新過程中返回主畫面,當更新完成後仍然會顯示提示訊息,這影響了使用者的體驗,因此我們需要一個可以只在前景時發出提示訊息的東西,而這樣的東西我們可以使用擁有 Lifecycle-aware 特性的 LiveData 來完成。 <details> <summary>仍然會顯示提示訊息之示意圖</summary> ![](https://hackmd.io/_uploads/S1sYbFAVn.png) </details> ## 正文 LiveData 是 Jetpack 架構系列之一,它是一個可觀察的資料持有類別,擁有 Observable 類別所沒有的 Lifecycle-aware 的特性,這也確保了 Activity、Fragment 只在活耀的狀態才會收到資料的變化。 當 LiveData 的 Value 發生改變時,若 View 在前景便會直接發送,而 View 在背景的話,value 將會被保留(hold),直到回到前景時才發送。此外,當 View 被 Destroy 時,LiveData 也會自動停止 Observe 行為,避免造成記憶體洩漏。 ## 基本使用 <font color=Tomato>以下將使用 ViewModel 篇的基本使用中的範例繼續實作</font> ### 匯入 LiveData 在 ==build.gradle(Module:app)== 加入以下程式碼,加入後 Sync,如果有在使用 ViewModel 時匯入過,就不需要再加入以下程式碼,因為它們同屬於 lifecycle component ``` def lifecycle_version = "2.2.0" kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version" //If your app uses Java 8, we recommend using this library instead of lifecycle-compiler implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" ``` ### 應用於 MVVM 架構 先參考此篇文章的 [基本使用](https://hackmd.io/ROW4JBjqRTOrwLDltAeZDQ?view#%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8),完成後再來加入 LiveData ==MainViewModel== 將 mData 修改成 MutableLiveData 型別,並透過 value 設定其值。MutableLiveData 是 LiveData 的子類別,提供 setValue() 和 postValue() 兩種方式更新 Value,差異在於前者是在 Main thread 執行,若需要在 Background thread 則改用後者。 ```kotlin= class MainViewModel : ViewModel() { private val dataModel = DataModel() val mData = MutableLiveData<String>() val isLoading = ObservableField(false) fun refresh() { isLoading.set(true) dataModel.retrieveData(object : DataModel.onDataReadyCallback { override fun onDataReady(data: String) { mData.value = data isLoading.set(false) } }) } } ``` ==main_activity.xml== 因為 mData 已經改用 LiveData,所以要將 TextView 的 `android:text="@{viewModel.mData}"` 刪除。刪除 DataBinding 後如下: ```xml= <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> ``` ==MainActivity== 將程式碼進行修改,使用 observe(owner, Observer) 來接收 callback,owner 為 this 表示 LiveData 會遵照 MainActivity 的生命週期判斷是否發送變更,而由於我們刪除了 TextView 的 DataBinding,所以要將更新後的資料重新指派給 TextView。完成後我們就可以在 APP 回到前景時才顯示出變更後的值以及 Toast 提示訊息。 ```kotlin= class MainActivity : AppCompatActivity() { private lateinit var viewModel: MainViewModel private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel = ViewModelProvider(this).get(MainViewModel::class.java) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = viewModel binding.btnRefresh.setOnClickListener { viewModel.refresh() } viewModel.mData.observe(this, { data -> binding.textView.text = data Toast.makeText(this@MainActivity, "Updated", Toast.LENGTH_SHORT).show() }) } } ``` :::danger 但上面的程式還有一個問題,就是當畫面旋轉時 Toast 會再出現一次,因為 View 在重新 create 後會立即收到 LiveData 的 Value,所以又觸發了一次 onChanged() 並顯示 Toast,因應這種情況,Google 寫了 SingleLiveEvent 這個類別來處理。 ::: ## SingleLiveEvent ==建立 SingleLiveEvent 類別== 透過以下程式碼建立 SingleLiveEvent 類別,裡面主要使用 AtomicBoolean 來判斷 value 是否更新過,當資料被更新時會進入 setValue(),此時將 mPending 設定為 true,而 observe 被觸發時會判斷 mPending 是否為 true,若是則將 mPending 更改為 false,並執行 observe.onChanged(),以表示資料確實被更新過,若判斷 mPending 為 false 則不執行 observe.onChanged()。 ```kotlin= class SingleLiveEvent<T> : MutableLiveData<T>() { private val mPending: AtomicBoolean = AtomicBoolean(false) @MainThread override fun observe(owner: LifecycleOwner, observer: Observer<in T>) { if (hasActiveObservers()) { Log.w(TAG, "Multiple observers registered but only one will be notified of changes.") } // Observe the internal MutableLiveData super.observe(owner, { t -> if (mPending.compareAndSet(true, false)) { observer.onChanged(t) } }) } @MainThread override fun setValue(@Nullable t: T?) { mPending.set(true) super.setValue(t) } /** * Used for cases where T is Void, to make calls cleaner. */ @MainThread fun call() { value = null } companion object { private const val TAG = "SingleLiveEvent" } } ``` ==MainViewModel== 由於我們希望提示訊息僅顯示一次,所以將 toastText 改為 SingleLiveEvent。 ```kotlin= class MainViewModel : ViewModel() { private val dataModel = DataModel() val mData = MutableLiveData<String>() val toastText = SingleLiveEvent<String>() val isLoading = ObservableField(false) fun refresh() { isLoading.set(true) dataModel.retrieveData(object : DataModel.onDataReadyCallback { override fun onDataReady(data: String) { mData.value = data toastText.value = "Updated" isLoading.set(false) } }) } } ``` ==MainActivity== 將 mData、toastText 分別監聽,當首次載入資料時兩者都會觸發,在 configuration change 發生後,mData 會立即觸發並顯示資料,而 toastText 因為 value 並沒有透過 setValue() 更新過,所以不會再次觸發。 ```kotlin= class MainActivity : AppCompatActivity() { private lateinit var viewModel: MainViewModel private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel = ViewModelProvider(this).get(MainViewModel::class.java) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = viewModel binding.btnRefresh.setOnClickListener { viewModel.refresh() } viewModel.mData.observe(this, { data -> binding.textView.text = data }) viewModel.toastText.observe(this, { data -> Toast.makeText(this@MainActivity, data, Toast.LENGTH_SHORT).show() }) } } ``` LiveData跟Data Binding角色有一點重疊,而Google也正在修改Data Binding library讓它也具有lifecycle-aware的特性,這就牽涉到 Data Binding V2 了 ## 參考文章 [Architecture Components - LiveData](https://ithelp.ithome.com.tw/articles/10193296)