在介绍MVVM框架之前,先给大家简单介绍一下MVC、MVP框架(由于本博文主要讲解MVVM,所以MVC和MVP将简化介绍,如果需要我将在以后的博文中补充进来)。
MVC框架:
- M-Model : 业务逻辑和实体模型(biz/bean)
- V-View : 布局文件(XML)
- C-Controller : 控制器(Activity)
相信大家都熟悉这个框架,这个也是初学者最常用的框架,该框架虽然也是把代码逻辑和UI层分离,但是View层能做的事情还是很少的,很多对于页面的呈现还是交由C实现,这样会导致项目中C的代码臃肿,如果项目小,代码臃肿点还是能接受的,但是随着项目的不断迭代,代码量的增加,你就会没办法忍受该框架开发的项目,这时MVP框架就应运而生。
MVP框架:
- M-Model : 业务逻辑和实体模型(biz/bean)
- V-View : 布局文件(XML)和Activity
- P-Presenter : 完成View和Model的交互
MVP框架相对于MVC框架做了较大的改变,将Activity当做View使用,代替MVC框架中的C的是P,对比MVC和MVP的模型图可以发现变化最大的是View层和Model层不在直接通信,所有交互的工作都交由Presenter层来解决。既然两者都通过Presenter来通信,为了复用和可拓展性,MVP框架基于接口设计的理念大家自然就可以理解其用意。
但MVP框架也有不足之处:
1.接口过多,一定程度影响了编码效率。
2.业务逻辑抽象到Presenter中,较为复杂的界面Activity代码量依然会很多。
3.导致Presenter的代码量过大。
MVVM框架:
- M-Model : 实体模型(biz/bean)
- V-View : 布局文件(XML)
- VM-ViewModel : DataBinding所在之处,对外暴露出公共属性,View和Model的绑定器
对比MVP和MVVM模型图可以看出,他们之间区别主要体现在以下两点:
1. 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多View重用这段视图逻辑。 在Android中,布局里可以进行一个视图逻辑,并且Model发生变化,View也随着发生变化。
2. 低耦合。以前Activity、Fragment中需要把数据填充到View,还要进行一些视图逻辑。现在这些都可在布局中完成(具体代码请看后面) 甚至都不需要再Activity、Fragment去findViewById()。这时候Activity、Fragment只需要做好的逻辑处理就可以了。
说了这么多理论知识,相信大家都有所厌烦了,下面就不来“虚”的了直接来“干”的,大家可能会问MVVM框架在Android怎么样使用?
Google在2015年的已经为我们提供DataBinding技术,以便让我们快速实现MVVM框架的实现。下面就详细讲解如何使用DataBinding?
由于本人使用的是AndroidStudio(以下简称AS),所以接下来都是关于AS相关使用规则:
1.检查你的AS版本,要求在1.3.0以上
2.Gradle 版本1.3.0-beta4以上
3.在工程根目录build.gradle文件加入如下配置:
dependencies { classpath "com.android.tools.build:gradle:1.3.0-beta4" classpath "com.android.databinding:dataBinder:1.0-rc1" } allprojects { repositories { jcenter() } }
4.在app里的build.gradle文件加入如下配置:
apply plugin: 'com.android.application' apply plugin: 'com.android.databinding'
下面来比较一下布局与之前大家常用的格式的区别:
<?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>
l 先将根布局改为layout
l 在布局里引入的model 中的数据类:
<variable name="user" type="com.example.User"/>(还有一种写法将在后面代码中介绍)
l 设置布局属性值,通过@{}语法:
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.firstName}"/>
数据实体类User:
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; } } 在Activity中进行数据的绑定: @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); }
这时你运行程序就会看到在界面上会显示你设置的测试用户数据,当然你还可以这样做去获取binding:
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
如果你使用的是ListView或者RecyclerView去显示界面,这时候在Items布局中使用Data Binding,在Adapter中你可以这样获取binding:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false); //or ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
接下来就是事件的实现,在我们以往的使用中对于事件的实现都是android:onClick或者在代码中使用View.setOnClickListener()来实现点击事件,在这里将有一种新的实现方式:
要将事件分配给它的处理程序,使用一个正常的绑定表达式,以值作为调用的方法名称。例如:你的数据对象有两种方法
public class MyHandlers { public void onClickFriend(View view) { ... } public void onClickEnemy(View view) { ... } }
绑定表达式可以为视图指定单击事件监听器
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="handlers" type="com.example.Handlers"/> <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:onClick="@{user.isFriend ?handlers.onClickFriend :handlers.onClickEnemy}"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.lastName}" android:onClick="@{user.isFriend ?handlers.onClickFriend :handlers.onClickEnemy}"/> </LinearLayout> </layout>
由于DataBinding对布局使用改动比较大,下面主要讲解一下布局:
①在布局中import导入
<data> <import type="android.view.View"/> </data>
之后在你的布局中通过View控件特性进行对其实现类隐藏和显示操作
<TextView android:text="@{user.lastName}" android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
可能有人会问如果我导入的类与已导入的类的名字冲突怎么办?那么接下来就会解决这个问题!
<import type="android.view.View"/> <import type="com.example.real.estate.View" alias="Vista"/>
这里的“alias”属性就是别名的意思,你可以采用别名的方式解决这个问题。
②当你在布局中引用的变量是一个List集合,需将集合的左”<”使用转义字符输入,如下(想要了解转义字符具体表达形式,请自行查询):
<data> <import type="com.example.User"/> <import type="java.util.List"/> <variable name="user" type="User"/> <variable name="userList" type="List<User>"/> </data>
③类型转换
<TextView android:text="@{((User)(user.connection)).lastName}" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
④当导入的类中存在静态属性和方法时,你也是可以在布局中直接使用
<data> <import type="com.example.MyStringUtils"/> <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"/>
⑤属性Variables:
在数据元素中可以使用任意数量的变量元素。每个可变元素描述一个属性,该属性可以设置在布局文件中的绑定表达式中使用的布局上:
<data> <import type="android.graphics.drawable.Drawable"/> <variable name="user" type="com.example.User"/> <variable name="image" type="Drawable"/> <variable name="note" type="String"/> </data>
⑥自定义Binding类的类名:
<data class="CustomBinding"></data> 在app_package/databinding下生成CustomBinding;
<data class=".CustomBinding"></data> 在app_package下生成CustomBinding;
<dataclass="com.example.CustomBinding"></data> 明确指定包名和类名。
⑦include使用
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:bind="http://schemas.android.com/apk/res-auto"> <data> <variable name="user" type="com.example.User"/> </data> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <include layout="@layout/name" bind:user="@{user}"/> <include layout="@layout/contact" bind:user="@{user}"/> </LinearLayout> </layout>
⑧DataBinding数据绑定不支持包括合并元素的直接子元素,例如下面的写法是不被允许的:
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:bind="http://schemas.android.com/apk/res-auto"> <data> <variable name="user" type="com.example.User"/> </data> <merge> <include layout="@layout/name" bind:user="@{user}"/> <include layout="@layout/contact" bind:user="@{user}"/> </merge> </layout>
注意:name.xml 和 contact.xml都必须包含 <variable name="user" ../>
⑨DataBinding支持的表达式有:
数学表达式: + - / *%
字符串拼接 +
逻辑表达式&& ||
位操作符 & | ^
一元操作符 + - ! ~
位移操作符 >>>>> <<
比较操作符 == >< >= <=
instanceof
分组操作符 ()
字面量 -character, String, numeric, null
强转、方法调用
字段访问
数组访问 []
三元操作符 ?
聚合判断(Null Coalescing Operator)语法 ‘??’
例如:
①
android:text="@{String.valueOf(index + 1)}" android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}" android:transitionName='@{"image_" + id}'
②
android:text="@{user.displayName ?? user.lastName}"
上面代码的意思是如果displayName为null,则显示lastName,否则显示displayName;
③
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
集合Collections
<data> <import type="android.util.SparseArray"/> <import type="java.util.Map"/> <import type="java.util.List"/> <variable name="list" type="List<String>"/> <variable name="sparse" type="SparseArray<String>"/> <variable name="map" type="Map<String, String>"/> <variable name="index" type="int"/> <variable name="key" type="String"/> </data> … android:text="@{list[index]}" … android:text="@{sparse[index]}" … android:text="@{map[key]}"
①String literals(字符串常量)
当使用单引号围绕属性值时,在表达式中使用双引号是很容易的:
android:text='@{map["firstName"]}'
②也可以使用双引号来环绕属性值。当你这样做的时候,String literals要么用";或反引号(`):
android:text="@{map[`firstName`}" android:text="@{map["firstName"]}"
Resources资源:
在DataBinding语法中,可以把resource作为其中的一部分:
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
除了支持dimen,还支持color、string、drawable、anim等。
注意:对mipmap图片资源支持还是有问题,目前只支持drawable。
一些资源要求需明确的类型赋值:
Type
Normal Reference
Expression Reference
String[]
@array
@stringArray
int[]
@array
@intArray
TypedArray
@array
@typedArray
Animator
@animator
@animator
StateListAnimator
@animator
@stateListAnimator
Color int
@color
@color
ColorStateList
@color
@colorStateList
到这里基本的属性使用方法介绍就结束了,下面大家应该对数据变化时,UI如何呈现很迷惑,好了,现在开始讲数据的变化如何让UI的更新?
任何普通的java对象(POJO)可用于数据绑定,但修改一个POJO不会造成UI更新。数据绑定(DataBinding)的真正力量可以通过给你的数据对象在数据改变时通知你来使用。有三种不同的数据变化通知机制,Observable objects, observable fields, and observable collections。
下面来逐个讲解:
Observable Objects
一个实现可观察到的接口的类,将允许绑定到绑定一个单一的监听器绑定到一个绑定对象,以监听该对象上的所有属性的更改;观察到的接口有一个机制来添加和删除监听器,但通知是由开发人员来进行的。为使开发更容易,一个基类,baseobservable,是为了实现监听器注册机制。数据类实现者仍然是负责通知时的性能变化。这是通过分配一个绑定注释getter和setter进行通知:
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); } }
注意:BR类自动生成的
好了,现在你就会发现当通过set方法改变数据后,UI就会自动更新!
ObservableFields private static class User { public final ObservableField<String> firstName = new ObservableField<>(); public final ObservableField<String> lastName = new ObservableField<>(); public final ObservableInt age = new ObservableInt(); }
在代码中设置数据:
user.firstName.set("Google"); int age = user.age.get(); Observable Collections ObservableArrayMap<String, Object> user = new ObservableArrayMap<>(); user.put("firstName", "Google"); user.put("lastName", "Inc."); user.put("age", 17);
布局中使用:
<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是Integer,可以使用ObservableArrayList代替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"/>
文章写到这里,DataBinding的一些基本用法已经介绍完毕,如果您想要了解更加详细、全面的内容,请自行去sdk的Doc文档中阅读或者关注我以后的博文。