昨天將Data Binding做完整之後View就處理得差不多了,之後會朝調整ViewModel和在Model新增資料庫為主。
今天要講的是LiveData和Transformations的搭配使用,很多時候我們在撈資料時都會有一個key值,例如使用者id、商品id或是本篇的repository名稱等等,而Transformations就能讓LiveData更好的處理這種情境。
目前撈資料的流程是Model用callback來回傳內容給ViewModel,我們將callback改成直接回傳LiveData,這樣Model -> ViewModel -> View就一路都用LiveData傳遞,也與我們的藍圖GithubBrowserSample相同。
修改DataModel
public class DataModel {
...
public MutableLiveData<List<Repo>> searchRepo(String query) {
final MutableLiveData<List<Repo>> repos = new MutableLiveData<>();
githubService.searchRepos(query)
.enqueue(new Callback<RepoSearchResponse>() {
@Override
public void onResponse(@NonNull Call<RepoSearchResponse> call, @NonNull Response<RepoSearchResponse> response) {
repos.setValue(response.body().getItems());
}
...
});
return repos;
}
}
在searchRepo中建立新的MutableLiveData並回傳,稍後連線完成時會在onResponse中setValue()
更新此LiveData的內容。
那RepoViewModel的部分呢,如果你改成這樣:
public class RepoViewModel extends ViewModel {
...
MutableLiveData<List<Repo>> searchRepo(String query) {
// NO!
return dataModel.searchRepo(query);
}
}
以上寫法是由於DataModel已經準備好LiveData了,貌似ViewModel可以不用再處理直接交給View,但這樣有兩個問題:
這時就要用到Transformations了!
switchMap方法是將會改變的key值也寫成LiveData並作為trigger,以本篇來而言此trigger即為搜尋關鍵字query,當此trigger的value改變時便會觸發令主要資料的LiveData更新value而不用回傳新的instance,直接看程式碼比較清楚:
public class RepoViewModel extends ViewModel {
...
private final MutableLiveData<String> query = new MutableLiveData<>();
private final LiveData<List<Repo>> repos;
public RepoViewModel(final DataModel dataModel) {
...
repos = Transformations.switchMap(query, new Function<String, LiveData<List<Repo>>>() {
@Override
public LiveData<List<Repo>> apply(String userInput) {
return dataModel.searchRepo(userInput);
}
});
}
...
void searchRepo(String userInput) {
query.setValue(userInput);
}
}
我們將query當成trigger,並提供searchRepo(String userInput)
來更新它的value,每當query更新的時候就會觸發switchMap中的Function使repos自DataModel接收到新的資料。
Transformations不會使LiveData變成新的instance,因此View一直都是對repos做observe,即解決了上面提到的兩個問題。
另外google sample中做了一個AbsentLiveData來協助檢查trigger是否為null
/**
* A LiveData class that has {@code null} value.
*/
public class AbsentLiveData extends LiveData {
private AbsentLiveData() {
postValue(null);
}
public static <T> LiveData<T> create() {
//noinspection unchecked
return new AbsentLiveData();
}
}
其內容是一個會post null value的LiveData,用來避免trigger是null時撈資料可能會出錯。
於switchMap中使用:
repos = Transformations.switchMap(query, new Function<String, LiveData<List<Repo>>>() {
@Override
public LiveData<List<Repo>> apply(String userInput) {
if (TextUtils.isEmpty(userInput)) {
return AbsentLiveData.create();
} else {
return dataModel.searchRepo(userInput);
}
}
});
當userInput的值為空時回傳AbsentLiveData,否則就跟model請求資料。
map是將作為關鍵字的LiveData當成source,並用source的value轉換成主要資料,寫法跟switchMap很相似:
repos = Transformations.map(query, new Function<String, List<Repo>>() {
@Override
public List<Repo> apply(String input) {
// do something
}
});
因為我們專案沒有map的需求所以上面例子也許不太能體會,看看官方文件的例子:
LiveData userLiveData = ...;
LiveData userName = Transformations.map(userLiveData, new Function<User, String>() {
@Override
public String apply(User user) {
return user.firstName + " " + user.lastName;
}
});
第一個userLiveData作為source,當它的value改變時會觸發Function讓我們將user的firstName加上lastName作為LiveData userName的value。
總結來說,當你需要以某個key值來更新LiveData的value時可以優先考慮用Transformations來達成需求。
GitHub source code:
https://github.com/IvanBean/ITBon2018/tree/day07-livedata-transformations