纯粹使用 RxJava 实现 ViewModel

栏目: 后端 · 发布时间: 7年前

内容简介:纯粹使用 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 来演示我们的实现方式。 你可以在 To do app specification 看到这个 APP 的功能介绍。

纯粹使用 RxJava 实现 ViewModel

对 MVVM 比较熟悉的读者,可以直接去看代码了。

$ git clone git@github.com:listenzz/android-architecture.git
$ git checkout todo-mvvm-rxcommand

和 todo-mvvm-databinding 分支对照着看,效果更佳。

如果你想运行项目,记得选择 prodDebug 的 Build Variant, 才会有初始数据。

纯粹使用 RxJava 实现 ViewModel

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 作为状态管理容器。

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

GUI设计禁忌2.0

GUI设计禁忌2.0

Jeff Johnson / 盛海艳 等 / 机械工业出版社 / 2008 / 49.00元

本书描述软件开发人员在设计图形用户界面(GUI)时经常犯的“禁忌”,并提出避免这些错误的基本原则和理论依据。本书将GUI禁忌分为7种类型:GUI控件禁忌、导航禁忌、文字禁忌、图形设计和布局禁忌、交互禁忌、响应性禁忌以及管理禁忌,并分别进行详述。 本书编排独特,条理清晰,针对性极强,是不可多得的GUI设计优秀资源。本书适合软件开发人员、web站点设计人员、开发经理、用户界面设计人员等阅读。一起来看看 《GUI设计禁忌2.0》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

URL 编码/解码
URL 编码/解码

URL 编码/解码

MD5 加密
MD5 加密

MD5 加密工具