纯粹使用 RxJava 实现 ViewModel
790266850
7年前
<p>在阅读本文前,你需要对什么是 MVC、MVP、MVVM 以及它们之间的区别有清楚的认识,如果你不太清楚,推荐你看 <a href="/misc/goto?guid=4959749640708573803" rel="nofollow,noindex">MVC vs. MVP vs. MVVM on Android</a> .</p> <p>说到 Android MVVM,相信大家都会想到 Google 2015 年推出的Data Binding Library. 然而两者的概念是不一样的,不能混为一谈。MVVM 是一种架构模式,而 Data Binding Library 是一个实现数据和 UI 绑定的框架,是构建 MVVM 模式的一个工具。</p> <p>我们不会用到 Data Binding Library, 因为在复杂的业务中,并不仅仅是简单的在 XML 中绑定就能解决问题,对部分属性变化和事件触发的处理仍然需要在 View (Activity / Fragment) 中编写 Java 代码,并且为了实现复杂数据的绑定,需要编写各种绑定适配器,这样会导致代码的分散,给阅读和维护代码带来不便。</p> <p>我们提供了基于 <a href="/misc/goto?guid=4959749640792816262" rel="nofollow,noindex">RxJava</a> 的实现方式,所有的数据和事件都在 Activity / Fragment 中绑定,便于阅读和维护代码。RxJava 中的 Observable 和 Data Binding Library 中的 ObservableFiled 一样是可观测的,而且,RxJava 提供了强大的数据映射、转换、过滤、组合功能,能够轻松处理非常复杂的业务问题。</p> <h2>Demo</h2> <p>我们通过大家比较熟悉的 <a href="/misc/goto?guid=4959749640873531053" rel="nofollow,noindex">TO-DO APP</a> 来演示我们的实现方式。</p> <p><img src="https://simg.open-open.com/show/97991632b97a63aa00927aafbfdfb2f2.gif"></p> <p>todo</p> <p>对 MVVM 比较熟悉的读者,可以直接去看代码了。</p> <pre> <code class="language-java">$ git clone git@github.com:listenzz/todo-android.git $ git checkout todo-mvvm-rxcommand</code></pre> <p>和 todo-mvvm-databinding 分支对照着看,效果更佳。</p> <p>如果你想运行项目,记得选择 prodDebug 的 Build Variant, 才会有初始数据。</p> <p><img src="https://simg.open-open.com/show/b519e5e6333060f49de1c297a1bfa023.png"></p> <p>todo-build-variants</p> <h2>MVVM</h2> <p>读者可能对 MVP 比较熟悉,而不了解 MVVM. MVVM 中 ViewModel 扮演的角色和 MVP 中 Presenter 的角色是非常相似的。这两个架构的主要区别就是 View 和 ViewModel 或 Presenter 的通信方式。</p> <ul> <li>在 MVVM 中,当 app 修改了 ViewModel, View 会自动更新。你不可以从 ViewModel 中直接更新 View, 因为 ViewModel 不持有 View 的引用。</li> <li>在 MVP 中,你可以通过 Presenter 来更新 View,因为 Presenter 持有 View 的引用。当需要更改时,你可以通过 Presenter 显式地调用 View 来更新它。</li> </ul> <h2>ViewModel</h2> <p>维基百科是这样定义 ViewModel 的:</p> <p>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.</p> <p>ViewModel 的职责就是对 Model 进行包装,准备 View 需要的可观测数据(public properties),以及提供 View 可以传递事件给 Model 的钩子(commands)。当 ViewModel 准备好这些东西后,就需要绑定到 View. 如果使用 Data Binding Library, 绑定就主要发生在 XML 中,如果像本文那样基于 RxJava, 绑定就发生在 Activity 或 Fragment 中。</p> <p>我们抽取 todo-mvvm-databinding 和 todo-mvvm-rxcommand 这两个分支中同样的类来对比讲解数据和事件的绑定。我们抽取的类是 AddEditTaskViewModel , 因为它比较简单,但是足够说明问题。</p> <h3>Data Binding</h3> <p>ViewModel 如何提供可观测的属性? 这些属性怎样绑定到 View ?</p> <p>我们先来看看,使用 Data Binding Library 的代码长得是什么样子的。</p> <ul> <li>首先在 AddEditTaskViewModel 中定义 ObservableField</li> </ul> <pre> <code class="language-java">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); }</code></pre> <ul> <li>在代码中适当的地方,设置 ObservableField, 每次设置 ObservableField 时,UI 就会自动更新</li> </ul> <pre> <code class="language-java">// 这是个回调函数,稍后会讲解如何发起请求拉取 task public void onTaskLoaded(Task task) { title.set(task.getTitle()); description.set(task.getDescription()); dataLoading.set(false); // 设置数据已经加载完成,这时 loading 会停止 }</code></pre> <ul> <li>在 XML 中,绑定这些定义在 ViewModel 中的 ObservableField</li> </ul> <pre> <code class="language-java"><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></code></pre> <p>使用 RxJava 又该如何提供可绑定的数据,以及如何绑定呢?</p> <ul> <li>首先在 AddEditTaskViewModel 中声明 Observable.</li> </ul> <pre> <code class="language-java">public class AddEditTaskViewModel { public final Observable<String> title; public final Observable<String> description; //如何实现 loading,在 Event Binding 一节中会讲到 }</code></pre> <ul> <li>在构造函数中定义这些 Observable.</li> </ul> <pre> <code class="language-java">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()); }</code></pre> <ul> <li>在 AddEditTaskFragment 中,绑定这些定义在 ViewModel 中的 Observable, 每当数据发生变化时,相应的 UI 就会自动更新。</li> </ul> <pre> <code class="language-java">@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)); }</code></pre> <h3>Event Binding</h3> <p>事件就是页面显示或隐藏这些 app 生命周期事件,或者点击按钮、下拉刷新这些用户操作。事件绑定就是当这些事件发生时,调用定义在 ViewModel 中的方法。就是这么简单,没什么高深的概念。</p> <p>在我们这个页面中,有两个事件,一个是页面生命周期事件 onResume , 另外一个是保存按钮的点击事件。</p> <p>先来看看使用 Data Binding Library 是如何定义 command 的,在 AddEditTaskViewModel 中:</p> <pre> <code class="language-java">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); }</code></pre> <p>你没看错,command 就是普通的方法。</p> <p>再来看看使用 Data Binding Library 是如何将命令绑定到事件的。 事件和属性一样是可以直接通过 XML 绑定的,在 todo-mvvm-databinding 这个 git 分支中,你可以看到这样的例子。不过在 AddEditTaskFragment 这个页面中,这两个事件都是通过代码来绑定。这些绑定发生在 AddEditTaskFragment 中:</p> <pre> <code class="language-java">@Override public void onResume() { super.onResume(); if (getArguments() != null) { mViewModel.start(getArguments().getString(ARGUMENT_EDIT_TASK_ID)); } else { mViewModel.start(null); } }</code></pre> <p>没错,这就是绑定。</p> <p>使用 RxJava 又该如何提供可绑定的命令,以及如何绑定呢?</p> <p>命令可以是个普通的方法,当然也可以封装成一个对象。</p> <p>RxCommand 是一个基于 RxJava 的,为 ViewModel 提供命令绑定到 View 的非常轻量级的库,含注释 300 来行代码。读者可以通过 <a href="/misc/goto?guid=4959749640957741319" rel="nofollow,noindex">《使用 RxCommand 在 Android 上实现 MVVM》</a> 一文详细了解 RxCommand 的用法。 RxCommand 分离了任务执行的关注点,让我们可以有选择地关注任务是否在执行,是否发生了异常,从而决定是否显示 loading,提示错误信息等等。</p> <p>来看看我们是如何定义 command 的,以下是 AddEditTaskViewModel 的构造函数的完整定义:</p> <pre> <code class="language-java">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())); }</code></pre> <p>如何绑定呢 ?</p> <pre> <code class="language-java">@Override public void onResume() { super.onResume(); if (getArguments() != null) { mViewModel.populateTaskCommand.execute(getArguments().getString(ARGUMENT_EDIT_TASK_ID)); } else { mViewModel.populateTaskCommand.execute(null); } }</code></pre> <p>现在让我们来处理 loading, 使用 Data Binding Library 时,我们定义了一个名为 dataLoading 的 ObservableBoolean, 绑定到 XML 来控制 loading 的显示。那么使用 RxJava 该如何实现呢?我们强大的 RxCommand 登场了,来看看它是怎么处理是否正在加载中的情形的。 还是在 onViewCreated 中</p> <pre> <code class="language-java">mViewModel.populateTaskCommand .executing() .skip(1) // command 没执行前默认会发送一次 false, 我们跳过它 .subscribe(executing -> { // 根据是否正在执行来决定是否显示 loading mRefreshLayout.setRefreshing(executing); // 根据是否正在执行来显示相应界面 if (executing) { mContentLayout.setVisibility(View.GONE); } else { mContentLayout.setVisibility(View.VISIBLE); } });</code></pre> <p>另外 RxJava 和 Data Binding Library 不是互斥的,如果你喜欢 Data Binding Library,也是可以利用 RxCommand 来帮你构建 command 的,比一个普通的方法好多了。</p> <h2>总结</h2> <p>属性和命令是 ViewModel 的主要组成,ViewModel 也不可避免地需要依赖 Model,完成业务逻辑的转发。以下是个人在开发过程中总结的 Android MVVM 构建思想。</p> <ul> <li>纯代码实现的 MVVM,为了避免代码分散,ViewModel 的粒度不宜过细,只有 Activity 或 Fragment 这样级别的 View 才应该拥有 ViewModel,它们是一一对应的关系,同生共死。</li> <li>ViewModel 各自独立,互不依赖,也不会有子 ViewModel.</li> <li>ViewModel 和 View 协同处理页面呈现逻辑,ViewModel 不处理业务逻辑,业务逻辑是 Model 的职责。</li> <li>如果数据还不是适合展示的最终形态,View 不应该自己去转换格式,这是 ViewModel 的职责,ViewModel 根据情况提供适合 View 直接展示的数据,或者提供可以转换成适合展示的数据的方法给 View 调用。</li> <li>ViewModel 管理 local state, 状态管理容器管理 global state. ViewModel 之间通过状态管理容器共享状态。笔者所在公司使用 RxJava 来实现 Redux 作为状态管理容器。</li> </ul> <p> </p> <p>来自:https://juejin.im/post/5934f898ac502e0068adeccc</p> <p> </p>