image.png
1 MVVM总览
本文包含Android
中MVVM
体系中的很多部分,主要对ViewModel
DataBinding
RxJava
LiveData
Lifecycle
等笔者所使用的技术体系进行解析.
本文字数较多,内容较为完整并且后续还会追加更新,阅读本篇文章需要较长时间,建议读者分段阅读.
所有文字均为个人学习总结和理解,仅供参考,如有纰漏还请指出,笔者不胜感激.
1.1 配置环境
- 笔者的
Android Studio
版本=3.2
-
Jetpack
最低兼容到Android
=2.1
,API
=7
1.2 为什么要选择MVVM?
为什么要选择MVVM?要回答这个问题首先就要介绍MVC
与MVP
这两种模式,从MVC
到MVVM
其实大家想的都是怎么把Model
和View
尽可能的拆开(熟悉三者定义的朋友可以跳过该节).
1.2.1 MVC
MVC
(Model
-View
-Controller
)即传统Android
开发中最常用的模式:
- 通常使用
Activity
/Fragment
作为Controller
层, - 以
android.view.View
的子类以xml
构建文件构建起的布局
作为View
层 - 以
SQLite
数据库,网络请求作为Model
层.
但由于Activity
/Fragment
的功能过于强大
并且实际上包含了部分View
层功能,导致最后Activity
/Fragment
既承担了View
的责任,又承担了Controller
的责任.所以一般较复杂的页面,Activity
/Fragment
很容易堆积代码,最终导致Controller
混杂了View
层和业务逻辑(也就是你们所知道的一个Activity
三千行)
在MVC
中View
层与Model
几乎几乎完全没有隔离,View
层可以直接操作Model
层,Model
层的回调
里也可能会直接给View
赋值.Controller
的概念被弱化,最后只剩下MV
没有C
了.
这也将导致但你想把某个界面上的元素进行更新时,他会牵扯到一堆跟Model
层相关的代码,这个问题在你变更Model
层的时候同样也会出现,这个问题其实是没有很好的将逻辑分层导致的.
1.2.2 MVP
MVP
(Model
-View
-Presenter
)架构设计,是当下最流行的开发模式,目前主要以Google
推出的TodoMVP
为主,MVP
不是一种框架,它实际上更类似一种分层思想
,一种接口约定
,具体体现在下面:
- 定义
IView
接口,并且在接口中约定View
层的各种操作,使用android.view.View
的子类以xml
构建文件构建起的布局
和Activity
/Fragment
作为布局控制器,实现IView
这个View
层的接口,View
层的实际实现类保留一个IPresenter
接口的实例. - 定义
IPresenter
接口,并且在接口中约定Presenter
层的各种操作.可以使用一个与View
无关的类实现它,一般是XxxPresenterImpl
.通常情况下Presenter
层会包含Model
层的引用和一个IView
接口的引用,但不应该直接或者间接引用View
层android.view.View
的子类,甚至是操作的参数中也最好不要有android.view.View
的子类传进来,因为它应该只负责业务逻辑和数据的处理并通过统一的接口IView
传递到View
层. - 不需要为
Model
层定义一个IModel
的接口,这一层是改造最小的.以前该怎么来现在也差不多该怎么来.但是现在Presenter
把它和View
隔开了,Presenter
就可以作为一段独立的逻辑被复用.
MVP
模式解决了MVC
中存在的分层问题,Presenter
层被突出强调,实际上也就是真正意义上实现了的MVC
但是MVP
中其实仍然存在一些问题,比如当业务逻辑变得复杂以后,IPresenter
和IView
层的操作数量可能将会成对的爆炸式增长,新增一个业务逻辑,可能要在两边增加数个通信接口,这种感觉很蠢.
并且,我们要知道一个Presenter
是要带一个IView
的,当一个Presenter
需要被复用时,对应的View
就要去实现所有这些操作,但往往一些操作不是必须实现的,这样会留下一堆TODO
,很难看.
1.2.3 MVVM
MVVM
(Model
-View
-ViewModel
)由MVP
模式演变而来,它由View
层,DataBinding
,ViewModel
层,Model
层构成,是MVP
的升级版并由Google
的Jetpack
工具包提供框架支持:
-
View
层包含布局,以及布局生命周期控制器(Activity
/Fragment
) -
DataBinding
用来实现View
层与ViewModel
数据的双向绑定(但实际上在Android Jetpack
中DataBinding
只存在于布局和布局生命周期控制器之间,当数据变化绑定到布局生命周期控制器时再转发给ViewModel
,布局控制器可以持有DataBinding
但ViewModel
不应该持有DataBinding
) -
ViewModel
与Presenter
大致相同,都是负责处理数据和实现业务逻辑,但是ViewModel
层不应该直接或者间接地持有View
层的任何引用,因为一个ViewModel
不应该直达自己具体是和哪一个View
进行交互的.ViewModel
主要的工作就是将Model
提供来的数据直接翻译成View
层能够直接使用的数据,并将这些数据暴露出去,同时ViewModel
也可以发布事件,供View
层订阅. -
Model
层与MVP
中一致.
MVVM
的核心思想是观察者模式,它通过事件
和转移View
层数据持有权
来实现View
层与ViewModel
层的解耦.
在MVVM
中View
不是数据的实际持有者,它只负责数据如何呈现以及点击事件的传递,不做的数据处理工作,而数据的处理者和持有者变成ViewModel
,它通过接收View
层传递过来的时间改变自身状态,发出事件或者改变自己持有的数据触发View
的更新.
MVVM
解决了MVP
中的存在的一些问题,比如它无需定义接口,ViewModel
与View
层彻底无关更好复用,并且有Google
的Android Jetpack
作为强力后援.
但是MVVM
也有自己的缺点,那就是使用MVVM
的情况下ViewModel
与View
层的通信变得更加困难了,所以在一些极其简单
的页面中请酌情
使用,否则就会有一种脱裤子放屁的感觉,在使用MVP
这个道理也依然适用.
2 DataBinding
2.1 坑
要用一个框架那么就要先说它的坑
点.那就是不建议在使用DataBinding
的模块同时使用apply plugin: 'kotlin-kapt'
.
因为现在kapt
还有很多Bug
,使用kapt
时,在Windows
下DataBinding
格式下的xml
中如果包含有中文,会报UTF-8
相关的错误.
笔者一开始猜想这是由于JVM
启动参数没有设置成-Dfile.encoding=UTF-8
导致的,在gradle.properties
中改过了,无果,Stack Overflow
搜过了,没找到,如果有大佬知道怎么解决,还请指点一二
如果你在模块中同时使用kotlin
和DataBinding
是可以的,但是请一定不要使用kapt
,除非JB
那帮大佬搞定这些奇怪的问题.
这就意味这你所有的kotlin
代码都不能依赖注解处理器来为你的代码提供附加功能,但是你可以把这些代码换成等价的Java
实现,它们可以工作得很好.
2.2 DataBinding的兼容性
先说一点,DataBinding
风格的xml
会有"奇怪"的东西入侵Android
原生的xml
格式,这种格式LayoutInfalter
是无法理解,但是,当你对这些奇怪的xml
使用LayoutInfalter#inflate
时亦不会报错,并且布局也正常加载了,这是为什么呢?
这是因为在打包时,Gradle
通过APT
把你的DataBinding
风格的xml
全部翻译了一遍,让LayoutInfalter
能读懂他们,正是因为这个兼容的实现,而使得我们可以在使用和不使用DataBinding
间自由的切换.
2.3 DataBinding风格的XML
要想使用DataBinding
,先在模块的build.gradle
中添加
android{
//省略...
dataBinding {
enabled = true
}
}
来启用DataBinding
支持.
DataBinding
不需要额外的类库支持,它被附加在你的android
插件中,它的版本号与你的android
插件版本一致.
classpath 'com.android.tools.build:gradle:3.3.2'
在DataBinding
风格的xml
中,最外层必须是layout
标签,并且不支持merge
标签,编写xml
就像下面这样
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="text"
type="String"/>
<variable
name="action"
type="android.view.View.OnClickListener"/>
</data>
<TextView
android:onClick="@{action}"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<layout/>
2.3.1 变量领域
data
标签包裹的是变量领域,在这里你可以使用variable
定义这个布局所要绑定的变量类型,使用name
来指定变量名,然后用type
来指定其类型.
如果一些类型比较长,而且由需要经常使用你可以像Java
一样使用import
导入他们(java.lang.*
会被默认导入),然后就不用写出完全限定名了,就像这样
<import
type="android.view.View"
alias="Action"/>
<variable
name="action"
type="Action"/>
有必要时(比如名字冲突),你还可以用Action
为一个类型指定一个别名,这样你就能在下文中使用这个别名.
2.3.2 转义字符
熟悉xml
的同学可能都知道<
和>
在xml
中是非法字符,那么要使用泛型的时候,我们就需要使用xml
中的转义字符<
和>
来进行转义
//↓错误,编译时会报错×
<variable
name="list"
type="java.util.List<String>"/>
//↓正确,可以通过编译√
<variable
name="list"
type="java.util.List<String>"/>
data
标签结束后就是原本的布局编写的位置了,这部分基本和以前差不多,只是加入了DataBinding
表达式
<data>
//......
<data/>
<TextView
android:onClick="@{action}"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
2.3.3 DataBinding表达式
以@{}
包裹的位置被称为DataBinding
表达式,DataBinding
表达式几乎支持Java
所有的运算符,并且增加了一些额外的操作,这允许我们在xml
中有一定的Java
编程体验,学过Java web
的同学可能会觉得它很像JSP
:
- 不需要
xml
转义的二元运算-
,/
,*
,%
,||
,|
,^
,==
- 需要
xml
转义的二元运算&&
,>>
>>>
,<<
,>
,<
,>=
,<=
,与泛型一样运算符>=
,>
,<
,<=
等,也是需要转义的,&
需要用&
转义,这确实有些蹩脚,但这是xml
的局限性,我们无法避免,所以在DataBinding
风格的xml
中应该尽可能的少用这些符号. -
lambda
表达式@{()->persenter.doSomething()}
- 三元运算
?:
-
null
合并运算符??
,若左边不为空则选择左边,否则选择右边
android:text="@{nullableString??`This a string`}"
- 自动导入的
context
变量,你可以在xml
中的任意表达式使用context
这个变量,该Context
是从该布局的根View
的getContext
获取的,如果你设置了自己的context
变量,那么将会覆盖掉它 - 若表达式中有字符串文本
xml
需要特殊处理
用单引号包围外围,表达式使用双引号
android:text='@{"This a string"}'
或者使用`包围字符串,对,就Esc下面那个键的符号
android:text="@{`This a string`}"
- 判断类型
instanceof
- 括号
()
- 空值
null
- 方法调用,字段访问,以及
Getter
和Setter
的简写,比如User#getName
和User#setName
现在都可以直接写成@{user.name}
,这种表达式也是最简单的表达式,属于直接赋值表达式 - 默认值
default
,在xml
中
`android:text="@{file.name, default=`no name`}"`
- 下标
[]
,不只是数组,List
,SparseArray
,Map
现在都可以使用该运算符 - 使用
@
读取资源文件,如下,但是不支持读取mipmap
下的文件
android:text="@{@string/text}"
//或者把它作为表达式的一部分
android:padding="@{large? @dimen/large : @dimen/small}"
有一些资源需要显示引用
类型 | 正常情况 | DataBinding表达式引用 |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
ColorStateList | @animator | @stateListAnimator |
StateListAnimator | @color | @colorStateList |
还有一些操作是DataBinding
表达式中没有的,我们无法使用它们:
- 没有
this
- 没有
super
- 不能创建对象
new
- 不能使用泛型方法的显示调用
Collections.<String>emptyList()
编写简单的DataBinding
表达式,就像下面这样
<data>
<improt type="android.view.View"/>
<variable
name="isShow"
type="Boolean"/>
<data/>
<TextView
android:visibility="@{isShow?View.VISIBLE:View.GONE}"
android:text="@{@string/text}"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
应该避免出现较为复杂的DataBinding
表达式,以全部都是直接赋值表达式为佳,数据的处理应该交给布局控制器或者ViewModel
来做,布局应该只负责渲染数据.
2.3.4 使用在Java中生成的ViewDataBinding
使用DataBinding
后Android Studio
会为每个xml
布局生成一个继承自ViewDataBinding
的子类型,来帮助我们将xml
文件中定义的绑定关系映射到Java
中.
比如,如果你有一个R.layout.fragment_main
的布局文件,那么他就会为你在当前包下生成一个,FragmentMainBinding
的ViewDataBinding
.
在Java
实化DataBinding
风格xml
布局与传统方式有所不同.
- 在
Actvity
中
private ActivityHostBinding mBinding;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_host);
}
- 在自定义
View
和Fragment
中
private FragmentMainBinding mBinding;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
mBinding = DataBindingUtil.inflate(inflater,
R.layout.fragment_main,
container,
false);
return mBinding.getRoot();
}
- 在已经使用普通
LayoutInfalter
实例化的View
上(xml
必须是DataBinding
风格的,普通LayoutInflater
实例化布局时不会触发任何绑定机制,DataBindingUtil#bind
才会发生绑定)
View view = LayoutInflater.from(context).inflate(R.layout.item_view,null,false);
ItemViewBinding binding = DataBindingUtil.bind(view);
你在xml
设置的变量他会在这个类中为你生成对应的Getter
和Setter
.你可以调用它们给界面赋值,比如之前的我们定义的action
.
//这里的代码是Java8的lambda
mBinding.setAction(v->{
//TODO
})
2.3.5 使用BR文件
它还会为你生成一个类似R
的BR
文件,里面包含了你在DataBinding
风格xml
中定义的所有变量名的引用(由于使用的是APT
生成,有时候需要Rebuild Project
才能刷新),比如我们之前的action
,它会为我们生成BR.action
,我们可以这么使用它
mBinding.setVariable(BR.action,new View.OnClickListener(){
@Override
void onClick(View v){
//TODO
}
})
2.3.6 传递复杂对象
在之前给xml
中的变量中赋值时,我们用的都是一些类似String
的简单对象,其实我们也可以定义一些复杂的对象,一次性传递到xml
布局中
//java
public class File
{
public File(String name,
String size,
String path)
{
this.name = name;
this.size = size;
this.path = path;
}
public final String name;
public final String size;
public final String path;
}
//xml
<data>
<variable
name="file"
type="org.kexie.android.sample.bean.File"/>
<data/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<TextView
android:text="@{file.name}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:text="@{file.size}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:text="@{file.path}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<LinearLayout/>
个人认为绑定到xml
中的数据最好是不可变的,所以上面的字段中我使用了final
,但这不是必须的,根据你自己的需求来进行定制
2.3.7 绑定并非立即发生
这里有一点值得注意的是,你给ViewDataBinding
的赋值并不是马上生效的,而是在当前方法执行完毕回到事件循环后,并保证在下一帧渲染之前得到执行,如果需要立即执行,请调用ViewDataBinding#executePendingBindings
2.3.8 使用android:id
如果你使用了android:id
,那么这个View
就也可以当成一个变量在下文的DataBinding
表达式中使用,就像写Java
.它还会帮你View
绑定到ViewDataBinding
中,你可以这么使用它们
//xml
<TextView
android:id="@ id/my_text"
android:layout_width="match_parent"
android:layout_height="wrap_context"/>
<TextView
android:id="@ id/my_text2"
android:text="@{my_text.getText()}"
android:layout_width="match_parent"
android:layout_height="wrap_context"/>
//在java中my_text被去掉下划线,更符合java的命名习惯
mBinding.myText.setText("This is a new text");
用过ButterKnife的同学可能都知道,ButterKnife
出过一次与gradle
版本不兼容的事故,但是DataBinding
是与gradle
打包在一起发布的,一般不会出现这种问题,如果你不想用ButterKnife
但有不想让DataBinding
的风格的写法入侵你的xml
太狠的话,只使用android:id
将会是一个不错的选择.
2.4 正向绑定
某些第三方View
是肯定没有适配DataBinding
的,业界虽然一直说MVVM
好,但现在MVP
的开发方式毕竟还是主流,虽然这种情况我们可以用android:id
,然后在Activity
/Fragment
中解决,但有时候我们想直接在xml
中配置,以消除一些样板代码,这时候就需要自定义正向绑定.
2.4.1 自定义正向绑定适配器
我们可以使用@BindingAdapter
自定义在xml
中可使用的View
属性,名字空间是不需要的,加了反而还会给你警告.
@Target(ElementType.METHOD)
public @interface BindingAdapter {
/**
* 与此绑定适配器关联的属性。
*/
String[] value();
/**
* 是否必须为每个属性分配绑定表达式,或者是否可以不分配某些属性。
* 如果为false,则当至少一个关联属性具有绑定表达式时,将调用BindingaAapter。
*/
boolean requireAll() default true;
}
//@BindingAdapter需要一个静态方法,该方法的第一个参数是与该适配器兼容的View类型
//从第二个参数开始,依次是你自定义的属性传进来的值.
//使用requireAll来指定这些属性是全部需要,还是只要一个就可以
//如果requireAll = false,触发适配器绑定时,没有被设置的属性将获得该类型的默认值
//框架优先使用自定义的适配器处理绑定
@BindingAdapter(value = {"load_async", "error_handler"},requireAll = true)
public static void loadImage(ImageView view, String url, String error) {
Glide.with(view)
.load(url)
.error(Glide.with(view).load(error))
.into(view);
}
//在xml中使用它(下面那两个网址都不是实际存在的)
<ImageView
load_async="@{`http://android.kexie.org/image.png`}"
error_handler="@{`http://android.kexie.org/error.png`}"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
2.4.2 第三方View适配
DataBinding
风格的xml
还能在一定程度上适配第三方View
//如果你的自定义View中有这么一个Setter↓
public class RoundCornerImageView extends AppCompatImageView{
//......
public void setRadiusDp(float dp){
//TODO
}
}
//那么你可以在xml中使用radiusDp来使用它
<org.kexie.android.ftper.widget.RoundCornerImageView
radiusDp="@{100}"
android:id="@ id/progress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:scaleType="centerCrop"
android:src="@drawable/progress"/>
//它会自己为你去找名称为setRadiusDp并且能接受100为参数的方法.
2.4.3 xml中的属性重定向
使用@BindingMethod
来将xml
属性重定向:
@Target(ElementType.ANNOTATION_TYPE)
public @interface BindingMethod {
//需要重定向的View类型
Class type();
//需要重定向的属性名
String attribute();
//需要重定向到的方法名
String method();
}
//这是DataBinding源码中,DataBinding对于系统自带的TextView编写的适配器
//这是androidx.databinding.adapters.TextViewBindingAdapter的源码
@BindingMethods({
@BindingMethod(type = TextView.class, attribute = "android:autoLink", method = "setAutoLinkMask"),
@BindingMethod(type = TextView.class, attribute = "android:drawablePadding", method = "setCompoundDrawablePadding"),
@BindingMethod(type = TextView.class, attribute = "android:editorExtras", method = "setInputExtras"),
//......
})
public class TextViewBindingAdapter {
//......
}
//这样就可以建立起xml中属性与View中Setter的联系
2.4.4 添加转换层
使用@BindingConversion
为添加转换层
@BindingConversion
public static ColorDrawable toDrawable(int color) {
return new ColorDrawable(color);
}
//可以把color整形转换为android:src可接受的ColorDrawable类型
//但是转换只适用于直接的赋值
//如果你写了复杂的表达式,比如使用了?:这种三元运算符
//那就照顾不到你了
2.5 反向绑定
有正向绑定就一定有反向绑定,正向绑定和反向绑定一起构成了双向绑定.
在我们之前编写的DataBinding
表达式中,比如TextView
中android:text
之类的属性我们都是直接赋值一个String
过去的,这就是正向绑定,我们给View
的值能够直接反应到View
上,而反向绑定就是View
值的变化和也能反应给我们.
2.5.1 使用双向绑定
所有使用之前所有使用@{}
包裹的都是正向绑定,而双向绑定是@={}
,并且只支持变量,字段,Setter
(比如User#setName
,就写@={user.name}
)的直接编写并且不支持复杂表达式
2.5.2 兼容LiveData与ObservableField
实际上,android:text
不只能接受String
,当使用双向绑定时,它也能接受MutableLiveData<String>
和ObservableField<String>
作为赋值对象,这种赋值会将TextView
的android:text
的变化绑定到LiveData(实际上是MutableLiveData)
或者是ObservableField
上,以便我们在View
的控制层(Activity
/Fragment
)更好地观察他们的变化.
当然除了ObservableField
在androidx.databinding
包下还有不装箱的ObservableInt
,ObservableFloat
等等.
但是为了支持LiveData
我们必须开启第二版的DataBinding APT
.
在你的gradle.properties
添加
android.databinding.enableV2=true
现在我们可以通过LiveData(实际上是MutableLiveData)
将android:text
的变化绑定到Activity
/Fragment
//xml
<data>
<variable
name="liveText"
type="MutableLiveData<String>">
<data/>
<TextView
android:text="@={text}"
android:layout_width="match_parent"
android:layout_height="wrap_context"/>
//然后在Activity/Fragment中
MutableLiveData<String> liveText = new MutableLiveData<String>();
mBinding.setLiveText(liveText);
liveText.observe(this,text->{
//TODO 观察View层变化
});
2.5.3 自定义反向绑定适配器
下面我们回到androidx.databinding.adapters.TextViewBindingAdapter
的源码,继续对自定义反向绑定适配器进行分析.
//我们可以看到源码中使用了@InverseBindingAdapter自定义了一个反向绑定器
//指定了其属性以及相关联的事件
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
public static String getTextString(TextView view) {
return view.getText().toString();
}
//并为这个事件添加了一个可接受InverseBindingListener的属性
//为了说明方便,下面的代码已简化,源码并非如此,但主要逻辑相同
@BindingAdapter(value = {"android:textAttrChanged"})
public static void setTextWatcher(TextView view , InverseBindingListener textAttrChanged){
view.addTextChangedListener(new TextWatcher(){
//......
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
textAttrChanged.onChange();
}
});
}
//至此android:text的反向绑定完成
//当你使用@={}时实际上是用android:textAttrChanged属性向TextView设置了TextWatcher
//传入的InverseBindingListener是反向绑定监听器
//当调用InverseBindingListener的onChange时
//会调用@BindingAdapter所注解的方法将获得数据并写回到变量中.
2.6 配合DataBinding打造通用RecyclerView.Adapter
下面进行一个小小的实战吧,我们可以站在巨人的肩膀上造轮子.
代码语言:javascript复制 //导入万能适配器作为基类,可以大大丰富我们通用适配器的功能
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.46'
由于基类很强大所以代码不多:
代码语言:javascript复制//X是泛型,可以是你在item中所使用的java bean
public class GenericQuickAdapter<X>
extends BaseQuickAdapter<X, GenericQuickAdapter.GenericViewHolder> {
//BR中的变量名
protected final int mName;
//layoutResId是DataBinding风格的xml
public GenericQuickAdapter(int layoutResId, int name) {
super(layoutResId);
mName = name;
openLoadAnimation();
}
@Override
protected void convert(GenericViewHolder helper, X item) {
//触发DataBinding
helper.getBinding().setVariable(mName, item);
}
public static class GenericViewHolder extends BaseViewHolder {
private ViewDataBinding mBinding;
public GenericViewHolder(View view) {
super(view);
//绑定View获得ViewDataBinding
mBinding = DataBindingUtil.bind(view);
}
@SuppressWarnings("unchecked")
public <T extends ViewDataBinding> T getBinding() {
return (T) mBinding;
}
}
}
//实例化
GenericQuickAdapter<File> adapter = new GenericQuickAdapter<>(R.layout.item_file,BR.file);
//在xml中使用起来就像这样
<layout>
<data>
<variable
name="file"
type="org.kexie.android.sample.bean.File"/>
<data/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<TextView
android:text="@{file.name}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:text="@{file.size}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:text="@{file.path}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<LinearLayout/>
<layout/>
3 Lifecycle
在Android
中,组件的管理组件的生命周期一直是一个比较麻烦的东西,而自Google
推出Android Jetpack
组件包以来,这个问题得到的比较妥善的解决,Lifecycle
组件后来也成为Android Jetpack
的核心。
3.1 导入
以AndroidX
为例,要使用Lifecycle
组件,先在模块的build.gradle
文件中添加依赖:
api 'androidx.lifecycle:lifecycle-extensions:2.1.0-alpha02'
由于Lifecycle
组件由多个包构成,使用api
导入时即可将其依赖的包全部导入该模块,包括common
,livedata
,process
,runtime
,viewmodel
,service
等。
如果要使用Lifecycle
中的注解,你还需要添加如下注解处理器,以便在编译时,完成对相应注解的处理。
annotationProcessor 'androidx.lifecycle:lifecycle-compiler:2.0.0'
对于一个App
来说,使用Lifecycle
组件是没有任何侵入性的,因为他已经天然的融合到Google
的appcompat
库中了,而如今无论是什么应用程序都几乎离不开appcompat
,可以说集成Lifecycle
只是启用了之前没用过的功能罢了。
3.2 LifecycleOwner
LifecycleOwner
是Lifecycle
组件包中的一个接口,所有需要管理生命周期的类型都必须实现这个接口。
public interface LifecycleOwner
{
/**
* Returns the Lifecycle of the provider.
*
* @return The lifecycle of the provider.
*/
@NonNull
Lifecycle getLifecycle();
}
但其实很多时候我们根本无需关心LifecycleOwner
的存在。在Android
中, Fragment
、Activity
、Service
都是具有生命周期的组件,但是Google
已经让他们都实现了LifecycleOwner
这个接口,分别是androdx.fragment.app.Fragment
、AppCompatActivity
、androidx.lifecycle.LifecycleService
.
在项目中,只要继承这些类型,可以轻松的通过LifecycleOwner#getLifecycle()
获取到Lifecycle
实例.这是一种解耦实现,LifecycleOwner
不包含任何有关生命周期管理的逻辑,实际的逻辑都在Lifecycle
实例中,我们可以通过传递Lifecycle
实例而非LifecycleOwner
来防止内存泄漏.
而Lifecycle
这个类的只有这三个方法:
@MainThread
public abstract void removeObserver(@NonNull LifecycleObserver observer);
@MainThread
@NonNull
public abstract State getCurrentState();
@MainThread
public abstract void addObserver(@NonNull LifecycleObserver observer);
getCurrentState()
可以返回当前该LifecycleOwner
的生命周期状态,该状态与LifecycleOwner
上的某些回调事件相关,只会出现以下几种状态,在Java
中以一个枚举类抽象出来定义在Lifecycle
类中。
public enum State
{
DESTROYED,
INITIALIZED,
CREATED,
STARTED,
RESUMED;
}
DESTROYED
,在组件的onDestroy
调用前,会变成该状态,变成此状态后将不会再出现任何状态改变,也不会发送任何生命周期事件INITIALIZED
,构造函数执行完成后但onCreate
未执行时为此状态,是最开始时的状态CREATED
,在onCreate
调用之后,以及onStop
调用前会变成此状态STARTED
,在onStart
调用之后,以及onPause
调用前会变成此状态RESUMED
,再onResume
调用之后会变成此状态
addObserver
,此方法可以给LifecycleOwner
添加一个观察者,来接收LifecycleOwner
上的回调事件。回调事件也是一个枚举,定义在Lifecycle
类中:
public enum Event
{
/**
* Constant for onCreate event of the {@link LifecycleOwner}.
*/
ON_CREATE,
/**
* Constant for onStart event of the {@link LifecycleOwner}.
*/
ON_START,
/**
* Constant for onResume event of the {@link LifecycleOwner}.
*/
ON_RESUME,
/**
* Constant for onPause event of the {@link LifecycleOwner}.
*/
ON_PAUSE,
/**
* Constant for onStop event of the {@link LifecycleOwner}.
*/
ON_STOP,
/**
* Constant for onDestroy event of the {@link LifecycleOwner}.
*/
ON_DESTROY,
/**
* An {@link Event Event} constant that can be used to match all events.
*/
ON_ANY
}
每种事件都对应着Fragment
/Activity
中的事件。
3.3 LifecycleObserver
LifecycleObserver
是生命周期的观察者,可能是这个包中我们最常用的接口了.
查看源码得知,他就是一个空接口,不包含任何实现,但是若我们想使用,还是得继承此接口。
代码语言:javascript复制public interface LifecycleObserver { }
继承LifecycleObserver
后使用@OnLifecycleEvent
注解(这时之前申明得注解处理器派上了用场),并设置需要监听的生命周期回调事件。
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void test()
{
///TODO...
}
然后在Activity
/Fragment
中:
getLifecycle().addObserver(yourLifecycleObserver);
即可在运行时收到相应的的回调事件,但是注意添加@OnLifecycleEvent
注解的方法应该是包内访问权限或是public
的,否则可能在编译时会报错,或者收不到回调。
若想在运行时移除LifecycleObserver
,同样也还有Lifecycle#removeObserver
方法。
4 LiveData
LiveData
是对Android
组件生命周期感知的粘性事件
,也就是说,在LiveData
持有数据时,你去订阅它就能收到他最后一次接收到的数据.在实战中,我们能用到的LiveData
一般是它的两个子类MutableLiveData
和MediatorLiveData
.
4.1 LiveData基本使用
我们可以通过LiveData#observe
来观察它所持有的值的变化,还可以通过LiveData#getValue
来直接获取内部保存的值(非线程安全)
//LiveData 一般是用来给ViewModel保存数据的
public class MyViewModel extends ViewModel{
private MutableLiveData<Boolean> mIsLoading = new MutableLiveData<>();
LiveData<Boolean> isLoading(){
return mIsLoading;
}
}
//Activity/Fragment观察ViewModel
mViewModel.isLoading().observe(this, isLoading -> {
//TODO 发生在主线程,触发相关处理逻辑
});
//LiveData是依赖Lifecycle实现的
//传入的this是LifecycleOwner
//LiveData只会通知激活态的(STARTED和RESUMED)的LifecycleOwner
//并且在Activity/Fragment被重建也能重新接收到LiveData保存的数据
//在组件DESTROYED时,LiveData会把它移出观察者列表
//当然你也可以不关联LifecycleOwner,让订阅一直保持.
//需要这样时需要使用observeForever
mViewModel.isLoading().observeFo(isLoading -> {
//TODO
});
//这个订阅永远不会被取消
//除非你显示调用LiveData#removeObserver
4.2 MutableLiveData
顾名思义就是可变的LiveData
,基类LiveData
默认是不可变的,MutableLiveData
开放了能够改变其内部所持有数据的接口.
public class MutableLiveData<T> extends LiveData<T> {
/**
* Creates a MutableLiveData initialized with the given {@code value}.
*
* @param value initial value
*/
public MutableLiveData(T value) {
super(value);
}
/**
* Creates a MutableLiveData with no value assigned to it.
*/
public MutableLiveData() {
super();
}
@Override
public void postValue(T value) {
super.postValue(value);
}
@Override
public void setValue(T value) {
super.setValue(value);
}
}
分别是postValue
和setValue
,其中setValue
内部检查线程是否为主线程,不允许在子线程中使用,用了就报错.postValue
会将值通过主线程的Handler
转发到主线程上.
LiveData
可以有初始值,也可以没有,如果在没有初始值得情况下被订阅,则订阅者不会收到任何的值.
4.3 MediatorLiveData
MediatorLiveData
继承自MutableLiveData
,它主要用来实现多个LiveData
数据源的合并.
public class MediatorLiveData<T> extends MutableLiveData<T> {
private SafeIterableMap<LiveData<?>, Source<?>> mSources = new SafeIterableMap<>();
@MainThread
public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<? super S> onChanged) {
Source<S> e = new Source<>(source, onChanged);
Source<?> existing = mSources.putIfAbsent(source, e);
if (existing != null && existing.mObserver != onChanged) {
throw new IllegalArgumentException(
"This source was already added with the different observer");
}
if (existing != null) {
return;
}
if (hasActiveObservers()) {
e.plug();
}
}
@MainThread
public <S> void removeSource(@NonNull LiveData<S> toRemote) {
Source<?> source = mSources.remove(toRemote);
if (source != null) {
source.unplug();
}
}
@CallSuper
@Override
protected void onActive() {
for (Map.Entry<LiveData<?>, Source<?>> source : mSources) {
source.getValue().plug();
}
}
@CallSuper
@Override
protected void onInactive() {
for (Map.Entry<LiveData<?>, Source<?>> source : mSources) {
source.getValue().unplug();
}
}
private static class Source<V> implements Observer<V> {
final LiveData<V> mLiveData;
final Observer<? super V> mObserver;
int mVersion = START_VERSION;
Source(LiveData<V> liveData, final Observer<? super V> observer) {
mLiveData = liveData;
mObserver = observer;
}
void plug() {
mLiveData.observeForever(this);
}
void unplug() {
mLiveData.removeObserver(this);
}
@Override
public void onChanged(@Nullable V v) {
if (mVersion != mLiveData.getVersion()) {
mVersion = mLiveData.getVersion();
mObserver.onChanged(v);
}
}
}
}
它比MutableLiveData
多了两个方法addSource
和removeSource
,通过这两个方法我们可以将其他LiveData
合并到此LiveData
上,当其他LiveData
发生改变时,此LiveData
就能收到通知.
@MainThread
public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<? super S> onChanged)
@MainThread
public <S> void removeSource(@NonNull LiveData<S> toRemote)
通过查看源码,我们可以知道在有观察者时LiveData#onActive
会被回调,MediatorLiveData
会在内部迭代,用observeForever
订阅所有被合并进来的LiveData
,这样就能接收所有LiveData
的变化,在没有观察者时LiveData#onInactive
会被回调,此时执行反操作removeObserver
.
4.4 变换
使用androidx.lifecycle.Transformations
这个工具类可以将持有一种类型的LiveData
转换为另一种LiveData
.他有类似于RxJava
的使用方式.
LiveData<Boolean> boolLiveData = getBoolLiveData();
LiveData<String> stringLiveData = Transformations.map(boolLiveData,bool->Boolean.toString(bool));
上面只是一个演示,实际上可以执行更为复杂的逻辑,并且这种转换是惰性的,在没有激活态观察者时,这种转换不会发生.
5 ViewModel
5.1 自定义ViewModel
ViewModel
其实没什么可说的,其源码主要的部分其实就只有这些
public abstract class ViewModel {
protected void onCleared() {
}
}
简直一目了然,我们可以在ViewModel
上使用LiveData
作为字段保存数据,并编写业务逻辑
(数据处理逻辑).就像这样
public class MyViewModel extends ViewModel
{
public MutableLiveData<String> username = new MutableLiveData<>();
public MutableLiveData<String> password = new MutableLiveData<>();
public MutableLiveData<String> text = new MutableLiveData<>();
public void action1(){
//TODO
}
public void initName(){
username.setValue("Luke Luo");
}
//......
@Override
protected void onCleared() {
//TODO 清理资源
}
}
onCleared
会在组件销毁的时候回调,我们可以重写这个方法在ViewModel
销毁时添加一些自定义清理逻辑.
ViewModel
还有一个子类AndroidViewModel
也是一目了然,只是保存了Application
实例而已.
public class AndroidViewModel extends ViewModel {
@SuppressLint("StaticFieldLeak")
private Application mApplication;
public AndroidViewModel(@NonNull Application application) {
mApplication = application;
}
/**
* Return the application.
*/
@SuppressWarnings("TypeParameterUnusedInFormals")
@NonNull
public <T extends Application> T getApplication() {
//noinspection unchecked
return (T) mApplication;
}
}
5.2 自定义ViewModel构造方式
我们可以通过ViewModelProviders
来获取ViewModel
,这样获取的ViewModel
会绑定组件的生命周期(即在销毁时自动调用onCleared
)
mViewModel = ViewModelProviders.of(this).get(CustomViewModel.class);
在Android
的Lifecycle
实现中框架向Activity
中添加了一个继承了系统Fragment
的ReportFragment
来汇报组件的生命周期,如果你使用的是appcompat
的Fragment
,那么它对你就是不可见的,所以一定要避免使用系统的Fragment
(在API28
中已被标记为弃用).
ViewModel
通过Lifecycle
来管理自身释放,在组件的ON_DESTROY
事件来到时,它的onCleared()
也会被调用.
如果你想有自定义构造函数参数的ViewModel
那你就得继承ViewModelProvider.AndroidViewModelFactory
了
//自定义构造函数的ViewModel
public class NaviViewModel extends AndroidViewModel
{
private AMapNavi mNavi;
public NaviViewModel(AMapNavi navi,Application application)
{
super(application);
mNavi = navi;
}
//......
}
//继承并重写create
public final class NaviViewModelFactory
extends ViewModelProvider.AndroidViewModelFactory
{
private final AMapNavi navi;
private final Application application;
public NaviViewModelFactory(@NonNull Context context, AMapNavi navi)
{
super((Application) context.getApplicationContext());
this.application = (Application) context.getApplicationContext();
this.navi = navi;
}
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass)
{
try
{
Constructor<T> constructor = modelClass
.getConstructor(Application.class, AMapNavi.class);
return constructor.newInstance(application, navi);
} catch (Exception e)
{
return super.create(modelClass);
}
}
}
//使用
NaviViewModelFactory factory = new NaviViewModelFactory(context, navi);
mViewModel = ViewModelProviders.of(this, factory).get(NaviViewModel.class);
说白了就是反射调用构造函数创建,也是一目了然.
6 RxJava
本篇文章只是针对响应式编程在MVVM
体系下的应用,不对RxJava
展开深度讨论,但是后面还会专门出一篇文章讨论RxJava
的有关知识.
RxJava
在MVVM
中主要用于发布事件,下面是需要注意的一些点.
6.1 使用AutoDispose
RxJava
是响应式编程这种思想在JVM
这个平台上的实现,所以它一开始并没有为Android
平台的特点而做出优化.
就像上面所介绍过的一样,Android
的组件是有明确的生命周期的,如果在组件销毁后,RxJava
仍有后台线程
在运行且你的Observer
引用了你的Activity
,就会造成内存泄漏.
但其实RxJava
是提供了释放机制的,那就是Disposeable
,只不过这个实现这个机制的逻辑需要我们手动在Activity#onDestroy
中进行硬编码,这会带来大量的样板代码.
为了解决这一局面,在Android Jetpack
还没有诞生的时候,有大神开发了RxLifecycle,但是这个框架需要强制继承基类,对于一些现有项目的改造来说,其实是不太友好的,个人感觉并没有从根本上解决问题.
Android Jetpack
诞生后AutoDispose给了我们另外一条出路.它使用RxJava2
中的as
运算符,将订阅者
转换成能够自动释放
的订阅者对象
.
在你的build.gradle
中添加依赖:
implementation 'io.reactivex.rxjava2:rxjava:2.2.6'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'com.uber.autodispose:autodispose:1.1.0'
implementation 'com.uber.autodispose:autodispose-android-archcomponents:1.1.0'
一个简单的示例:
代码语言:javascript复制Observable.just(new Object())
//使用AutoDispose#autoDisposable
//并使用AndroidLifecycleScopeProvider#form
//指定LifecycleOwner和需要在哪一个事件进行销毁
//关键↓是这行
.as(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(activity, Lifecycle.Event.ON_DESTROY)))
.subscribe();
上面代码的时间订阅将会在组件的Lifecycle.Event.ON_DESTROY
事件来到时被释放,当然你也可以指定其他事件时释放.
6.2 防止多重点击
首先你可以使用JW大神
的RxBinding来实现这一需求,但是今天我们不讨论RxBinding
,因为网上的讨论RxBinding
的文章已经太多了,随便抓一篇出来都已经非常优秀.
今天我们模仿RxBinding
实现一个简单的,轻量化的,基于Java动态代理
的,并且兼容所有第三方View
所自定义Listener
接口的防止多重点击机制.
二话不说先上代码:
代码语言:javascript复制import androidx.collection.ArrayMap;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import com.uber.autodispose.AutoDispose;
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider;
import io.reactivex.subjects.PublishSubject;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static io.reactivex.android.schedulers.AndroidSchedulers.mainThread;
public final class RxOnClick<X>
{
//默认最低的可取的时间
private static final int MINI_TIME = 200;
private final Class<X> mInterface;
private X mInner;
private LifecycleOwner mOwner;
private int mTime;
private Lifecycle.Event mEvent;
private RxOnClick(Class<X> type)
{
mInterface = type;
}
//从一个创建接口类型创建
public static <X> RxOnClick<X> create(Class<X> type)
{
return new RxOnClick<>(type);
}
//实际处理事件的Listener
public RxOnClick<X> inner(X inner)
{
mInner = inner;
return this;
}
//依附于的组件也就是LifecycleOwner
public RxOnClick<X> owner(LifecycleOwner owner)
{
mOwner = owner;
return this;
}
//只去time毫秒内的第一个结果作为有效结果
public RxOnClick<X> throttleFirst(int time)
{
mTime = time;
return this;
}
//在哪一个事件进行释放
public RxOnClick<X> releaseOn(Lifecycle.Event event)
{
mEvent = event;
return this;
}
//创建代理类实例
@SuppressWarnings("unchecked")
public X build()
{
//检查参数
if (mInterface == null || !mInterface.isInterface())
{
throw new IllegalArgumentException();
}
if (mTime < MINI_TIME)
{
mTime = MINI_TIME;
}
if (mEvent == null)
{
mEvent = Lifecycle.Event.ON_DESTROY;
}
if (mOwner == null || mInner == null)
{
throw new IllegalStateException();
}
//用反射遍历获取所有方法
Map<Method, PublishSubject<Object[]>> subjectMap = new ArrayMap<>();
for (Method method : mInterface.getDeclaredMethods())
{
PublishSubject<Object[]> subject = PublishSubject.create();
subject.throttleFirst(mTime, TimeUnit.MILLISECONDS)
.observeOn(mainThread())
.as(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(mOwner, mEvent)))
.subscribe(args -> method.invoke(mInner, args));
subjectMap.put(method, subject);
}
//使用动态代理代理代理该接口并使用PublishSubject进行转发
return (X) Proxy.newProxyInstance(mInterface.getClassLoader(),
new Class[]{mInterface},
(proxy, method, args) -> {
//Object类的方法直接调用
if (Object.class.equals(method.getDeclaringClass()))
{
return method.invoke(proxy, args);
}
//否则转换为Rx事件流
PublishSubject<Object[]> subject = subjectMap.get(method);
if (subject != null)
{
subject.onNext(args);
}
return null;
});
}
}
上面类在设计上采用了Builder
模式,所以它实际是一个Builder
.
其核心原理就是使用Java的动态代理
机制创建Listener
的代理类,代理类不处理事件,而是将事件通过PublishSubject
(释放订阅后接收到的事件)转换为RxJava
事件流推送到真正处理事件的Listener
上.
这样我们就可以在这个事件流上对事件做手脚了,并且这样还能兼容RxBinding
所不能兼容的第三方自定义View
.
比如上面就加入了xxx毫秒内只取第一次点击和绑定组件的生命周期,用起来的时候就像是下面,依然非常简洁并且非常的有用:
代码语言:javascript复制 View.OnClickListener listener = RxWrapper
.create(View.OnClickListener.class)
.owner(this)
.inner(v -> {
//TODO
})
.build();
7 使用MVVM改造Android现有体系
笔者就Android
现有体系
下的各种类库
和框架
,通过自己实践的得出的经验将其进行如下归类,观点仅供参考,在实践中应该视项目特点进行适当进行改造.
7.1 View层
现有体系下的内容:
-
Activity/Fragment
(布局生命周期与逻辑控制器) - 原来就存在的
View
- ......
设计原则:
-
View
层不应该承担处理数据的责任,它应该只负责数据如何显示. - 它不应该直接持有
Model
层的任何引用,也不应该直接持有Model
层的数据. -
View
层正常的行为应该是观察某个ViewModel
,间接获取该ViewModel
从Model
层中获取并处理过能在View
层上直接显示的数据. - 这样可以保证在
Activity
重建时页面上有关的数据不会丢失而且也不会造成View
层与Model
层的耦合.
7.2 DataBinding
现有体系下的内容:
-
Jetpack DataBinding
函数库 -
View
的Adapter
- ......
设计原则:
- 理想状态下,
DataBinding
与View
构建的关系应该是数据驱动的,即只要数据不改变View
层实现的变更不会导致逻辑的重新编写(如把TextView
改成EditText
也不需要修改一行代码). - 虽然
DataBinding
函数库已经完成了大多数DataBinding
应该做的事,但是不要为了数据驱动而排斥使用android:id
来获取View
并对View
直接赋值,虽然这不够数据驱动,但是适当使用是可以的,毕竟Android
的View
层目前还没有办法做到完全的数据驱动(主要是第三方库的兼容问题). -
Adapter
应该属于DataBinding
的一种,与DataBinding
函数库中生成的DataBinding
相同,它也是使用数据来触发View
层的改变.所以尽可能不要把它写到ViewModel
中,但这不是必须的,做在对List
操作要求比较高的情况下可以写到ViewModel
中,但要保证一个原则——ViewModel
应该只负责提供数据,而不应该知道这些数据要与何种View
进行交互.
7.3 事件传递
现有体系下的内容:
-
EventBus
事件总线 -
RxJava
事件流
设计原则:
-
Jetpack
中实现的LiveData
能够很好的作为数据持有者,并且是生命周期感知的,但是有些时候我们需要向View
层发送一些单次的数据,这时LiveData
并不能够很好地工作.Rxjava
和EventBus
是更好的选择.
7.4 ViewModel层
现有体系下的内容:
Jetpack ViewModel
Jetpack LiveData
- 用于将
Model
数据转换成View
能直接显示的数据的工具类 - ......
设计原则:
-
ViewModel
通常应该使用LiveData
持有View
层数据的实际控制权 -
ViewModel
可以包含操作,但是ViewModel
不应该直接或者间接地引用View
,即使是方法中的参数也最好不要,因为ViewModel
不应该知道自己到底是与哪一个View
进行交互. -
ViewModel
与Model
的关系应该是——将Model
层产生的数据翻译
成View
层能够直接消化吸收的数据。 -
ViewModel
可以向View
层发送事件,然后View
可以订阅这些事件以收到ViewModel
层的通知.
7.5 Model层
现有体系下的内容:
- 部分与
Activity
无关的系统服务 -
Room
(SQLite
数据库) -
Retrofit
(网络数据) SharedPreferences
- ......
设计原则:
- 涉及
Activity
请一定不要包含进来,如WindowManager
,它们属于View
层. -
Model
层主要是原始数据的来源,由于存储格式/传输格式
与显示格式
存在的巨大差异,View
层往往并不能很好的直接消化这些数据,这时就需要一个中间人
作为翻译
,由此抽象出了ViewModel
.
8 实战
我编写了一个简单的FTP
客户端作为本次MVVM
博文的演示Demo
,该项目简单实践了QMUI
MVVM
DataBinding
RxJava
LiveData
Room
的技术栈并由kotlin
和Java
混编写成,代码质量比较一般,有爱自取吧.
9 参考资料以及推荐阅读
- DataBinding源码解析
- Google官方黑科技 - DataBinding
10 结语
本篇文章多达10000
字,感谢您在百忙之中抽空观看.所有内容均为个人学习总结与理解,仅供参考.
【附】相关架构及资料
Android高级技术大纲