Jetpack-DataBinding

Jetpack-DataBinding数据双向绑定(我只用到了单向绑定)

DataBinding

用来替代 findViewById 等的实例化控件。(DataBinding是为了能更好的实现MVVM架构而设计的)

优势:

  • 项目更简洁,可读性更高。部分与UI控件相关的代码可以在布局文件中完成
  • 不再需要 findViewById() 方法
  • 布局文件可以包含简单的业务逻辑。UI控件能够直接与数据模型中的字段绑定,甚至能相应用户的交互。

用viewmodel、livedata、databinding架构图

jetpack-用viewmodel、livedata、databinding架构图

此时把findViewById等绑定控件的操作都抽到databinding中进行管理

DataBinding基本用法

打开databinding开关

app/build.gradle--android--defaultConfig最后添加

1
2
3
4
5
6
7
8
dataBinding{
enabled true
}

//或者【项目中用的这个】
buildFeatures{
dataBinding = true
}

布局中使用databinding布局

最外面套一层layout,再加data标签(用来管理数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?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>
<!-- 自定义name,关联自己的viewmodel类 -->
<variable
name="myData"
type="com.ab.viewmodelandlivedataanddatabinding.MyViewModel" />
</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<!-- 使用viewModel中的属性 -->
<TextView
android:id="@+id/show_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(myData.number)}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.157" />

<!-- 使用viewModel中的方法 -->
<Button
android:id="@+id/add_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/button"
android:onClick="@{() -> myData.add()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

其中@{写类似java的代码}。其中若调用方法则用 ()->函数

<variable />内的数据类型可以是自定义类型,也可以是基本类型,如String

绑定事件的另一种写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class EventHanleActivity extends AppCompatActivity{
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
ActivityEventHanleBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_event_handle);
binding.setEventHandler(new EventHandleListener(this));
}

public class EventHandleListener{
private Context context;
public EventHandleListener(Context context){
this.context = context;
}
public void onButtonClicked(View view){
//...
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
<layout>
<data>
<variable
name="EventHandler"
type="com.ab.databindingdemo.EventHanleActivity.EventHandleListener"/>
</data>

<Button
andorid:onClick="@{EventHandler.onButtonClicked}"/>
<Button
andorid:onClick="@{EventHandler::onButtonClicked}"/>
</layout>

通过布局表达式调用;或者通过双冒号::来调用;

使用databinding后解放Activity中的findViewById的关联

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.ab.viewmodelandlivedataanddatabinding.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
// lateinit var btn : Button
// lateinit var showTv : TextView
lateinit var myViewMainActivity: MyViewModel
lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_main)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
myViewMainActivity = ViewModelProviders.of(this).get(MyViewModel::class.java)
//使用databinding的数据绑定(未在xml中定义自己的data时)
// btn = findViewById(R.id.add_btn)
// showTv = findViewById(R.id.show_tv)
/*myViewMainActivity.getNumber().observe(this, Observer {
// showTv.text = it.toString()
binding.showTv.text = it.toString()
})
binding.addBtn.setOnClickListener{
myViewMainActivity.add()
}*/


//最终形式,数据反向绑定到xml中,彻底解放activity中的viewGroup关联
//设置自定义data的数据源
binding.myData = myViewMainActivity
//设置databinding的lefecycleOwner
binding.lifecycleOwner = this
}
}

给布局文件传递对象

  • 可以在 Activity 中调用 setVariable(BR.myData, 具体对象) 把具体对象传递给布局文件

    BR类类似于Android中的R类,由DataBinding库自动生成,用于统一存放所有布局变量的id。
    BR.myData,后面就是布局中变量的name

  • 也可以在 Activity 中调用 setMyData(myData)来传递对象

    DataBinding为了方便我们使用,为布局变量提供了Setter类。

DataBindingUtil源码内有好几种获取Binding对象的方法

1
2
3
4
5
6
inflate
find
findBinding
getBinding
setContentView
bindToAddedViews

二级页面的绑定【嵌套的子view中使用databinding】

二级页面:在布局文件中通过<include>标签引用的页面

一级页面传递数据

一级页面传递给二级页面。在一级页面中添加 xmlns:app命名空间,再通过命名空间xmlns:app引用布局变量将数据对象传递给二级页面。(如:app:book="@{book}"

fold
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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">
<data>
<variable
name="book"
type="com.michael.databindingdemo.Book"/>
</data>

<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
layout="@layout/layout_content"
app:book="@{book}"/>
</LinearLayout>
</layout>

layout_content.xml

fold
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8">
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="book"
type="com.michael.databindingdemo.Book"/>
</data>

<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:gravity="centenr"
android:layout_height="match_parent">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{book.author}"/>
</LinearLayout>
</layout>

BindingAdapter的原理

在 gradle 启用了 DataBinding 库后会自动生成绑定所需要的各种类,包括大量针对 UI 控件的、名为 XXXBindingAdapter 的类。这些类中包含各种静态方法,且这些静态方法前都有 @BindingAdapter 标签,标签中的别名对应 UI 控件在布局中的属性。

如:DataBinding 库针对 android:padding 属性所编写的代码

fold
1
2
3
4
5
6
7
8
9
public class ViewBindingApdapter{
//...
@BindingAdapter({"android:padding"})
public static void setPadding(View view, float paddingFloat){
final int padding = pixelsToDimensionPixelSize(paddingFloat);
view.setPadding(padding, padding, padding, padding);
}
//...
}

如:DataBinding 库为 TextView 生成的 TextViewBindingAdapter 类的部分源码。源码展示了 DataBinding 库针对 android:text 属性所编写的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TextViewBindingAdapter{
//..
@BindingApdater("android:text")
public static void setText(TextView view, CharSequence text){
final CharSequence oldText = view.getText();
if(text == oldText || (text == null && oldText.length() == 0)){
return;
}
if(text instanceOf Spanned){
if(text.equals(oldText)){
return;
}
} else if(!haveContentsChanged(text, oldText)){
return;
}
view.setText(text);
}
//...
}

DataBinding 库以静态方法的形式为 UI 控件的各个属性绑定了相应的代码。若工程师在 UI 控件的 属性中使用了布局表达式,那么当布局文件被渲染时,属性所绑定的静态方法就会被自动调用。

1
2
3
<TextView
android:padding="@{myPadding}"
andorid:text="@{book.title}"/>

上面这个代码,在TextView被渲染时,android:paddingandroid:text属性会分别自动调用ViewBindingAdapter.setPadding()TextViewBindingAdapter.setText()

自定义BindingAdapter

BindingAdapter 中的方法均为静态方法第一个参数是调用者本身。第二个参数是布局文件在调用该方法时传递过来的参数。
如:对 ImageView 写个自定义 BindingAdapter
app/build.gradle导入Picasso这个加载图片的库

1
dependencies{    implementataion 'com.squareup.picasso:picasso:2.71828'}

清单文件中添加权限

1
<uses-permission android:name="andorid.permission.INTERNET"/>

写自定义 BindingAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ImageViewBindingAdapter{
/**
* 加载网络图片的
*/
@BindingAdapter("image")
public static void setImage(ImageView imageView, String imageUrl){
if(!TextUtils.isEmpty(imageUrl)){
Picasso.get()
.loa(imageUrl)
.placeholder(R.drawable.ic_default)
.error(R.drawable.ic_error)
.inot(imageView);
} else {
imageView.setBackgrountColor(Color.DKGRAY);
}
}

/**
* 方法重载。加载本地图片
*/
@BindingAdapter("image")
public static void setImage(ImageView imageView, int imageResource){
imageView.setImageResource(imageResource);
}
}

然后在布局文件中调用这个 BindingAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<layout ...>
<data>
<variable
name="networkImage"
type="String"/>
<variable
name="localImage"
type="int"/>
</data>

<ImageView
app:image="@{networkImage}"/>
<ImageView
app:image="@{localImage}"/>
</layout>

在 Activity 中为布局变量赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
public class BindingAdapterActivity extends Activity{
private ActivityBindingAdapterBinding binding;

@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_binding_adapter);
//给变量赋值(网络图片的Url)
binding.setNetworkImage("http://****.jpg");
//给变量赋值(本地图片)
binding.setLocalImage(R.mipmap.local_image);
}
}

多参数重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ImageViewBindingAdapter{
@BindingAdapter(value = {"image", "defaultImageResource"}, requireAll = false)
public static void setImage(ImageView imageView, String imageUrl, int imageResource){
if(!TextUtils.isEmpty(imageUrl)){
Picasso.get()
.load(imageUrl)
.placeholder(R.drawable.ic_default)
.error(R.drawable.ic_error)
.into(imageView);
} else {
imageView.setImageResource(imageResource);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<layout ...>
<data>
<variable
name="networkImage"
type="String"/>
<variable
name="localImage"
type="int"/>
</data>

<ImageView
app:image="@{networkImage}"
app:defaultImageResource="@{localImage}"/>
</layout>

可选旧值

某些情况下希望在方法中得到该属性的旧值。如修改padding这个属性,想要得到修改前的padding,以防止方法重复调用。代码如下:

1
2
3
4
5
6
7
@BindingAdapter("padding")
public static void setPadding(View view, int oldPadding, int newPadding){
Log.e(TAG, "oldPading:" + oldPadding + " newPadding:"+newPadding);
if(oldPadding != newPadding){
view.setPadding(newPadding, newPadding, newPadding, newPadding);
}
}

注意:使用可选旧值时,方法中参数顺序需要先写旧值,后写新值。即oldPadding在前,newPadding在后。

例子演示可选旧值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<layout>
<data>
<variable
name="imagePadding"
type="String"/>
<variable
name="ClickHandler"
type="com.ab.demo.BindingAdapterActivity.ClickHandler"/>
</data>

<LinearLayout>
<ImageView
app:padding="@{imagePadding}"/>
<Button
android:onClick="@{ClickHandler.onClick}"
android:text="change padding"/>
</LinearLayout>
</layout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class BindingAdapterActivity extends Activity{
private ActivityBindingAdapterBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_binding_adapter);
binding.setImagePadding(40);
//旧值
binding.setClickHandler(new ClickHandler());
}

public class ClickHandler{
public void onClick(View view){
//新值
binding.setImagePadding(180);
}
}
}

双向绑定

不是继承 ViewModel,而是继承 BaseObservable。本质都是观察者模式。BaseObservable 是 DataBinding 库为了方便实现观察者模式而提供的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 写个 LoginModel 类
*/
public class LoginModel{
public String userName;
}

/**
* 写个双向绑定的 ViewModel
*/
public class TwoWayBindingViewModel extends BaseObservable{
private LoginModel loginModel;
public TwoWayBindingViewModel(){
loginModel = new LoginModel();
loginModel.userName = "Michael";
}

//添加 @Bindable 是告诉编译器希望对这个字段进行双向绑定。
@Bindable
public String getUserName(){
return loginModel.userName;
}

//用户编辑EditText中的内容时,会自动调用这个 setter 方法
public void setUserName(String userName){
if(userName != null && !userName.equals(loginModel.userName)){
loginModle.userName = userName;
//可以在此处理一些与业务相关的逻辑,例如保存userName字段
//通知观察者数据已经更新。观察者收到通知后,会对 Setter 方法进行调用。(所以此处添加旧值与新值的判断,如果不加判断就会循环调用)
notifyPropertyChanged(BR.userName);
}
}
}

notifyPropertyChanged()BaseObservable 类中的一个方法

1
2
3
4
5
6
7
8
9
10
11
/**
* 设置布局变量
*/
public class TwoWayBindingActivity extends Activity{
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
ActivityTwoWayBindingBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_two_way_binding);
binding.setViewModel(new TwoWayBindingViewModle());
}
}

完成双向绑定

1
2
3
4
5
6
7
8
9
<layout>
<data>
<variable
name="viewModel"
type="com.ab.demo.TwoWayBindingViewModel"/>
</data>
<EditText
android:text="@={viewModel.userName}"/>
</layout>

注意用的是 “@={}

运行程序,当修改EditText值时,类中的Setter会被自动调用,userName字段会随着EditText内容的变化而变化。

使用ObservableField优化双向绑定

ObservableField<T>将普通对象包装成一个观察对象。ObservableField可用于包装各种基本类型、集合数组类型和自定义类型的数据。

上面的例子用 ObservableField 进行优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TwoWayBindingViewModel{
private ObservableField<LoginModel> loginModelObservableField;
public TwoWayBindingViewModel(){
LoginModel loginModel = new LoginModel();
loginModel.userName = "Michael";
loginModelObservableField = new ObservableField<>();
loginModelObservableField.set(loginModel);
}

public String getUserName(){
return loginModelObservableField.get().userName;
}

public void setUserName(String userName){
loginModelObservableField.get().userName = userName;
}
}

其他的不变,只改了继承 BaseObservable 的ViewModel类

ObservableField与LiveData

ObservableField的使用方式与LiveData的很像。实际上,二者可以替换使用。

二者区别在于,LiveData与生命周期相关,通常 在ViewModel中使用,并且在页面中通过 observe() 方法对变化进行监听。而本示例中的双向绑定无须加入额外的代码,耦合度更低。

生成的绑定类

可以自定义绑定类名称

  • 默认情况下,绑定类是根据布局文件的名称生成的,以大写字母开头,移除下划线_,将后一个字母首字母大写并加后缀Binding组成。该类位于模块包下databinding包中。例如布局文件是contact_item.xml,模块包是com.example.my.app,则绑定类是com.example.my.app.databinding.ContactItemBinding

  • 我们在布局文件的<data>标签中添加class属性,可以指定绑定类的名称。(模块包路径是不变的)

    1
    2
    <data class="ContactItem">
    </data>

    也可以指定绑定类的在其他文件夹中生成(也可以只是加“句点”.ContactItem

    1
    2
    <data class="com.example.ContactItem">
    </data>

    则生成的绑定类com.example.databinding.ContactItem

RecyclerView的绑定机制

app/build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
android{
//启用databinding
defaultConfig{
buildFeatures {
dataBinding = true
}
}
}

dependencies{
//加入RecyclerView的依赖
implementation 'androidx.recyclerview:recyclerview:1.0.0'
}

编写RecyclerView的布局文件

1
2
3
4
5
6
7
8
9
10
11
<layout >
<data>
</data>

<LinearLayout>
<androidx.recyclerview.widget.RecyclerView
andorid:id="@+id/recyclerView"
andorid:layout_width="match_parent"
andorid:layout_height="wrap_content"/>
</LinearLayout>
</layout>

编写每个 Item 的布局文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<layout>
<data>
<variable
name="book"
type="com.ab.demo.model.Book"/>
</data>

<LinearLayout>
<ImageView
app:itemImage="@{book.image}"/>
<LinearLayout>
<TextView
android:text="@{book.title}"/>
<TextView
android:text="@{book.author}"/>
</LinearLayout>
</LinearLayout>
</layout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/**
* 实体类
*/
public class Book{
public String title;
public String author;
public String image;
public Book(String title, String author){
this.title = title;
this.author = author;
}
}


/**
* 自定义 BindingAdapter
*/
public class RecyclerViewImageBindingAdapter{
@BindingAdapter("itemImage")
public static void setImage(ImageView imageView, String imageUrl){
if(!TextUtils.isEmpty(imageUrl)){
Picasso.get()
.load(imageUrl)
.placeholder(R.drawable.ic_default)
.error(R.drawable.ic_error)
.into(imageView);
} else {
imageView.setBackgroundColor(Color.DKGRAY);
}
}
}


/**
* 数据绑定
*/
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.MyViewHolder>{
private List<Book> books;
public RecyclerViewAdapter(List<Book> books){
this.books = books;
}


@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType){
//通过 DataBindingUtil.inflate() 实例化布局文件
LayoutItemBinding layoutItemBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.layout_item, parent, false);
return new MyViewHolder(layoutItemBinding);
}

@Override
public void onBindViewHolder(MyViewHolder holder, int position){
//设置布局变量的值
Book book = books.get(position);
holder.layoutItemBinding.setBook(book);
}

@Override
public int getItemCount(){
return books.size();
}

class MyViewHolder extends RecyclerView.ViewHolder{
LayoutItemBinding layoutItemBinding;
public MyViewHolder(LayoutItemBinding itemView){
//getRoot() 返回的是布局文件的最外层 UI 视图
super(itemView.getRoot());
layoutItemBinding = itemView;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 模拟假数据
*/
public class RecyclerViewViewModel{
public List<Book> getBooks(){
List<Book> books = new ArrayList<>();
for(int i = 0; i < 100; i++){
Book book = new Book("Android高性能编程"+i, "叶坤"+i);
book.image = "https://*****.jpg";
books.add(book);
}
return books;
}
}


/**
* 在Activity中配置RecyclerView,并为其添加模拟数据
*/
public class RecyclerViewActivity extends Activity{
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
ActivityRecyclerviewBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_recyclerview);
binding.recyclerView.setLyaoutManager(new LinearLayoutManager(this));
binding.recyclerView.setHasFixedSize(true);
RecyclerViewAdapter adapter = new RecyclerViewAdapter(new RecyclerViewViewModel().getBooks());
binding.recyclerView.setAdapter(adapter);
}
}

自定义属性,xml给java传值

java中

1
2
3
public void setClick(int a){

}

xml中

1
app:click="@{}"

注意:

在java中,定义的方法要publicset开头且第二个单词首字母大写、要有入参

在xml中,自定义属性是小写的(去掉set的方法名),入参要用@{}