使用 RxCommand 在 Android 上实现 MVVM

pinkboy55 8年前
   <p>RxCommand 是一个基于 RxJava 的,UI 相关的,主要用来响应用户触发的异步任务,尤其是网络访问的库。它分离了对异步任务的关注点,譬如任务是否处于可执行状态,任务是否正在执行,任务返回结果,任务执行过程中发生错误。这些关注点以 Observable 的形式返回,可以有选择性地订阅,以及和其它流组合,处理复杂的业务逻辑。这个库相当轻量级,包含注释在内300来行代码。</p>    <h2>前提</h2>    <p>你喜欢 RxJava , 并且已在项目中实践。</p>    <h2>为什么放弃MVP</h2>    <p>MVP 中的 V 和 P 都拥有对对方的控制权。它们相互调用代码,同一个逻辑流程的代码分散各处,给阅读和理解代码带来不便。而 MVVM 中的 VM 不持有 V , V 观察 VM 中的状态变更来刷新自己的界面,可以在一个地方处理完相关的逻辑。 所以MVP是命令式的,而MVVM是响应式的。</p>    <h2>为什么选择RxCommand</h2>    <p>MVP 是Android开发社区目前比较流行的架构,而 MVVM 是iOS和前端开发社区目前比较流行的架构。造成这种局面的最大原因是语言的差异,以 Objective-C 为例, 它有一项很酷的语言特性,那就是 KVO , 全称是键值观测,一个对象的属性自带观察者模式,在它的值发生变化时,你可以轻易地得到通知。 借助 ReactiveCocoa 只需要一行代码,就可以把从 View 接收到的输入绑定到 ViewModel.</p>    <pre>  RAC(viewModel, username) = RACObserve(view, textView);</pre>    <p>也只需要一行代码,就可以把 ViewModel 的状态变更反馈到 View 上:</p>    <pre>  RAC(view, label) = RACObserve(viewModel, email);</pre>    <p>Google 开发了 Data Biding 来帮助开发者在 Android 上实现 MVVM, 不过实现起来比较繁琐,尤其是把代码写在 XML 中,实在是不雅。</p>    <p>RxCommand具有如下特点:</p>    <ul>     <li>基于 RxJava , 和 RxJava 完美配合</li>     <li>配合 RxBinding 等框架,不需要把代码写在 XML 中,可以实现双向绑定</li>     <li>分离关注点,便于有选择地处理任务执行的状态(执行中,错误,完成等等)</li>     <li>ViewModel是个普通类</li>     <li>相关代码集中,便于阅读和维护</li>    </ul>    <h2>Demo</h2>    <p>我们通过一个 Demo 来讲解如何通过 RxCommand 来实现 V 和 VM . 看图:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/3790f7bc7f95186924036610faf1acd7.png"></p>    <p>假设我们需要做一个登录功能,这太常见了。</p>    <ul>     <li>当手机号码不合法时,获取验证码按钮处于disable状态</li>     <li>当正在获取验证码时,获取验证码按钮处于disable状态,并显示loading</li>     <li>当获取验证码成功时,倒计时开始,按钮仍处于disable状态,倒计时结束,按钮重新可用</li>     <li>当获取验证码失败时,不会开始倒计时,按钮恢复可用状态</li>     <li>当手机号码和验证码都合法时,登录按钮处于可点击状态,否则不可点击</li>     <li>当点击登陆按钮时,登录按钮处于不可点状态,同时显示loading</li>     <li>当登录成功时,停止 loading, 跳转到主界面</li>     <li>当登录失败时,停止 loading, 并提示错误</li>    </ul>    <h3>源码</h3>    <p>以下是对 Demo 源码的解读,如果你想直接看代码,请看 <a href="/misc/goto?guid=4959746054490169978" rel="nofollow,noindex">完整的项目源码以及 demo</a> .</p>    <h3>实现 View</h3>    <p>让我们来看看,要实现以上需求,LoginActivity 该怎么写。</p>    <p>这四行代码就完成了接收用户输入,验证用户输入是否合法,来决定按钮是否可点击,以及响应用户点击按钮触发任务,在任务执行期间,按钮不可点击等需求。</p>    <pre>  //绑定用户输入到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());</pre>    <p>获取验证码</p>    <p>处理获取验证码的执行状态,如果正在获取,显示获取中,如果获取成功或发生错误,则重置。</p>    <pre>  viewModel.codeCommand()      .executing()      .subscribe(executing -> {          if (executing) {              codeButton.setText("fetch...");          } else {              codeButton.setText("fetch code");          }      });</pre>    <p>处理获取验证码成功的情形,command 内部已经过滤了发生错误的情形,不需要在这里处理失败的情况</p>    <pre>  viewModel.codeCommand()      .switchToLatest()      .observeOn(AndroidSchedulers.mainThread())      .subscribe(result -> Toast.makeText(LoginActivity.this, result, Toast.LENGTH_LONG).show());</pre>    <p>获取验证码成功后,会开启倒计时,避免用户在没及时收到短信的情况下狂点 <strong> 获取验证码 </strong> 按钮。 倒计时结束后, <strong> 获取验证码 </strong> 按钮会自动处于可点击状态。这里处理的是正在倒数时, <strong> 获取验证码 </strong> 按钮上显示的文字。</p>    <pre>  viewModel.countdownCommand()      .switchToLatest()      .observeOn(AndroidSchedulers.mainThread())      .subscribe(s -> codeButton.setText(s));</pre>    <p>处理倒数完成后的情形,让 <em>获取验证码</em> 按钮上的文字重置。</p>    <pre>  viewModel.countdownCommand()      .executing()      .subscribe(executing -> {          if (!executing) {              codeButton.setText("fetch code");          }      });</pre>    <p>登录</p>    <p>现在,我们开始处理登录的执行状态,当用户点击 <strong> 登录 </strong> 按钮时,按钮将不可再点,并且show loading, 告诉用户正在登录, 一旦登录成功或者发生异常,dismiss loading.</p>    <pre>  viewModel.loginCommand()      .executing()      .subscribe(executing -> {          if (executing) {              loginButton.setText("login...");          } else {              loginButton.setText("login");          }      });</pre>    <p>这里处理了登录成功后的情形</p>    <pre>  viewModel.loginCommand()      .switchToLatest()      .observeOn(AndroidSchedulers.mainThread())      .subscribe(val -> {           Toast.makeText(LoginActivity.this, "login success!! Now goto the MainActivity.", Toast.LENGTH_LONG).show();      });</pre>    <p>异常处理</p>    <p>万一发生异常呢?我们在这里一并处理获取验证码和登录发生异常的情况。</p>    <pre>  Observable.merge(      viewModel.codeCommand().errors(),      viewModel.loginCommand().errors())      .subscribe(throwable ->           Toast.makeText(LoginActivity.this, throwable.getLocalizedMessage(), Toast.LENGTH_LONG).show()      );</pre>    <p>从上面的例子可以看到,RxCommand 有效分离了enabled, executing, success, error 等异步任务常有的关注点,使我们可以有选择地处理它们,各个处理的代码互不依赖。</p>    <h3>实现 ViewModel</h3>    <p>前面我们说过, ViewModel 是一个普通类。 要实现上面整个业务流程,ViewModel 中的代码仅有 110 行。</p>    <pre>  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);    }   }</pre>    <p>倒计时</p>    <p>让我来看看 countdownCommand 该怎么实现</p>    <pre>  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;  }</pre>    <p>我们通过 RxCommand 的静态工厂方法 #create 来创建一个 Command, 它接收一个函数作为参数, 这个函数接收一个 obj ( 可以为 null ), 返回一个 Observable . 这个 Observable 把倒计的时间转换为可以在 <strong> 获取验证码 </strong> 按钮显示的文字。</p>    <p>获取验证码</p>    <p>RxCommand 还有另外一个静态工厂方法,它除了接收一个返回 Observable 的函数,还接收一个发射 Boolean 的 Observable 作为参数。这个 Observable 发射的值决定了 command 是否可以执行, 反应到界面上就是按钮是否可点击。</p>    <pre>  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;  }</pre>    <p>登录</p>    <p>登录的逻辑处理比获取验证码简单多了,毕竟获取验证码和倒计时这两个 command 需要联动。</p>    <pre>  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;  }</pre>    <p>来看看 login 的模拟实现</p>    <pre>  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!!"));              }          });  }</pre>    <h3>集成到项目中</h3>    <pre>  dependencies {      compile 'com.shundaojia:rxcommand:1.0.0'  }</pre>    <p> </p>    <p>来自:https://listenzz.github.io/使用RxCommand在Android上实现MVVM</p>    <p> </p>