Mvvm模式: Databinding 与 ViewModel+LiveData+Repository(上篇)

阿凡达2018-07-20 13:46

前言:

本文主要是对常见设计模式的一些分析,以及讲述在Android项目中实现Mvvm模式的两种方式。通过Databinding或者ViewModel+LiveData+Repository如何实现Mvvm的相关设计以及他们各自优缺点的一些比较。

作为一名移动开发者,在项目开发的过程中,总会遇到一些问题。比如,在现在的项目开发过程中,就遇到一个类中或者说一个模块的代码逻辑过多的问题,尤其是在Activity/Fragment/View中动辄上千行代码,各种数据请求逻辑(网络,数据库),数据处理逻辑(排序,分类),UI渲染(View层本职工作)全部混杂在其中。写的时候非常潇洒,思路很清晰,想到一步写一步。然而等到再过一段时间需要修改这些功能的时候,发现自己已经无法找回原来自己的思路了。UI与数据逻辑耦合严重,导致后期代码维护和功能扩展时会异常艰难。基于上述的考虑和往常经验,没错,设计模式可以帮助我们解决这些问题。那么下面,我们先回顾下这些设计模式以及他们各自的优缺点。

设计模式组图摘自: MVC,MVP 和 MVVM 的图示

MVC模式:

视图(View):用户界面。 控制器(Controller):业务逻辑 模型(Model):数据保存

1、View 传送指令到 Controller

2、Controller 完成业务逻辑后,要求 Model 改变状态

3、Model 将新的数据发送到 View,用户得到反馈

MVP模式:

MVP 模式将 Controller 改名为 Presenter,同时改变了通信方向。

1、 各部分之间的通信,都是双向的。

2、View 与 Model 不发生联系,都通过 Presenter 传递。

3、View 非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。

基于上述MVC模式的优点,在实际开发中,MVP模式被更多的应用在项目开发过程中了。

MVVM模式

MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。

唯一的区别是,它采用双向绑定(>

各模式在Android项目中的应用:

MVC模式:

View:对应于xml布局文件 Model:实体模型 Controllor:对应于Activity业务逻辑,数据处理和UI处理

在Android开发过程中,乍一看Activity还是比较符合MVC的设计模式的。Xml文件对应View层,负责UI的呈现,数据实体对应model层,而Activity作为Controller,负责处理数据逻辑之后,将数据交给UI层去展示。问题在于在此模式下,Xml作为View层的职责实在太弱,比如涉及到背景切换,文字大小颜色变化等等,都不能直接在xml中进行设置和修改,还是需要Activity中进行相应的切换。这样就导致,Activity作为View层+Controller层,其功能涵盖UI渲染和数据逻辑获取及处理,导致逻辑和UI 耦合严重,甚至经常出现上千行甚至几千行代码的情况,不利于代码的维护和后续扩展。

MVP模式:

View:对应于Activity/Fragment/自定义View,主要负责UI渲染。 Model:实体模型 Presenter: 负责数据处理以及View和Model的交互等,持有Model和View的引用。

在MVP模式中,View层只负责UI渲染,不再需要处理对应的业务逻辑,View层的量级大大轻化。而Presenter在获取到Model的数据,并进行处理后,通过View层暴露的接口调用去更新UI,这样View层和 Model层不互相持有引用,保证是隔离和解耦的。这样整个业务逻辑处理和视图的渲染是隔离的,就不会再出现上文提到的Activity/Fragment/View臃肿和混乱的问题。 但是MVP模式也会存在一系列的缺点:

1、Presenter层要处理的业务逻辑过多,复杂的业务逻辑会使P层非常庞大和臃肿。

2、Presenter通过接口方式持有View层引用,接口及接口中声明的方法粒度无法把握,可能需要声明大量接口以及接口中需要声明太多方法,而其中有些方法是否会用到以及是否会增加或删减还需要后续进一步确认。

3、Activity中需要声明大量跟UI相关的方法,而相应的事件通过Presenter调用相关方法来实现。两者互相引用和调用,存在耦合。一旦View层的UI视图发生改变,接口中的方法就需要改变,View层和P层同时都需要修改。

MVVM模式:

View:对应于Activity/Fragment/自定义View,主要负责UI渲染。 Model:实体模型 ViewModel: 负责业务逻辑处理,负责View和Model的交互。和View层双向绑定。

Mvvm模式是通过将View层和ViewModel层进行双向绑定, View层的变化会自动通知给ViewModel层,而ViewModel层的数据变化也会通知给View层进行相应的UI的更新。这样,Model层只负责暴露获取数据的方法,View层只负责监听数据的变化更新,而ViewModel负责接收View层的事件指令以及获取并处理数据。从而实现业务逻辑和Ui的隔离。

使用MVVM模式的优点:

1、低耦合度:

在MVVM模式中,数据处理逻辑是独立于UI层的。ViewModel只负责提供数据和处理数据,不会持有View层的引用。而View层只负责对数据变化的监听,不会处理任何跟数据相关的逻辑。在View层的UI发生变化时,也不需要像MVP模式那样,修改对应接口和方法实现,一般情况下ViewModel不需要做太多的改动。

2、数据驱动:

MVVM模式的另外一个特点就是数据驱动。UI的展现是依赖于数据的,数据的变化会自然的引发UI的变化,而UI的改变也会使数据Model进行对应的更新。ViewModel只需要处理数据,而View层只需要监听并使用数据进行UI更新。

3、异步线程更新Model:

Model数据可以在异步线程中发生变化,此时调用者不需要做额外的处理,数据绑定框架会将异步线程中数据的变化通知到UI线程中交给View去更新。

4、方便协作:

View层和逻辑层几乎没有耦合,在团队协作的过程中,可以一个人负责Ui 一个人负责数据处理。并行开发,保证开发进度。

5、易于单元测试:

MVVM模式比较易于进行单元测试。ViewModel层只负责处理数据,在进行单元测试时,测试不需要构造一个fragment/Activity/TextView等等来进行数据层的测试。同理View层也一样,只需要输入指定格式的数据即可进行测试,而且两者相互独立,不会互相影响。

6、数据复用:

ViewModel层对数据的获取和处理逻辑,尤其是使用Repository模式时,获取数据的逻辑完全是可以复用的。开发者可以在不同的模块,多次方便的获取同一份来源的数据。同样的一份数据,在版本功能迭代时,逻辑层不需要改变,只需要改变View层即可。

Databinding的使用:

(对databinding已经比较熟悉的同学可以直接忽略本章)

Google之前推出Android Mvvm模式的框架,Databinding。先来看下Databinding是如何实现Mvvm模式的数据绑定框架的。

build.gradle中添加:

android {
    ....
    dataBinding {
        enabled = true
    }
}

先来看下布局文件:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"/>
   </LinearLayout>
</layout>

其中需要注意的是,Databinding的布局文件中是以layout为根节点的,然后通过

   <data>
       <variable name="user" type="com.example.User"/>
   </data>

这种方式来声明类中需要使用的数据Model。 在layout中绑定数据时,

<TextView android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="@{user.firstName}"/>
绑定的数据实体

使用@{}语句来将User的firstName属性和TextView进行绑定。 其中进行数据绑定的数据Model的属性是不可变的。

public class User {
   public final String firstName;
   public final String lastName;
   public User(String firstName, String lastName) {
       this.firstName = firstName;
       this.lastName = lastName;
   }
}
public class User {
   private final String firstName;
   private final String lastName;
   public User(String firstName, String lastName) {
       this.firstName = firstName;
       this.lastName = lastName;
   }
   public String getFirstName() {
       return this.firstName;
   }
   public String getLastName() {
       return this.lastName;
   }
}

在databinding时,上述两种写法是等价的。android:text="@{user.firstName}" 通过这种方式,android:text就会和User的firstName属性以及getFirstName()方法绑定。当然如果存在firstName()方法也是可以的。

数据绑定

默认情况下,databinding会根据layout文件的名字生成一个binding class。比如如果布局文件是main_activity.xml,就会生成一个MainActivityBinding的class。MainActivityBinding知道布局文件中所有的View属性以及他们的绑定关系(比如上文中的User),还有如何通过binding表达式对他们进行赋值。下面有个简单的示例:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
   User user = new User("Test", "User");
   binding.setUser(user);
}

通过上面的方式就完成了User和View的绑定。很明显,你也可以通过下述方式获取整个layout的View。

MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());

如果你是在ListView 或者RecycleView的Adapter中bind Item,你可以通过如下方式获取:

ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
事件绑定

Databinding支持你写表达式的方式来处理从View分发出的事件(比如android:onClick)。事件的属性名字是有Listener的方法名决定的,需要保持一致。比如View.OnLongClickListener就有一个onLongClick()的方法,那么这个事件的属性就是android:onLongClick。

public class MyHandlers {
    public void onClickFriend(View view) { ... }
    public void onLongClickFriend(View view){...}
}

Binding表达式可以为View添加一个ClickListener:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="handlers" type="com.example.MyHandlers"/>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"
           android:onLongClick="@{handlers::onLongClickFriend}"
           android:onClick="@{handlers::onClickFriend}"/>
   </LinearLayout>
</layout>

需要注意的是表达式中的方法签名需要和Listener中的方法签名保持一直,否则是会报错的。 事件绑定是有两种方式的,Method References和Listener Bindings 。他们之间最大的差别就是,Method References的Listener实现创建是在数据被绑定时就完成的,而Listener Bindings 则是在相应事件触发的时候才会执行绑定表达式的操作。这里不再做过多介绍,感兴趣的可以直接去官方文档查看。

相应类的import

databinding支持像Java一样,直接引用其他的类。

<data>
    <import type="com.example.MyStringUtils"/>
    <import type="android.view.View"/>
    <variable name="user" type="com.example.User"/>
</data><TextView
   android:text="@{MyStringUtils.capitalize(user.lastName)}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
/>
Databinding支持表达式:

可以在layout中 像Java一样使用如下的一些表达式:

数学表达式 + – / * %

字符串链接 +

逻辑操作符 && ||

二元操作符 & | ^

一元操作符 + – ! ~

Shift >> >>> <<

比较 == > < >= <=

instanceof

Grouping ()

Literals – character, String, numeric, null

Cast

函数调用

值域引用(Field access)

通过[]访问数组里面的对象

三元操作符 ?: 示例:

android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
Observable objects, observable collections. 当他们中的任何一个与UI进行绑定,并且其数据属性发生变化时,都会通知到UI进行更新。

Observable Objects:

private static class User extends BaseObservable {
   private String firstName;
   private String lastName;
   @Bindable
   public String getFirstName() {
       return this.firstName;
   }
   @Bindable
   public String getLastName() {
       return this.lastName;
   }
   public void setFirstName(String firstName) {
       this.firstName = firstName;
       notifyPropertyChanged(BR.firstName);
   }
   public void setLastName(String lastName) {
       this.lastName = lastName;
       notifyPropertyChanged(BR.lastName);
   }
}

注意,此时User需要继承 BaseObservable ,相应属性获取方法需要添加注解@Bindable,而且在setValue时需要调用notifyPropertyChanged(BR.propertyName)。

ObservableFields:

如果你的Entity不想继承BaseObservable 以及做上述的那些操作,你也可以使用ObservableFields。 android.databinding.ObservableField android.databinding.ObservableBoolean, android.databinding.ObservableByte, android.databinding.ObservableChar, android.databinding.ObservableShort, android.databinding.ObservableInt, android.databinding.ObservableLong, android.databinding.ObservableFloat, android.databinding.ObservableDouble, android.databinding.ObservableParcelable. ObservableFields 等等。使用时,在Data Class中创建一个Public final 的属性即可。

private static class User {
   public final ObservableField<String> firstName =
       new ObservableField<>();
   public final ObservableField<String> lastName =
       new ObservableField<>();
   public final ObservableInt age = new ObservableInt();
}

通过下述方式就可以设置并得到Data 属性的值。

user.firstName.set("Google");
int age = user.age.get();

使用set方法时,会实时通知到UI进行相应更新。

Observable Collections:

当key是引用类型,比如String时, android.databinding.ObservableArrayMap 是比较有用的。

ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);

在layout中,map可以通过String类型的Key获取到对应的Value

<data>
    <import type="android.databinding.ObservableMap"/>
    <variable name="user" type="ObservableMap<String, Object>"/>
</data>
…
<TextView
   android:text='@{user["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
<TextView
   android:text='@{String.valueOf(1 + (Integer)user["age"])}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

key是整数时,可以使用android.databinding.ObservableArrayMap

ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
<data>
    <import type="android.databinding.ObservableList"/>
    <import type="com.example.my.app.Fields"/>
    <variable name="user" type="ObservableList<Object>"/>
</data>
…
<TextView
   android:text='@{user[Fields.LAST_NAME]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
<TextView
   android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
自定义绑定
相信这时候大家就会有这个疑问了,那如果我给一些View添加了自定义的方法或者属性(官方不支持的方法),该如何进行绑定呢?是的,databinding当然也是支持自定义进行绑定。 我们先找一个官方支持的属性看时如何实现的,比如android:paddingLeft,是可以单独进行设置的,但是View的方法中,是没有setPaddingLeft的方法的,只有setPadding(left, top, right, bottom)方法。那么此时单独绑定paddingLeft就如下:
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
   view.setPadding(padding,
                   view.getPaddingTop(),
                   view.getPaddingRight(),
                   view.getPaddingBottom());
}
Binding adapters在其他类型的定制上是很有用的。当存在冲突时,开发者自己创建的binding adapter会覆盖默认的adpter。你也可以创建接收多个参数的adapter。
@BindingAdapter({"bind:imageUrl", "bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
   Picasso.with(view.getContext()).load(url).error(error).into(view);
}
<ImageView app:imageUrl="@{venue.imageUrl}"
app:error="@{@drawable/venueError}"/>
这个adapter只会在ImageView存在String类型的url值和Drawable类型的error值传入时才会触发执行。
当一个Listener有两个方法时,我们需要将他们拆成两个接口。比如 View.OnAttachStateChangeListener有两个方法,onViewAttachedToWindow()) 和 onViewDetachedFromWindow()) 我们需要需要为其创建两个不同的接口和Handler 方法。
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
    void onViewDetachedFromWindow(View v);
}

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
    void onViewAttachedToWindow(View v);
}
因为改变一个Listener就会影响到另一个,所以我们需要建三个Binding Adapters,以便他们的方法可以被单独设置,也可以一起同时被设置。
@BindingAdapter("android:onViewAttachedToWindow")
public static void setListener(View view, OnViewAttachedToWindow attached) {
    setListener(view, null, attached);
}

@BindingAdapter("android:onViewDetachedFromWindow")
public static void setListener(View view, OnViewDetachedFromWindow detached) {
    setListener(view, detached, null);
}

@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"})
public static void setListener(View view, final OnViewDetachedFromWindow detach,
        final OnViewAttachedToWindow attach) {
    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
        final OnAttachStateChangeListener newListener;
        if (detach == null && attach == null) {
            newListener = null;
        } else {
            newListener = new OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {
                    if (attach != null) {
                        attach.onViewAttachedToWindow(v);
                    }
                }

                @Override
                public void onViewDetachedFromWindow(View v) {
                    if (detach != null) {
                        detach.onViewDetachedFromWindow(v);
                    }
                }
            };
        }
        final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view,
                newListener, R.id.onAttachStateChangeListener);
        if (oldListener != null) {
            view.removeOnAttachStateChangeListener(oldListener);
        }
        if (newListener != null) {
            view.addOnAttachStateChangeListener(newListener);
        }
    }
}
相关阅读:
本文来自网易实践者社区,经作者朱强龙授权发布。