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)}" |
不可以用的操作符
this
super
new
- 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 | "imageUrl", "placeholder"}, requireAll=false) (value={ |
提供新值,旧值参数
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好了。