MVVM一种android架构模式,谷歌官方架构中实现了,包括databinding,viewmodel,livedata,room,lifecycle,等一套用于mvvm架构的架构组件。
前言
整体学习MVVM之前,我们可以先来单独看看DataBinding是干嘛的
首先附上官方文档地址:Data Binding Library ,以及官方demo
官方是这么说的:
数据绑定库是一种支持库,借助该库,您可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。
就像下面这样:
1  | <TextView android:text="@{viewmodel.userName}" />  | 
诶,好像似曾相识,对了,和那个jsp的el表达式,以及spring中表达式有点像啊。这样就好理解了,xml UI通过某种数据绑定,可以通过这种声明式将数据传到UI中。不得不说有点亲切,哈哈。我下面的写东西也基本上是总结(翻译233)官方文档,所以结构也是一样的。
初步
API要求14以上,gradle1.5以上
构建环境
开启DataBinding
1  | android {  | 
AS对databinding的支持
- 语法高亮
 - 语法错误标记
 - xml代码完成
 - 引用定位至导航和文档
 
布局和绑定表达式
这里开始啦啊
包含DataBinding的layout
在原本基础上外层套上以layout标签,以及内部紧跟着声明data标签,在data标签使用variable里面就可声明绑定变量了。在xml中使用绑定数据通过@{user.firstName}的形式使用
1  | 
  | 
声明数据对象
使用pojo简单对象,或者老套路,get/set准备,然后@{user.firstName}会去调用getFirstName()或者firstName(),或者直接访问前提是public的firstName
1  | public class User {  | 
绑定数据
通过上述声明layout文件,开始构建,会生成以xml文件开头,尾缀为Binding的类,比如ActivityMainBinding.java,下面看下如何将对象绑定到layout文件中的
1  | 
  | 
这样一来数据就和layout绑定好了,layout就能通过表达是显示绑定数据
使用LayoutInflater获取binding
1  | ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());  | 
如果在Fragment,ListView,RecyclerView adapter中使用databinding,可以这样
1  | ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);  | 
表达式语言
通用表达
表达式语言和代码很相似,可以使用以下的操作符和关键字
- 算术符:
+ - / * % - 字符串连接符:
+ - 逻辑符
&& || - 位操作符:
& | ^ - 一元运算符:
+ - ! ~ - 移位符:
>> >>> << - 比较符:== > < >= <=
(Note that<needs to be escaped as<`) instanceof- 括号
() - 字面量: character, String, numeric, 
null - 类型转换
 - 方法调用
 - 属性访问
 - 数组访问 
[] - 三元符 
?: 
例如:
1  | android:text="@{String.valueOf(index + 1)}"  | 
不可以用的操作符
thissupernew- Explicit generic invocation (我理解位泛型调用行吗?)
 
空合并运算符
1  | android:text="@{user.displayName ?? user.lastName}"  | 
等价于
1  | android:text="@{user.displayName != null ? user.displayName : user.lastName}"  | 
属性访问
1  | android:text="@{user.lastName}"  | 
防止空指针异常
生成的绑定代码自动检查null以及防止空指针异常,比如@{user.name}中如果user为空,那么name会被分配为null,如果是@{user.age},则age分配为0
运用集合
自然是少不不了集合了的使用了,数组,list,sparse list ,map可以通过[]形式访问
1  | <data>  | 
注意:像List<String>这种带泛型需要写成List<String>,其它的依次类推
字符串字面量key
下面的两种形式也是可以的,通过字面量访问值,双引号,反引号
1  | android:text='@{map["firstName"]}'  | 
访问资源
结合表达式来访问资源
1  | android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"  | 
格式化字符串
1  | android:text="@{@string/nameFormat(firstName, lastName)}"  | 
默认值
1  | <TextView android:layout_width="wrap_content"  | 
事件处理
Data Binding允许通过表达式来处理分发的view事件,例如点击事件,需要在onclick属性上写表达式。有三种需要特殊处理
| Class | Listener setter | Attribute | 
|---|---|---|
SearchView | 
setOnSearchClickListener(View.OnClickListener) | 
android:onSearchClick | 
ZoomControls | 
setOnZoomInClickListener(View.OnClickListener) | 
android:onZoomIn | 
ZoomControls | 
setOnZoomOutClickListener(View.OnClickListener) | 
android:onZoomOut | 
有两种方式来处理事件:
- 方法引用:通过调用具有相同签名的方法
 - 监听器绑定: 通过lambda方式书写对象
 
方法引用
类似于书写onClick属性关联到activity的同签名方法,不过这个方法引用会在编译时处理,即如果出现签名不一致等错误,会在编译时报错。签名必须与触发事件一致
声明处理器
1  | public class MyHandlers {  | 
调用,:: .两种都行,最好别用吧,因为被废弃了。
1  | 
  | 
监听器绑定
实际处理器
1  | public class Presenter {  | 
通过@{() -> presenter.onSaveClick(task)}类似lambda表示式
1  | 
  | 
监听器的实现是数据绑定时被创键的,和方法引用类似,但是监听器绑定的处理方法不要求签名完全一致,只需要保证返回值一致就行了
当然你也可以使用view参数
1  | android:onClick="@{(view) -> presenter.onSaveClick(task)}"  | 
也可以继续传入到处理方法中
1  | public class Presenter {  | 
当然某些事件不止一个参数
1  | public class Presenter {  | 
1  | <CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content"  | 
结合三元表达式使用默认返回值,null,0等
1  | android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"  | 
使用过程要保证监听器简洁,不宜过度复杂。
import 和 variable,includes
import
和java文件中的导包一样。
1  | <data>  | 
使用别名,类名冲突时使用别名
1  | <import type="android.view.View"/>  | 
反正和java中怎么导入怎么用基本一样,包括了静态方法,嗯,真香
1  | <data>  | 
variable
声明变量,这些变量都是会在编译时进行类型检查的。binding会为每个变量分配默认值,参考java类属性默认值。
有一个特殊的变量context,即view.getContext
include
传递变量到其它布局
1  | 
  | 
不支持直接的merge子节点
1  | 
  | 
使用可观察对象
databinding库允许数据是可观察的。在可观察性下,数据发生改变会自动更新与之绑定的UI数据。当然后续会使用自带生命周期感知的LiveDta来替换Obervable增强功能
Observable fields
当你不需要观察整体时,考虑使用可观察字段,有以下的选项
1  | ObservableBoolean  | 
声明为final
1  | private static class User {  | 
通过get,set访问
1  | user.firstName.set("Google");  | 
Observable objects
整体声明为可观察对象,通过继承BaseObservable,通过在set方法中调用notifyPropertyChanged();来通知UI变化,对了get方法需要使用@Bindle来注解,不然无法从BR找到字段资源ID,这个ID是和UI关联的,通过ID定位到UI数据
1  | private static class User extends BaseObservable {  | 
生成绑定类
可以定制化的生成banding类
创键banding类
通过inflate注入
1  | 
  | 
添加到父viewgroup
1  | MyLayoutBinding binding = MyLayoutBinding.inflate(getLayoutInflater(), viewGroup, false);  | 
绑定view
1  | MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);  | 
不知类型的情况下绑定view
1  | View viewRoot = LayoutInflater.from(this).inflate(layoutId, parent, attachToParent);  | 
在fragment,listview,recyclerview adpter,优先使用inflate
1  | ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);  | 
setContentView
1  | ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);  | 
通过id定位view
以前没有databinding时总是需要手动findViewById,或者通过ButterKnife来辅助,现在有了DataBinding后可以直接通过xxxBinding.id的形式来获得view对象(不过用kotlin来写话的天然直接通过id引用对象233)
1  | <Button  | 
会将下划线式的命名规则转变为小驼峰式
1  | binding.btnToViewmodelActivity.setOnclcikListener();  | 
立即执行绑定
原本在可观察数据变化时,会在下一帧进行ui变化,如果你想强制立即执行调用 executePendingBindings()
动态变量
例如在RecyclerView.Adapter中你可能不知道Banding是那个。通过以下方式通知修改数据
1  | public void onBindViewHolder(BindingHolder holder, int position) {  | 
多线程问题
在databinding中,修改数据可以在任何线程,数据在UI的上的改变会自动在本地化的UI线程进行更新。需要注意的是不能在后台线程操纵集合数据
自定义binding类的名字
1  | <data class="ContactItem">  | 
绑定适配器
绑定适配负责通过适当的方式来调用并且设置值,比如我们在设置属性的时候实际是通过setText来完成的,设置监听器是通过setOnclickListener来完成的。databing库允许我们指定方法调用来设置值,提供绑定逻辑,指定返回类型。
设置属性值
自动选择方法
通过名称和返回值类型来匹配对应的方法调用。比如app:scrimColor=@{@color/scrim}会调用setScrimColor(int)诸如此类,自动选择方法调用
指定属性触发方法
1  | ({  | 
1  | (  | 
在这样的声明下,通过给app:srcCompat设置属性值,会触发调用setImageResource方法
自定义触发方法逻辑
设置android:paddingLeft就会调用自定义的setPaddingLeft
1  | ("android:paddingLeft")  | 
同样支持自定义属性,和多属性
1  | @BindingAdapter({"imageUrl", "error"})  | 
1  | <ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />  | 
databing 库会忽略自定义属性的命名空间,所以不写也没事
可以设置不要求所有属性有值
1  | (value={"imageUrl", "placeholder"}, requireAll=false)  | 
提供新值,旧值参数
1  | ("android:paddingLeft")  | 
甚至新旧监听器参数
1  | ("android:onLayoutChange")  | 
然后使用监听器绑定
1  | <View android:onLayoutChange="@{() -> handler.layoutChanged()}"/>  | 
对于一个监听器中有多个监听方法的需要拆分出来
1  | (VERSION_CODES.HONEYCOMB_MR1)  | 
对象转换
一般情况下会有自动转换,比如android:text="@{String}",string就会转换为CharSequence以供setText使用
我们也可以使用自定义对象转换比如:
1  | <View  | 
我们的参数值是一个int,但是最后起作用的是Drawable,所以我们需要将int转换为ColorDrawable
1  | 
  | 
这样就会执行转换
结合架构组件
使用LiveData
和ObservableField相似,LiveData数据也是可观察的,但是LiveData更加优秀的是感知生命周期,即在某些不应该触发通知的生命周期中是不会触发通知的。要求AS版本3.1以上,详情见之后的LiveData介绍
使用LiveData需要设定生命周期的承载者来保持LiveData作用
1  | class ViewModelActivity extends AppCompatActivity {  | 
然后再数据类中使用
1  | class ScheduleViewModel extends ViewModel {  | 
使用ViewModel管理UI关联数据
viewModel用于管控UI行为,以及存储绑定数据,在某些屏幕旋转情况下,activity会重建,而viewmodel可以一直存在,保存于UI的关联的重量级数据,同时作为逻辑沟通的桥梁,连接UI,连接Model。具体见后续的ViewModel
ViewModel实现Obervable
在某种不需要使用LiveData进行生命周期感知的情况下,可以进行实现Observable,使得ViewModel具有被观察的特性。
首先继承ViewModel,实现Observable,添加属性监听。
重写addOnPropertyChangedCallback removeOnPropertyChangedCallback,可以参看BaseObservable的基本实现范例
这样一个ViewModel就具有观察特性了
1  | /**  | 
双向绑定
基础
终于来到大名鼎鼎的双向绑定了。之前我们的普通操作都是修改绑定数据,触发UI变化。
单向绑定过程:data set-> data notify -> UI get->UI change
双向绑定过程:UI set -> data set -> data notify -> UI get -> UI change
简单来讲就是以前是通过直接修改绑定数据来影响UI值,现在双向绑定修改UI值来触发绑定数据改变。
单向绑定
1  | <CheckBox  | 
通过reemberMeChanged来触发改变事件,继而自行书写修改绑定数据逻辑
现在双向绑定
1  | <CheckBox  | 
使用@={viewmodel.rememberMe}来表示双向绑定,意思是在checkbox UI属性变化时会去调用setRememberMe方法,UI数据和绑定数据互相可以影响,但是需要通过一些判断来防止进入无限的循环调用
1  | public class LoginViewModel extends BaseObservable {  | 
用于自定义属性
自定义属性也可以进行双向绑定
使用@BindingAdapter绑定set方法
1  | ("time")  | 
使用@InverseBindingAdapter绑定get方法
1  | ("time")  | 
设置同用的监听
1  | ("app:timeAttrChanged")  | 
转换
在双向绑定中的转换就会涉及到反向转换,比如原来我们是date to string,现在反向转换就是string to date了
1  | public class Converter {  | 
xml中
1  | <EditText  | 
避免无限循环
在双向绑定中因为涉及两个值循环变化,需要通过比较值来,避免无限循环的调用
databing 提供的双向绑定属性
这个就是说它内部实现一些双向绑定的Adapter,就是不用我们自己写,当然自定义的属性双向绑定还是得自己写咯
我就直接从官网拷啦
| Class | Attribute(s) | Binding adapter | 
|---|---|---|
AdapterView | 
android:selectedItemPosition android:selection | 
AdapterViewBindingAdapter | 
CalendarView | 
android:date | 
CalendarViewBindingAdapter | 
CompoundButton | 
android:checked | 
CompoundButtonBindingAdapter | 
DatePicker | 
android:year android:month android:day | 
DatePickerBindingAdapter | 
NumberPicker | 
android:value | 
NumberPickerBindingAdapter | 
RadioButton | 
android:checkedButton | 
RadioGroupBindingAdapter | 
RatingBar | 
android:rating | 
RatingBarBindingAdapter | 
SeekBar | 
android:progress | 
SeekBarBindingAdapter | 
TabHost | 
android:currentTab | 
TabHostBindingAdapter | 
TextView | 
android:text | 
TextViewBindingAdapter | 
TimePicker | 
android:hour android:minute | 
TimePickerBindingAdapter | 
这些都是已经帮你写好得双向绑定属性
结语
总结下来就是,可以将数据绑定到UI页面的操作。方便了书写,不用写过多的样版代码,同时也解耦了,耦合性高的都被APT自动生成了,不用我们管。反正写的时候我还是遇见了各种问题,尤其是用Kotlin写的时候,一样的代码都跑不通,kotlin还是得好好琢磨琢磨啊。这个文章基本算是简单得翻译了官方文档吧,有时间还是写个demo好了。