試想一種情境:當我們執行下載檔案之類的耗時任務,要在任務完成時發出Toast通知使用者,可以怎麼做?
以目前的程式可以用Data Binding的addOnPropertyChangedCallback來做,將MainActivity修改如下:
public class MainActivity extends AppCompatActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
...
viewModel.mData.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(Observable observable, int i) {
Toast.makeText(MainActivity.this, "下載完成", Toast.LENGTH_SHORT).show();
}
});
}
}
對MainViewModel中的mData增加callback,當值發生改變時觸發並顯示Toast,執行結果:
但是,如果這個任務耗時更久,當它完成時使用者已經返回桌面做其他操作了呢?將DataModel的delay從1500改成3000來模擬:
離開畫面後Toast還是出現了,導致使用者已經在用其他app卻突然看到我們的Toast訊息,影響其使用體驗。
要解決這個問題,我們需要一種observable可以達到:
第1點並不太特別,各個observable library都做得到,然而,第2點lifecycle-aware就是今天的主角LiveData才能辦到。
如上所述,LiveData最強大的地方在於lifecycle-aware特性,當LiveData的value發生改變時,若View在前景便會直接發送,而View在背景的話,value將會被保留(hold)住,直到回到前景時才發送。此外,當View被destroy時,LiveData也會自動停止observe行為,避免造成memory-leak。
加入dependencies,跟昨天的ViewModel同屬於lifecycle component,如果昨天有加過今天就不用了。
// ViewModel and LiveData
implementation "android.arch.lifecycle:extensions:1.0.0"
annotationProcessor "android.arch.lifecycle:compiler:1.0.0"
修改MainViewModel,將mData改成MutableLiveData
public class MainViewModel extends ViewModel {
...
public final MutableLiveData<String> mData = new MutableLiveData<>();
...
public void refresh() {
...
dataModel.retrieveData(new DataModel.onDataReadyCallback() {
@Override
public void onDataReady(String data) {
mData.setValue(data);
...
}
});
}
}
MutableLiveData是方便我們使用的LiveData子類別,提供setValue()
和postValue()
兩種方式更新value,差異在於前者是在main thread執行,若需要在background thread則改用後者。
因為mData已經改用LiveData了,所以在main_activity.xml中修改一下TextView,刪掉Data Binding那一行
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="ivankuo.com.itbon2018.MainViewModel" />
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="ivankuo.com.itbon2018.MainActivity">
...
<TextView
android:id="@+id/txtHelloWord"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
... />
</android.support.constraint.ConstraintLayout>
</layout>
MainActivity中接收mData的callback
public class MainActivity extends AppCompatActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
viewModel.mData.observe(this, new Observer<String>() {
@Override
public void onChanged(@Nullable String data) {
binding.txtHelloWord.setText(data);
Toast.makeText(MainActivity.this, "下載完成", Toast.LENGTH_SHORT).show();
}
});
}
}
使用observe(owner, Observer)
來接收callback,owner用this表示LiveData會遵照MainActivity的生命週期判斷是否發送變更。
將delay縮短至1500並再次執行,就會看到Toast在app回到前景時才顯示:
上面的程式還有一個問題,當畫面旋轉時,Toast會再出現一次:
因為View在重新create後會立即收到LiveData的value,所以又觸發了一次onChanged()
並顯示Toast。
因應這種情況,Google寫了SingleLiveEvent這個class來處理
public class SingleLiveEvent<T> extends MutableLiveData<T> {
private static final String TAG = "SingleLiveEvent";
private final AtomicBoolean mPending = new AtomicBoolean(false);
@MainThread
public void observe(LifecycleOwner owner, final Observer<T> observer) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.");
}
// Observe the internal MutableLiveData
super.observe(owner, new Observer<T>() {
@Override
public void onChanged(@Nullable T t) {
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t);
}
}
});
}
@MainThread
public void setValue(@Nullable T t) {
mPending.set(true);
super.setValue(t);
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
public void call() {
setValue(null);
}
}
SingleLiveEvent只會發送更新的value,原value若已經發送過就不會再次發送,即避免了configuration change後又顯示一次同樣內容的問題。因此對於提示訊息、畫面跳轉等動作就很適合用SingleLiveEvent來處理,使用方式跟MutableLiveData一樣。
我們修改ViewModel將提示訊息改用SingleLiveEvent處理
public class MainViewModel extends ViewModel {
...
public final MutableLiveData<String> mData = new MutableLiveData<>();
public final SingleLiveEvent<String> toastText = new SingleLiveEvent<>();
...
public void refresh() {
isLoading.set(true);
dataModel.retrieveData(new DataModel.onDataReadyCallback() {
@Override
public void onDataReady(String data) {
mData.setValue(data);
toastText.setValue("下載完成");
isLoading.set(false);
}
});
}
}
修改MainActivity:
public class MainActivity extends AppCompatActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
viewModel.mData.observe(this, new Observer<String>() {
@Override
public void onChanged(@Nullable String data) {
binding.txtHelloWord.setText(data);
}
});
viewModel.toastText.observe(this, new Observer<String>() {
@Override
public void onChanged(@Nullable String text) {
Toast.makeText(MainActivity.this, text, Toast.LENGTH_SHORT).show();
}
});
}
}
將主要資料mData和toast分開,當首次載入資料時兩者都會觸發,在configuration change發生之後,mData會立即觸發讓畫面上顯示資料,而toastText因為value並沒有透過setValue()
更新過,所以不會再次觸發。
執行結果:
LiveData跟Data Binding角色有一點重疊,一般而言我會讓Data Binding處理元件的visible這類屬性,而主要顯示在UI的資料用LiveData以免除各種lifecycle衍生的問題。
Google也正在修改Data Binding library讓它也具有lifecycle-aware的特性,有興趣的可以留意#issue34
LiveData其他的內容像是Transform、MediatorLiveData等之後實際用到時會再說明。
GitHub source code:
https://github.com/IvanBean/ITBon2018/tree/day04-livedata