使用 RxCommand 在 Android 上實現 MVVM
RxCommand 是一個基于 RxJava 的,UI 相關的,主要用來響應用戶觸發的異步任務,尤其是網絡訪問的庫。它分離了對異步任務的關注點,譬如任務是否處于可執行狀態,任務是否正在執行,任務返回結果,任務執行過程中發生錯誤。這些關注點以 Observable 的形式返回,可以有選擇性地訂閱,以及和其它流組合,處理復雜的業務邏輯。這個庫相當輕量級,包含注釋在內300來行代碼。
前提
你喜歡 RxJava , 并且已在項目中實踐。
為什么放棄MVP
MVP 中的 V 和 P 都擁有對對方的控制權。它們相互調用代碼,同一個邏輯流程的代碼分散各處,給閱讀和理解代碼帶來不便。而 MVVM 中的 VM 不持有 V , V 觀察 VM 中的狀態變更來刷新自己的界面,可以在一個地方處理完相關的邏輯。 所以MVP是命令式的,而MVVM是響應式的。
為什么選擇RxCommand
MVP 是Android開發社區目前比較流行的架構,而 MVVM 是iOS和前端開發社區目前比較流行的架構。造成這種局面的最大原因是語言的差異,以 Objective-C 為例, 它有一項很酷的語言特性,那就是 KVO , 全稱是鍵值觀測,一個對象的屬性自帶觀察者模式,在它的值發生變化時,你可以輕易地得到通知。 借助 ReactiveCocoa 只需要一行代碼,就可以把從 View 接收到的輸入綁定到 ViewModel.
RAC(viewModel, username) = RACObserve(view, textView);
也只需要一行代碼,就可以把 ViewModel 的狀態變更反饋到 View 上:
RAC(view, label) = RACObserve(viewModel, email);
Google 開發了 Data Biding 來幫助開發者在 Android 上實現 MVVM, 不過實現起來比較繁瑣,尤其是把代碼寫在 XML 中,實在是不雅。
RxCommand具有如下特點:
- 基于 RxJava , 和 RxJava 完美配合
- 配合 RxBinding 等框架,不需要把代碼寫在 XML 中,可以實現雙向綁定
- 分離關注點,便于有選擇地處理任務執行的狀態(執行中,錯誤,完成等等)
- ViewModel是個普通類
- 相關代碼集中,便于閱讀和維護
Demo
我們通過一個 Demo 來講解如何通過 RxCommand 來實現 V 和 VM . 看圖:

假設我們需要做一個登錄功能,這太常見了。
- 當手機號碼不合法時,獲取驗證碼按鈕處于disable狀態
- 當正在獲取驗證碼時,獲取驗證碼按鈕處于disable狀態,并顯示loading
- 當獲取驗證碼成功時,倒計時開始,按鈕仍處于disable狀態,倒計時結束,按鈕重新可用
- 當獲取驗證碼失敗時,不會開始倒計時,按鈕恢復可用狀態
- 當手機號碼和驗證碼都合法時,登錄按鈕處于可點擊狀態,否則不可點擊
- 當點擊登陸按鈕時,登錄按鈕處于不可點狀態,同時顯示loading
- 當登錄成功時,停止 loading, 跳轉到主界面
- 當登錄失敗時,停止 loading, 并提示錯誤
源碼
以下是對 Demo 源碼的解讀,如果你想直接看代碼,請看 完整的項目源碼以及 demo .
實現 View
讓我們來看看,要實現以上需求,LoginActivity 該怎么寫。
這四行代碼就完成了接收用戶輸入,驗證用戶輸入是否合法,來決定按鈕是否可點擊,以及響應用戶點擊按鈕觸發任務,在任務執行期間,按鈕不可點擊等需求。
//綁定用戶輸入到ViewModel RxTextView.textChanges(phoneEditText).subscribe(viewModel.phone()); RxTextView.textChanges(codeEditText).subscribe(viewModel.code()); //綁定按鈕和command,當command正在執行或者輸入不合法時,按鈕將處于disable狀態 RxCommandBinder.bind(codeButton, viewModel.codeCommand()); RxCommandBinder.bind(loginButton, viewModel.loginCommand());
獲取驗證碼
處理獲取驗證碼的執行狀態,如果正在獲取,顯示獲取中,如果獲取成功或發生錯誤,則重置。
viewModel.codeCommand()
.executing()
.subscribe(executing -> {
if (executing) {
codeButton.setText("fetch...");
} else {
codeButton.setText("fetch code");
}
});
處理獲取驗證碼成功的情形,command 內部已經過濾了發生錯誤的情形,不需要在這里處理失敗的情況
viewModel.codeCommand()
.switchToLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> Toast.makeText(LoginActivity.this, result, Toast.LENGTH_LONG).show());
獲取驗證碼成功后,會開啟倒計時,避免用戶在沒及時收到短信的情況下狂點 獲取驗證碼 按鈕。 倒計時結束后, 獲取驗證碼 按鈕會自動處于可點擊狀態。這里處理的是正在倒數時, 獲取驗證碼 按鈕上顯示的文字。
viewModel.countdownCommand()
.switchToLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> codeButton.setText(s));
處理倒數完成后的情形,讓 獲取驗證碼 按鈕上的文字重置。
viewModel.countdownCommand()
.executing()
.subscribe(executing -> {
if (!executing) {
codeButton.setText("fetch code");
}
});
登錄
現在,我們開始處理登錄的執行狀態,當用戶點擊 登錄 按鈕時,按鈕將不可再點,并且show loading, 告訴用戶正在登錄, 一旦登錄成功或者發生異常,dismiss loading.
viewModel.loginCommand()
.executing()
.subscribe(executing -> {
if (executing) {
loginButton.setText("login...");
} else {
loginButton.setText("login");
}
});
這里處理了登錄成功后的情形
viewModel.loginCommand()
.switchToLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(val -> {
Toast.makeText(LoginActivity.this, "login success!! Now goto the MainActivity.", Toast.LENGTH_LONG).show();
});
異常處理
萬一發生異常呢?我們在這里一并處理獲取驗證碼和登錄發生異常的情況。
Observable.merge(
viewModel.codeCommand().errors(),
viewModel.loginCommand().errors())
.subscribe(throwable ->
Toast.makeText(LoginActivity.this, throwable.getLocalizedMessage(), Toast.LENGTH_LONG).show()
);
從上面的例子可以看到,RxCommand 有效分離了enabled, executing, success, error 等異步任務常有的關注點,使我們可以有選擇地處理它們,各個處理的代碼互不依賴。
實現 ViewModel
前面我們說過, ViewModel 是一個普通類。 要實現上面整個業務流程,ViewModel 中的代碼僅有 110 行。
public class LoginViewModel {
//這個 command 負責倒計時
private RxCommand<String> _countdownCommand;
//這個 command 負責獲取驗證碼
private RxCommand<String> _codeCommand;
//這個 command 負責登錄
private RxCommand<Boolean> _loginCommand;
//用來接收用戶輸入的手機號碼
private Subject<CharSequence> _phone;
//用來接收用戶輸入的驗證碼
private Subject<CharSequence> _code;
//驗證用戶輸入的驗證碼是否合法
private Observable<Boolean> _codeValid;
//驗證用戶輸入的手機號碼是否合法
private Observable<Boolean> _phoneValid;
public LoginViewModel() {
_phone = BehaviorSubject.create();
_code = BehaviorSubject.create();
_codeValid = _code.map(s -> s.toString().length() == 6);
_phoneValid = _phone.map(s -> s.toString().length() == 11);
}
}
倒計時
讓我來看看 countdownCommand 該怎么實現
public RxCommand<String> countdownCommand() {
if (_countdownCommand == null) {
_countdownCommand = RxCommand.create(o -> Observable
.interval(1, TimeUnit.SECONDS)
.take(10)//from 0 to 9
.map(aLong -> "fetch " + (9 - aLong) + "'"));
}
return _countdownCommand;
}
我們通過 RxCommand 的靜態工廠方法 #create 來創建一個 Command, 它接收一個函數作為參數, 這個函數接收一個 obj ( 可以為 null ), 返回一個 Observable . 這個 Observable 把倒計的時間轉換為可以在 獲取驗證碼 按鈕顯示的文字。
獲取驗證碼
RxCommand 還有另外一個靜態工廠方法,它除了接收一個返回 Observable 的函數,還接收一個發射 Boolean 的 Observable 作為參數。這個 Observable 發射的值決定了 command 是否可以執行, 反應到界面上就是按鈕是否可點擊。
public RxCommand<String> codeCommand() {
if (_codeCommand == null) {
//構造第一個參數,來決定獲取驗證碼按鈕是否可以點擊
Observable<Boolean> enabled = Observable.combineLatest(
_phoneValid,
countdownCommand().executing(),
//當手機輸入合法以及倒計時 command 不在執行時
(valid, executing) -> valid && !executing);
_codeCommand = RxCommand.create(enabled, o -> {
String phone = _phone.blockingFirst().toString();
//通過網絡獲取驗證碼
Observable fetchCode = fetchVerificationCode(phone);
//倒計時,用defer來使倒計時延遲到獲取驗證碼執行成功后才開始倒數
Observable countdown = Observable.defer(
() -> countdownCommand().execute(null).ignoreElements().toObservable()
);
//把獲取驗證碼和倒計時串起來,獲取驗證碼成功后就開始倒數
return Observable.concat(fetchCode, countdown);
});
}
return _codeCommand;
}
登錄
登錄的邏輯處理比獲取驗證碼簡單多了,畢竟獲取驗證碼和倒計時這兩個 command 需要聯動。
public RxCommand<Boolean> loginCommand() {
if (_loginCommand == null) {
//登錄按鈕是否用,決定于用戶輸入的手機號碼以及驗證碼是否合法
Observable<Boolean> inputValid = Observable.combineLatest(
_codeValid,
_phoneValid,
(codeValid, phoneValid) -> codeValid && phoneValid);
_loginCommand = RxCommand.create(inputValid, o -> {
String phone = _phoneNumber.blockingFirst().toString();
String code = _code.blockingFirst().toString();
//調用登錄的邏輯處理,這個方法返回一個Observable
return login(phone, code);
});
}
return _loginCommand;
}
來看看 login 的模擬實現
private Observable<Boolean> login(String phoneNumber, String code) {
return Observable.timer(4, TimeUnit.SECONDS)
.flatMap(aLong -> {
if (phoneNumber.equals("18503002163")){
return Observable.error(new RuntimeException("the phone number is not yours!"));
} else if (code.equals("123456")) {
return Observable.just(true);
} else {
return Observable.error(new RuntimeException("your code is wrong!!"));
}
});
}
集成到項目中
dependencies {
compile 'com.shundaojia:rxcommand:1.0.0'
}
來自:https://listenzz.github.io/使用RxCommand在Android上實現MVVM