Android DataBinding库(MVVM设计模式)
说到 DataBinding,就有必要先提起 MVVM设计模式。
Model–View–ViewModel(MVVM) 是一个软件架构设计模式,相比 MVVM,大家对 MVC 或 MVP 可能会更加熟悉。
- MVC:(VIew-Model-Controller)
早期将 View、Model、Controller 代码块进行划分,使得程序大部分分离,降低耦合。
- MVP:(VIew-Model-Presenter)
由于 MVC 中 View和Model之间的依赖太强,导致 Activity 中的代码过于臃肿。为了他们可以*对独立的存在,慢慢演化出了 MVP。在 MVP 中 View 并不直接使用 Model,它们之间的通信是通过 Presenter (MVC中的Controller) 来进行的。
- MVVM:(Model–View–ViewModel)
MVVM 可以算是 MVP的升级版,将 Presenter 改名为 ViewModel。关键在于 View和Model的双向绑定,当 View 有用户输入后,ViewModel 通知 Model 更新数据,同理 Model 数据更新后,ViewModel 通知 View 更新。
Data Binding
在Google I/O 2015上,伴随着 Android M 预览版发布了Data Binding兼容函数库:
https://developer.android.com/tools/data-binding/guide.html
不知道要扯什么了,还是直接上代码,来看看 Data Binding 的魅力吧。
环境要求
Data Binding 对使用的环境还是有一定要求的(这货有点挑):
- Android Studio 版本在 1.3以上
- Gradle 的版本要在 1.5.0-alpha1 以上
- 需要在 Android SDK manager 中下载 Android Support repository
然后在对应的 Module 的 build.gradle 中添加:
android { .... dataBinding { enabled =true } }
Gradle需要升级版本的可以参考:
升级Gradle版本
http://www.jianshu.com/p/00beddbe3dbc
创建对象
创建一个 User类:
布局
在 activity_main.xml 中布局:
这里跟平时的布局有点不同,*外层是 layout,里面分别是 data 以及 我们的布局。
data:声明了需要用到的 user对象,type 用于指定路径。
可以在 TextView 中的看到 android:text=”@{user.firstName}”, 这是什么鬼,没见过这么写的!!!(不急,继续往下看)
绑定数据
看看下面的 MainActivity:
问我 ActivityMainBinding 哪来的?我怎么知道…
ActivityMainBinding 是根据布局文件的名字生成的,在后面加了 Binding。
运行下看看效果吧:
有点懵逼了,就绑定了下而已,这些数据是怎么显示到界面上的。
他是怎么工作的?
原来 Data Binding 在程序代码正在编译的时候,找到所有它需要的信息。然后通过语法来解析这些表达式,*后生成一个类。
通过反编译我们可以看到,反编译可以参考这里:
http://blog.csdn.net/vipzjyno1/article/details/21039349
Data Binding 为我们生成了 databinding包,以及 ActivityMainBinding类:
看看我们在 onCreate 中*后调用的 binding.setUser(user),在 ActivityMainBinding 中可以看到这个方法:
我想就是这个 super.requestRebind() 对数据进行了绑定,至于里面怎么实现的,有待进一步研究。
更多用法
上面只是用一个简单的例子,展示了 Data Binding 的用法,如果想在实际项目中使用,可不是上面这例子可以搞定的。下面就来说说 Data Bindig 的更多用法。
消除空指针顾虑
自动生成的 DataBinding 代码会检查 null,避免出现NullPointerException。
例如在表达式中 @{user.phone}如果user == null 那么会为 user.phone 设置 默认值null 而不会导致程序崩溃(基本类型将赋予默认值如 int为0,引用类型都会赋值null)。
自定义DataBinding名
如果不喜欢自动生成的 Data Binding名,我们可以自己来定义:
<data class="MainBinding"> .... </data>
class对应 的就是生成的 Data Binding名。
导包
跟Java中的用法相似,布局文件中支持 import 的使用,原来的代码是这样:
<data> <variable name="user" type="com.example.gavin.databindingtest.User" /> </data>
使用 import 后可以写成这样:
<data> <import type="com.example.gavin.databindingtest.User"/> <variable name="user" type="User" /> </data>
遇到 相同的类名 的时候:
使用 alias 设置别名,这样 user 对应的就是 com.example.gavin.databindingtest.User,mcUser 就对应com.example.gavin.mc.User,然后:
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.firstName}"/>
当需要用到一些包时,在Java中可以自动导包,不过在布局文件中就没有这么方便了。需要使用 import 导入这些包,才能使用。如需要用到 View 的时候:
<data> <import type="android.view.View"/></data> ... <TextView ... android:visibility="@{user.isStudent ? View.VISIBLE : View.GONE}" />
注意:只要是在Java中需要导入包的类,这边都需要导入,如:Map、ArrayList 等,不过 java.lang 包里的类是可以不用导包的。
表达式
在布局中,不仅可以使用:
android:text="@{user.lastName}"
还可以使用表达式如:
三元运算:
在 User 中添加 boolean类型 的 isStudent属性,用来判断是否为学生:
注意:需要用到双引号的时候,外层的双引号改成单引号。
还可以这样用:
这里用到的 View 需要在 data 中声明:
<data> <import type="android.view.View"/> </data>
注意:android:visibility=”@{user.isStudent ? View.VISIBLE : View.GONE}”,可能会被标记成红色,不用管它编译会通过的。
??
除了常用的操作法,另外还提供了一个 null 的合并运算符号 ??,这是一个三目运算符的简便写法。
contact.lastName ?? contact.name
相当于:
contact.lastName != null ? contact.lastName : contact.name
所支持的操作符如下:
数学运算符 + – / * %
字符串拼接 +
逻辑运算 && ||
二进制运算 & | ^
一元运算符 + – ! ~
位运算符 >> >>> <<
比较运算符 == > < >= <=
instanceof
Grouping ()
文字 – character, String, numeric, null
类型转换 cast
方法调用 methods call
字段使用 field access
数组使用 [] Arrary access
三元运算符 ? :
显示图片
除了文字的设置,网络图片的显示也是我们常用的。来看看 Data Binding 是怎么实现图片的加载的。
首先要提到 BindingAdapter注解,这里创建了一个类,里面有显示图片的方法:
(这方法必须是public static的,否则会报错)
这里只用了 bind 声明了一个 image 自定义属性,等下在布局中会用到。
这个类中只有一个静态方法 imageLoader,里面有两参数,一个是需要设置图片的 view,另一个是对应的 Url,这里使用了 ImageLoader 库加载图片。
看看它的布局是什么样的吧:
*后在 MainActivity 中绑定下数据就可以了:
binding.setImageUrl("http://115.159.198.162:3000/posts/57355a92d9ca741017a28375/1467250338739.jpg");
哇靠!!!就这样?我都没看出来它是怎么设置这些图片的。
不管了,先看看效果。(其中的原理以后慢慢唠,这里就负责说明怎么使用,这篇已经够长了,不想再写了)
看个美女压压惊
使用 BindingAdapter 的时候,我这还出现了这样的提示,不过不影响运行。不知道你们会不会…
已解决
感谢 颜路 同学指出 @BindingAdapter({“bind:image”}) 改成 @BindingAdapter({“image”}) 就不会有警告了。
点击事件
在 MainActivity 中声明方法:
//参数View必须有,必须是public //参数View不能改成对应的控件,只能是View,否则编译不通过 public void onClick(View view) { Toast.makeText(this,"点击事件", Toast.LENGTH_LONG).show(); }
布局中:
*后记得在 MainActivity 中调用:
binding.setMainActivity(this);
发现:布局文件中,variable 中的 name,在 binding 中都会生成一个对应的 set方法,如:setMainActivity。有 set方法,那就应该有 get方法,试试 getMainActivity,还真有。
运行下看看效果:
当然如果你不想把点击事件写在 MainActivity 中,你把它单独写在一个类里面:
public class MyHandler { public void onClick(View view) { Toast.makeText(view.getContext(), "点击事件", Toast.LENGTH_LONG).show(); } }
在 MainActivity 调用:
binding.setHandle(new MyHandler());
调用Activity中变量
上面看到它调用 MainActivity中的onClick方法,那么可以调用 MainActivity 中的属性吗?
在 MainActivity 中定义 mName:
public static String mName = "MM";
布局中:
注意:这个变量必须是 public static。
数据改变时更新UI
当数据发生变化时,我们可以这样更新UI:
看看调用的这个 setUser 是什么:
从反编译的代码中可以看出,setUser 方法中重新绑定了数据。
看下效果:
BaseObservable
使用上面的代码实现了UI的更新你就满足了?其实官方为我们提供了更加简便的方式,使 User继承BaseObservable,代码如下:
只要 user 发生变化,就能达到改变UI的效果。在 MainActivity 中只要调用以下代码:
user.setFirstName("Com");
有了 BaseObservable 就够了?不不不,我比较懒,不想写那么多 @Bindable 和 notifyPropertyChanged。万一里面有几十个属性,那不写哭起来?而且还有可能写丢了。
Data Binding的开发者贴心得为我们准备了一系列的 ObservableField,包括: ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat,ObservableDouble 以及 ObservableParcelable (原文蓝字部分都是超链接,感兴趣的朋友可以通过原文查看,我这里就不贴出来了,下文若有蓝色字体视为同等情况)看看它们的用法。
ObservableField 的使用
1. 创建User2
这类里面 没有Get/Set。
2. 布局文件
<TextView ... android:text="@{user2.firstName}" /> <TextView ... android:text="@{user2.lastName}" /> <TextView ... android:text="@{String.valueOf(user2.age)}" />
3. MainActivity中:
mUser2 = new User2(); binding.setUser2(mUser2); mUser2.firstName.set("Mr"); mUser2.lastName.set("Bean"); mUser2.age.set(20); mUser2.isStudent.set(false);
这里 new 了一个 User2 对象后,直接就绑定了。之后只要 mUser2 中的数据发生变化,UI也会随之更新。
除了这几个 Map 跟 List 也是必不可少的,Data Binding为我们提供了ObservableArrayMap 和ObservableArrayList。
ObservableArrayMap 的使用
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>(); user.put("firstName", "Google"); user.put("lastName", "Inc."); user.put("age", 17);
ObservableArrayList 的使用
ObservableArrayList<Object> user = new ObservableArrayList<>(); user.add("Google"); user.add("Inc."); user.add(17);
在布局中使用到 ObservableBoolean 类型时,编译无法通过:
android:text='@{user2.isStudent?"学生":"非学生"}'
【目前已知】
将中文改成英文是可以通过编译的,像下面这样:
android:text='@{user2.isStudent?"Student":"Not Student"}'
为何使用中文不可以?原因未明。(感谢指教)
在RecyclerView或ListView中使用
前面说了那么多基础的用法,可还是不能达到我们的需求。几乎在每个app中都有列表的存在,RecyclerView 或 ListView,从上面所说的似乎还看不出 Data Binding 在 RecyclerView 或 ListView 中是否也能起作用。(用屁股想也知道,Google的开发团对怎么可能会犯这么低级的错误)。下面以 RecyclerView 为例子:
1. 直接看 Item 的布局(user_item.xml):
2. RecyclerView 的数据绑定是在 Adapter 中完成的,下面看看 Adapter,这里使用了一个 Adapter,如果你在使用的时候发现 RecyclerView 的动画没了,去这里寻找答案:
https://realm.io/cn/news/data-binding-android-boyar-mount/
3. *后在布局和 MainActivity 中的使用跟平时的用法一样。
布局中加入 RecyclerView:
<android.support.v7.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent"/>
MainActivity 中:
这样就可以了。
不过,在自动生成的 ActivityMainBinding 中,我们可以看到根据 RecyclerView的id,会自动生成一个 recyclerView。
所以在 MainActivity 中,我们可以不用 findViewById,直接使用 binding.recyclerView。
来看看效果吧:
Tips
tip1
若需要显示int类型,需要加上””,例如:
user.age 为 int类型,需要这样用:
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text='@{""+user.age}'/>
或者
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{String.valueOf(user.age)}"/>
tip2