1.前言
在一个公司呆久了就容易陷入安逸期,技术方面也是如此,很容易止步不前,想要推翻项目原有架构技术重构更是困难重重,但是作为一名程序员,只有不断学习才不会被淘汰,或者说才能跟上行业发展的节奏。
现在对于Android
开发来讲,随着行业发展越来越成熟,对人才的要求也越来越高,面试的问题也越来越深,更多是偏向于新技术或者老技术原理性的东西,所以今年准备好好学习下这些,一则为了提高自己的技术能力,这个是非常必要的,二来可以跟上步伐,以应对未来的一切突发状况,因为只有自己变得强大,才有话语权和选择权,所以各位加油吧!
databinding
这个词相信很多人并不陌生,很早就见过它,也看过一些博客教程等等,但是并没有实际去使用,后来也就慢慢搁置,其实看名字就可以知道它是个什么东西,就是数据绑定嘛,那么为什么会出现这么个东西,原始的MVC
它不香么?是的,它确实不香,各种耦合使得牵一发而动全身,一个activity
上千上万行,业务逻辑在其中到处游走,以至于项目越来越臃肿,那么咋办?不要问,问就是重构。
重构是不可能重构的,没事喝杯茶它不香么?正在喝茶期间,突然发现MVP
模式已经火了起来,那还等什么,人家有的我都要有,于是MVP
开始大放异彩,各种MVP
教程满天飞,对于MVP的讨论也越来越多,优缺点也很明显,优点呢,就是项目的耦合度有所降低,它将数据(Model
)、视图(View
)、控制(Presenter
)分开,实现了松耦合。
View
接受事件,传递给Presenter
Presenter
做逻辑处理,修改Model
Model
通知Presenter
数据变化,Presenter
更新View
看起来非常完美,但是世界上没有什么东西是完美的,它存在如下缺点:
- 与
MVC
一样,P层起到的控制功能伴随着业务的增多,也会变得臃肿。 Presneter
需要持有View
的引用,同时View
也需要持有Presenter
的引用,控制上存在一定复杂度。
俗话说的好,知足常乐,有点缺点就算了吧,不要斤斤计较,但是对于程序员来讲,都有强迫症,虽然凑合着用吧,但是肯定心里有点不舒服,所以MVVM
模式来了,不学习看来真不行,特别是一打开技术博客各种MVVM
的文章满天飞的时候。
太难了,终于扯到了今天的主题,MVVM
实现了数据与UI的双重绑定,其中DataBinding
是实现MVVM的工具,借用网上的一张图来看下MVVM
是个什么东西:
M代表了Model
,还是负责处理数据层的东西,V代表了View
,也就是页面,VM代表了ViewModel
,这里有个新引入的东西ViewModel
,它是databinding
数据绑定的关键所在,下面先介绍一下它。
2.ViewModel
ViewModel
官方释义:感知生命周期的形式来存储和管理视图相关的数据。
ViewModel
的生命周期很长,存在于所属对象(Activity
,Fragment
)的全部生命周期,可以与jetpack其他组件很方面的结合使用,另外比如activity
横竖屏切换,它依然可以保持数据不丢失,也可以用于同一activity
下fragment
的数据共享。
3.databinding具体使用
下面我们就结合databinding和ViewModel来看看基本使用方式,首先引入相关库:
// viewModel
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0'
设置databinding支持:
//老版使用这个
/*dataBinding{
//noinspection DataBindingWithoutKapt
enabled = true
}*/
//新版用这个
buildFeatures{
dataBinding = true
}
如果使用kotlin,还需要添加kotlin-kapt插件:
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
此时切换到layout
布局,鼠标放在ConstraintLayout
,Alt+Enter键会出现如下提示:
继续按回车键,布局就转换为了databinding
模式,形如:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
//这边用来引入viewmodel类,用于和布局绑定
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
//·····
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
此时我们可以新建一个类继承ViewModel
,并设置一个userName
变量,用于测试数据绑定:
class MyMainModel : ViewModel() {
var userName = "张三"
}
然后修改layout
布局,引入MyMainModel
类,并且把userName
设置到Textview
上:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
//导入MyMainModel类
<import type="com.xxx.mvvmblog.MyMainModel"/>
//这边相当于引入数据类,用于绑定布局
<variable
name="viewModel"
type="MyMainModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textColor="@color/black"
android:text="@{viewModel.userName}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnChange"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textColor="@color/black"
android:layout_marginTop="10dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_name"
android:text="change"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
我在TextView
下面添加了个Button
按钮,点击按钮修改MyMainModel
中userName
的值,由于数据和布局是绑定的,此时应该布局显示应该会更新,在activity
中获取viewmodel
,采用如下方式:
ViewModelProvider(this).get(MyMainModel::class.java)
获取databinding实例如下:
DataBindingUtil.setContentView(this, R.layout.activity_main)
那么此时activity
代码如下:
class MainActivity : AppCompatActivity() {
private lateinit var myMainModel: MyMainModel
private lateinit var activityMainBinding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
myMainModel = ViewModelProvider(this).get(MyMainModel::class.java)
activityMainBinding.viewModel = myMainModel
initView()
}
private fun initView() {
findViewById<Button>(R.id.btnChange).setOnClickListener {
myMainModel.userName = "我是改变后的值:李四"
activityMainBinding.viewModel = myMainModel
}
}
}
来,运行下,看看效果:
有那么回事了,通过databinding
修改布局中绑定的viewmodel
类的值,就可以自动更新ui,这也就是我们看到的单向绑定,何为单向,意思就是数据改变,布局改变,那既然有单向,肯定有双向,是的,双向则是指相互可以更新,我们用EditText
来演示下,修改下布局:
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:hint="请输入"
android:text="@={viewModel.userName}"
app:layout_constraintBottom_toTopOf="@+id/tv_name"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
单向绑定采用@{}的形式设置数据,双向使用@={},中间加了个=号,activity
代码也相应修改下:
插播一下,写着写着发现findViewById
太烦了,索性添加了如下插件:
id 'kotlin-android-extensions'
此时就可以直接使用布局中的id作为组件对象,太嗨皮了!!
咱们接着修改activity
代码:
editText.addTextChangedListener {
Log.e("userName","new userName = ${activityMainBinding.viewModel!!.userName}")
}
来看看效果:
因为我们把userName
和Edittext
进行了双向绑定,所以我们在修改EditText
的时候,相应的数据源也会发生改变,我们把viewModel
给打印了出来,可以发现此时数据是一直在变的,我们再修改下代码:
editText.addTextChangedListener {
Log.e("userName","new userName = ${activityMainBinding.viewModel!!.userName}")
//加上这句,Edittext变化后通知更新Textview
activityMainBinding.viewModel = myMainModel
}
效果如下:
注意:
1.类名字重复,使用别名
如果导入的类名字重复,可以使用别名alias指定别名<data>
<import type="com.xxx.mvvmblog.MyMainModel"
alias="AliasName"
/>
<variable
name="viewModel"
type="AliasName" />
</data>
2.多variable
可以指定多个variable,如果使用<>,需要使用转义字符:<data>
<import type="java.util.List" />
<import type="com.xxx.mvvmblog.MyMainModel" />
<variable
name="list"
type="List<MyMainModel>" />
<variable
name="viewModel"
type="MyMainModel" />
</data>
3.修改databinding名字
默认的Binding类生成的规则是:xml的名称去掉下划线,再首字母大写,最后尾部加个Binding,当然这个事可以自定义的,使用class关键字来修改,形如:
<data class="BindingName">
</data>
4.绑定方法
三种方式:
- 普通方法调用:android:onClick="@{()->viewModel.clickMe()}"
fun clickMe(){
Log.e("click","click me -- 方法")
}
- 双冒号调用:android:onClick="@{viewModel::clickMe}"
fun click(view: View){
Log.e("click","click me: $view, 使用::调用,方法定义需要与接口参数一致")
}
- 变量调用:android:onClick="@{viewModel.clickMe}"
val clickMe = View.OnClickListener {
Log.e("click", "click me -- 变量")
}
5.转换器
如果我们想要对某类数据进行单独处理,比如想在所有字符串后面添加(试用版)标识,那么就可以使用转化器BindingConversion
,具体使用方式如下,定义:
object ConvertionTest {
@JvmStatic
@BindingConversion
fun convert(value: String?) = "${value}(试用版)"
}
效果:
我们设置username
后,在绑定更新ui的时候结尾会自动加上(试用版)几个字。
6.自定义属性绑定方法
一、在view中,如果提供setXxxx
方法,就有xxxx
属性,我们可以直接在layout中使用该属性,比如selected
属性,Textview
在布局中并没有提供此属性,但是代码中有setSelected
方法,所以我们可以这么用:
app:selected="@{true}"
鼠标放上去按住ctrl键,可以看到相关属性和方法:
二、自定义属性绑定方法
比如我们想要自定义一个TextView
,一旦TextView
文本有变化,就弹出toast,那么我们可以这样做,使用@BindingMethods
注解,定义自定义属性:
@BindingMethods(
value = [
BindingMethod(
type = TextView::class,
attribute = "showToast",
method = "show"
)]
)
class MyTextView(context: Context?, attrs: AttributeSet?) :
AppCompatTextView(context!!, attrs) {
fun show(toastStr: String) {
Toast.makeText(context, "哈哈哈,$toastStr", Toast.LENGTH_LONG).show()
}
}
attribute
对应的就是xml中的属性,method
则对应我们自定义的show
方法,我们在xml中使用它:
<com.xxx.mvvmblog.MyTextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.userName}"
android:textColor="@color/black"
android:textSize="20sp"
app:showToast="@{viewModel.userName}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
看下效果:
通过 BindingMethod
注解,可以自定义属性名,并绑定指定方法。当绑定的数据变化时,会自动调用该方法。
7.自定义多属性绑定
@BindingAdapter
这个注解用于支持自定义属性,或者是修改原有属性。注解值可以是已有的 xml 属性,例如 android:src、android:text等,也可以自定义属性然后在 xml 中使用,我们都知道imageview并不支持直接加载网络图片,那么我们在使用databinding的时候,可以通过自定义属性的方式实现此功能,只需指定图片地址即可实现自动加载图片,那么实现起来也很简单,通过BindingAdapter
自定义一个imageUrl属性:
class ImageLoader {
companion object {
@JvmStatic
@BindingAdapter("imageUrl", "placeholder", "error",requireAll = false)
fun loadImageView(
imageView: ImageView,
url: String?,
drawHolder: Drawable?,
error: Drawable?
) {
Glide.with(imageView).load(url).into(imageView)
}
}
}
可以看到我们定义了imageUrl,placeholder,error等属性,当然你可以随便添加其他需要的属性,这边还设置了requireAll为false,它的含义是设置可以不配置所有属性,意思就是如果requireAll为true,需要指定全部自定义的属性,那么我们看下如何使用此属性:
<ImageView
android:layout_width="200px"
android:layout_height="200px"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toTopOf="@+id/editText"
imageUrl="@{`https://hbimg.huabanimg.com/52c4c08a08e719e1e2cad797fe53feb97917b6fa441d-I5Yl20_fw658/format/webp`}"
/>
也很简单,指定需要的参数和属性即可,然后运行看下效果:
可以看到,图片已经加载并显示出来了,此处仅仅抛砖引玉,你可以充分发挥自己的灵感,做其他任何类似的事情。
4.结合LiveData使用
首先,LiveData
是什么呢?LiveData
是一个可以被观察的数据持有类,它可以感知 Activity
、Fragment
或Service
等组件的生命周期,它有如下优点:
-
在组件处于激活状态的时候才会回调相应的方法,从而刷新相应的ui;
-
避免了内存泄漏;
-
当
config
导致activity
重新创建的时候,不需要手动去处理数据的储存和恢复,它已经帮我们封装好了;
先看个简单的例子吧,首先引入LiveData
库:
// liveData
api 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0'
然后在mainviewModel中加入如下代码:
//新建一个数据类Student,测试用
data class Student(val name: String)
class MyMainModel : ViewModel() {
val studentLiveData = MutableLiveData<Student>()
······
}
然后我们在MainActivity中获取viewmodel实例,并且为studentLiveData添加监听,此时一旦有数据变化,并且activity处于活跃状态下,就会回调observe,来看关键代码:
myMainModel = ViewModelProvider(this).get(MyMainModel::class.java)
activityMainBinding.viewModel = myMainModel
myMainModel.studentLiveData.observe(this, Observer {
//数据有更新/变化
myMainModel.userName = it.name
activityMainBinding.viewModel = myMainModel
})
然后我们模拟一下数据请求:
private fun loadData() {
launch {
withContext(Dispatchers.IO) {
delay(2000)
}
val student = Student("我是新来的学生")
myMainModel.studentLiveData.postValue(student)
}
}
当我们点击按钮的时候,开始执行loadData方法:
findViewById<Button>(R.id.btnChange).setOnClickListener {
loadData()
}
然后我们看下效果:
可以发现,我们点击按钮2秒后,通过postValue方法更新了studentLiveData的值,此时会回调observe方法,然后进行ui的刷新等操作,这个过程就和我们平时进行网络请求的过程非常类似。
livedata可以感知生命周期的含义就是,比如你在A页面进行observe监听,然后跳转到页面B,此时通过postValue更新数据(当然我们也可以使用setValue,区别就是setValue只能在主线程发送数据,post可以在子线程发送数据),此时是不回回调observe的,当你点击返回键,回到A页面的时候,才会触发observe回调,这样就避免了内存泄漏的风险,这个过程大家可以自行尝试并感知。
注意:如果在布局中直接绑定的livedata数据源,形如:
<com.xxx.mvvmblog.MyTextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
//注意这一行
android:myText="@{viewModel.studentLiveData.name}"
android:textColor="@color/black"
······/>
上面的例子我是直接使用的mytext="@{viewModel.userName}"
的方式更新的数据,此时会刷新ui,当时如果用了viewModel.studentLiveData.name
,就需要databinding设置LifecycleOwner
才会触发ui的更新,也就是要给databinding
加上如下代码:
activityMainBinding.lifecycleOwner = this
这样通过改变liveData
的数据就可以触发绑定其数据源的ui组件进行更新,换句话说就是DataBinding
需调用setLifecycleOwner(LifecycleOwner lifecycleOwner)
之后,绑定了LiveData
数据源的xml控件才会随着数据变化而改变。
本篇主要讲解了基础使用,文中必然存在一些问题和错误,轻喷,欢迎指正.
参考: