純粹使用 RxJava 實現 ViewModel
在閱讀本文前,你需要對什么是 MVC、MVP、MVVM 以及它們之間的區別有清楚的認識,如果你不太清楚,推薦你看 MVC vs. MVP vs. MVVM on Android .
說到 Android MVVM,相信大家都會想到 Google 2015 年推出的Data Binding Library. 然而兩者的概念是不一樣的,不能混為一談。MVVM 是一種架構模式,而 Data Binding Library 是一個實現數據和 UI 綁定的框架,是構建 MVVM 模式的一個工具。
我們不會用到 Data Binding Library, 因為在復雜的業務中,并不僅僅是簡單的在 XML 中綁定就能解決問題,對部分屬性變化和事件觸發的處理仍然需要在 View (Activity / Fragment) 中編寫 Java 代碼,并且為了實現復雜數據的綁定,需要編寫各種綁定適配器,這樣會導致代碼的分散,給閱讀和維護代碼帶來不便。
我們提供了基于 RxJava 的實現方式,所有的數據和事件都在 Activity / Fragment 中綁定,便于閱讀和維護代碼。RxJava 中的 Observable 和 Data Binding Library 中的 ObservableFiled 一樣是可觀測的,而且,RxJava 提供了強大的數據映射、轉換、過濾、組合功能,能夠輕松處理非常復雜的業務問題。
Demo
我們通過大家比較熟悉的 TO-DO APP 來演示我們的實現方式。
todo
對 MVVM 比較熟悉的讀者,可以直接去看代碼了。
$ git clone git@github.com:listenzz/todo-android.git
$ git checkout todo-mvvm-rxcommand
和 todo-mvvm-databinding 分支對照著看,效果更佳。
如果你想運行項目,記得選擇 prodDebug 的 Build Variant, 才會有初始數據。
todo-build-variants
MVVM
讀者可能對 MVP 比較熟悉,而不了解 MVVM. MVVM 中 ViewModel 扮演的角色和 MVP 中 Presenter 的角色是非常相似的。這兩個架構的主要區別就是 View 和 ViewModel 或 Presenter 的通信方式。
- 在 MVVM 中,當 app 修改了 ViewModel, View 會自動更新。你不可以從 ViewModel 中直接更新 View, 因為 ViewModel 不持有 View 的引用。
- 在 MVP 中,你可以通過 Presenter 來更新 View,因為 Presenter 持有 View 的引用。當需要更改時,你可以通過 Presenter 顯式地調用 View 來更新它。
ViewModel
維基百科是這樣定義 ViewModel 的:
The view model is an abstraction of the view exposing public properties and commands. Instead of the controller of the MVC pattern, or the presenter of the MVP pattern, MVVM has a binder.
ViewModel 的職責就是對 Model 進行包裝,準備 View 需要的可觀測數據(public properties),以及提供 View 可以傳遞事件給 Model 的鉤子(commands)。當 ViewModel 準備好這些東西后,就需要綁定到 View. 如果使用 Data Binding Library, 綁定就主要發生在 XML 中,如果像本文那樣基于 RxJava, 綁定就發生在 Activity 或 Fragment 中。
我們抽取 todo-mvvm-databinding 和 todo-mvvm-rxcommand 這兩個分支中同樣的類來對比講解數據和事件的綁定。我們抽取的類是 AddEditTaskViewModel , 因為它比較簡單,但是足夠說明問題。
Data Binding
ViewModel 如何提供可觀測的屬性? 這些屬性怎樣綁定到 View ?
我們先來看看,使用 Data Binding Library 的代碼長得是什么樣子的。
- 首先在 AddEditTaskViewModel 中定義 ObservableField
public class AddEditTaskViewModel {
// to do task 的標題
public final ObservableField<String> title = new ObservableField<>();
// task 的描述
public final ObservableField<String> description = new ObservableField<>();
// 是否正在加載數據
public final ObservableBoolean dataLoading = new ObservableBoolean(false);
}
- 在代碼中適當的地方,設置 ObservableField, 每次設置 ObservableField 時,UI 就會自動更新
// 這是個回調函數,稍后會講解如何發起請求拉取 task
public void onTaskLoaded(Task task) {
title.set(task.getTitle());
description.set(task.getDescription());
dataLoading.set(false); // 設置數據已經加載完成,這時 loading 會停止
}
- 在 XML 中,綁定這些定義在 ViewModel 中的 ObservableField
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View"/>
<variable
name="viewmodel"
type="com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskViewModel"/>
</data>
<com.example.android.architecture.blueprints.todoapp.ScrollChildSwipeRefreshLayout
app:enabled="@{viewmodel.dataLoading}"
app:refreshing="@{viewmodel.dataLoading}">
<ScrollView>
<LinearLayout android:visibility="@{viewmodel.dataLoading ? View.GONE : View.VISIBLE}">
<EditText android:text="@={viewmodel.title}"/>
<EditText android:text="@={viewmodel.description}"/>
</LinearLayout>
</ScrollView>
</com.example.android.architecture.blueprints.todoapp.ScrollChildSwipeRefreshLayout>
</layout>
使用 RxJava 又該如何提供可綁定的數據,以及如何綁定呢?
- 首先在 AddEditTaskViewModel 中聲明 Observable.
public class AddEditTaskViewModel {
public final Observable<String> title;
public final Observable<String> description;
//如何實現 loading,在 Event Binding 一節中會講到
}
- 在構造函數中定義這些 Observable.
AddEditTaskViewModel(Context context, TasksRepository tasksRepository) {
//這是一個用來拉取 task 的 command, Event Binding 一節中我們會講解它
populateTaskCommand = ...
// 我們通過 command 拉取的結果來構建 title 和 description observable
title = populateTaskCommand
.switchToLatest()
.map(task -> task.getTitle());
description = populateTaskCommand
.switchToLatest()
.map(task -> task.getDescription());
}
- 在 AddEditTaskFragment 中,綁定這些定義在 ViewModel 中的 Observable, 每當數據發生變化時,相應的 UI 就會自動更新。
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mViewModel.title
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> mTitleView.setText(s));
mViewModel.description
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> mDescriptionView.setText(s));
}
Event Binding
事件就是頁面顯示或隱藏這些 app 生命周期事件,或者點擊按鈕、下拉刷新這些用戶操作。事件綁定就是當這些事件發生時,調用定義在 ViewModel 中的方法。就是這么簡單,沒什么高深的概念。
在我們這個頁面中,有兩個事件,一個是頁面生命周期事件 onResume , 另外一個是保存按鈕的點擊事件。
先來看看使用 Data Binding Library 是如何定義 command 的,在 AddEditTaskViewModel 中:
public void start(String taskId) {
if (dataLoading.get()) {
// Already loading, ignore.
return;
}
mTaskId = taskId;
if (taskId == null) {
// No need to populate, it's a new task
mIsNewTask = true;
return;
}
if (mIsDataLoaded) {
// No need to populate, already have data.
return;
}
mIsNewTask = false;
// 設置正在加載數據,這時 loading 會顯示
dataLoading.set(true);
// 加載數據,成功后,回調 #onTaskLoaded
mTasksRepository.getTask(taskId, this);
}
你沒看錯,command 就是普通的方法。
再來看看使用 Data Binding Library 是如何將命令綁定到事件的。 事件和屬性一樣是可以直接通過 XML 綁定的,在 todo-mvvm-databinding 這個 git 分支中,你可以看到這樣的例子。不過在 AddEditTaskFragment 這個頁面中,這兩個事件都是通過代碼來綁定。這些綁定發生在 AddEditTaskFragment 中:
@Override
public void onResume() {
super.onResume();
if (getArguments() != null) {
mViewModel.start(getArguments().getString(ARGUMENT_EDIT_TASK_ID));
} else {
mViewModel.start(null);
}
}
沒錯,這就是綁定。
使用 RxJava 又該如何提供可綁定的命令,以及如何綁定呢?
命令可以是個普通的方法,當然也可以封裝成一個對象。
RxCommand 是一個基于 RxJava 的,為 ViewModel 提供命令綁定到 View 的非常輕量級的庫,含注釋 300 來行代碼。讀者可以通過 《使用 RxCommand 在 Android 上實現 MVVM》 一文詳細了解 RxCommand 的用法。 RxCommand 分離了任務執行的關注點,讓我們可以有選擇地關注任務是否在執行,是否發生了異常,從而決定是否顯示 loading,提示錯誤信息等等。
來看看我們是如何定義 command 的,以下是 AddEditTaskViewModel 的構造函數的完整定義:
AddEditTaskViewModel(Context context, TasksRepository tasksRepository) {
mContext = context.getApplicationContext(); // Force use of Application Context.
mTasksRepository = tasksRepository;
// 定義 command
populateTaskCommand = RxCommand.create(taskId -> {
mTaskId = (String) taskId;
if (taskId == null) {
// No need to populate, it's a new task
mIsNewTask = true;
return Observable.empty();
}
if (mIsDataLoaded) {
// No need to populate, already have data.
return Observable.empty();
}
mIsNewTask = false;
return mTasksRepository
.getTask((String) taskId)
.doOnNext(task -> mIsDataLoaded = true);
});
// 將 command 執行后獲得的結果轉換成我們想要的 observable property
title = populateTaskCommand
.switchToLatest()
.map(task -> task.getTitle());
description = populateTaskCommand
.switchToLatest()
.map(task -> task.getDescription());
mSnackbarTextSubject = PublishSubject.create();
snackbarText = mSnackbarTextSubject;
// 將 command 執行發生的錯誤轉換成可以提示給用戶的 observable property
populateTaskCommand.errors()
.subscribe(throwable -> mSnackbarTextSubject
.onNext(throwable.getLocalizedMessage()));
}
如何綁定呢 ?
@Override
public void onResume() {
super.onResume();
if (getArguments() != null) {
mViewModel.populateTaskCommand.execute(getArguments().getString(ARGUMENT_EDIT_TASK_ID));
} else {
mViewModel.populateTaskCommand.execute(null);
}
}
現在讓我們來處理 loading, 使用 Data Binding Library 時,我們定義了一個名為 dataLoading 的 ObservableBoolean, 綁定到 XML 來控制 loading 的顯示。那么使用 RxJava 該如何實現呢?我們強大的 RxCommand 登場了,來看看它是怎么處理是否正在加載中的情形的。 還是在 onViewCreated 中
mViewModel.populateTaskCommand
.executing()
.skip(1) // command 沒執行前默認會發送一次 false, 我們跳過它
.subscribe(executing -> {
// 根據是否正在執行來決定是否顯示 loading
mRefreshLayout.setRefreshing(executing);
// 根據是否正在執行來顯示相應界面
if (executing) {
mContentLayout.setVisibility(View.GONE);
} else {
mContentLayout.setVisibility(View.VISIBLE);
}
});
另外 RxJava 和 Data Binding Library 不是互斥的,如果你喜歡 Data Binding Library,也是可以利用 RxCommand 來幫你構建 command 的,比一個普通的方法好多了。
總結
屬性和命令是 ViewModel 的主要組成,ViewModel 也不可避免地需要依賴 Model,完成業務邏輯的轉發。以下是個人在開發過程中總結的 Android MVVM 構建思想。
- 純代碼實現的 MVVM,為了避免代碼分散,ViewModel 的粒度不宜過細,只有 Activity 或 Fragment 這樣級別的 View 才應該擁有 ViewModel,它們是一一對應的關系,同生共死。
- ViewModel 各自獨立,互不依賴,也不會有子 ViewModel.
- ViewModel 和 View 協同處理頁面呈現邏輯,ViewModel 不處理業務邏輯,業務邏輯是 Model 的職責。
- 如果數據還不是適合展示的最終形態,View 不應該自己去轉換格式,這是 ViewModel 的職責,ViewModel 根據情況提供適合 View 直接展示的數據,或者提供可以轉換成適合展示的數據的方法給 View 調用。
- ViewModel 管理 local state, 狀態管理容器管理 global state. ViewModel 之間通過狀態管理容器共享狀態。筆者所在公司使用 RxJava 來實現 Redux 作為狀態管理容器。
來自:https://juejin.im/post/5934f898ac502e0068adeccc