目錄
前言
DataBinding其實并不是一個新東西,15年 Google IO 大會就開始推了,一線大廠在比較早就開始使用了,隨著Jetpack架構組件的發展,使用DataBinding來進行我們的項目開發已經是大勢所趨
那么什么是 DataBinding 呢,字面就是幫我們實現視圖和數據綁定的工具,現在主流的前端框架(React、Vue)都用到了這種思想。它可以幫助我們實現,直接操作數據就可以自動改變視圖。而不是像過去那樣,首先要findViewById
拿到組件的引用,當數據發生變化后,我們還需要主動調用組件的相關方法(如setText)進行賦值,才能將數據的改變體現到視圖中。DataBinding就是幫助我們實現MVVM架構的最基本的技術支持
導入DataBinding
導入DataBinding非常簡單,只需要在app下的gradle文件中配置即可(如下所示)
//build.gradle(:app)
android {
....
buildFeatures {
dataBinding = true
}
}
可以這里有的人會好奇,導入Databinding竟然如此簡單,也不用添加任何依賴,這是為什么?
Android Studio中是依靠Gradle來管理項目的,在創建一個項目時,從開始創建一直到創建完畢,整個過程是需要執行很多個Gradle task的,這些task有很多是系統預先幫我們定義好的,比如build task,clean task等,DataBinding相關的task也是系統預先幫我們定義好的,但是默認情況下,DataBinding相關的task在task列表中是沒有的,因為我們沒有開啟dataBinding,但是一旦我們通過 buildFeatures{dataBinding= true}的方式開啟DataBinding之后,DataBinding相關的task就會出現在task列表中,每當我們執行編譯操作時,就會執行這些DataBinding Task, 這些task的作用就是檢查并生成相關DataBinding代碼
DataBinding基本使用
創建一個基本的xml文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>
使用快捷鍵option+enter可以快速生成binding layout
自動轉化后的格式,以根標記 layout
開頭,后跟 data
元素和 view
根元素
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>
</layout>
這里再推薦一款插件,可以簡化DataBinding的轉換操作并支持和ViewModel和與之關聯的layout文件的跳轉,可以提升開發時的效率,節省時間,感興趣的自己點下面的鏈接進一步了解
https://plugins.jetbrains.com/plugin/9271-databinding-support
現在先建立一個簡單的數據模型
public class News {
private String title;
private String content;
public News(String title, String content) {
this.title = title;
this.content = content;
}
public String getTitle() {
return title == null ? "" : title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content == null ? "" : content;
}
public void setContent(String content) {
this.content = content;
}
}
我們再寫一個簡單的布局
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="com.geekholt.databinding.bean.News" />
<variable
name="news"
type="com.geekholt.databinding.bean.News" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{news.title}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{news.content}" />
</LinearLayout>
</layout>
這里主要有兩點需要關注:
data
中的news
變量描述了可在此布局中使用的屬性布局中的表達式使用“
@{}
”語法寫入特性屬性中
數據綁定與整體更新
系統會為每個布局文件生成一個綁定類。默認情況下,類名稱基于布局文件的名稱,它會轉換為 Pascal 大小寫形式并在末尾添加 Binding 后綴。以上布局文件名為 activity_main.xml
,因此生成的對應類為 ActivityMainBinding
。此類包含從布局屬性(例如,news
變量)到布局視圖的所有綁定,并且知道如何為綁定表達式指定值
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding mainBinding = DataBindingUtil.setContentView(this,R.layout.activity_main);
News news = new News("Jetpack","一起學習DataBinding");
mainBinding.setNews(news);
}
這樣就完成了News
數據和activity_main.xml
的綁定,不僅省略了findViewById
的操作,同時也沒有針對單個的View
去進行賦值,當數據發生變化的時候,只需要重新調用mainBinding.setNews(news)
就可以讓與數據模型相關的控件都進行刷新
當然,我們也可以針對單個控件進行更新,我們通過一個點擊事件,觸發news.setContent("一起學習LiveData")
,去更改單個屬性
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
News news;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding mainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
news = new News("Jetpack", "一起學習DataBinding");
mainBinding.setNews(news);
}
public void submit(View view) {
news.setContent("一起學習LiveData");
}
}
這時候,發現頁面并沒有發生任何變化,這是為什么呢?
使用BaseObservable
更新單個屬性
BaseObservable
是一個基類,需要我們的數據 bean 繼承這個基類,然后給屬性的 get
方法
添加@Bindable
這個注解,然后在屬性的 set
方法中添加上 更新某個字段的方法notifyPropertyChanged
public class News extends BaseObservable {
private String title;
private String content;
public News(String title, String content) {
this.title = title;
this.content = content;
}
@Bindable
public String getTitle() {
return title == null ? "" : title;
}
public void setTitle(String title) {
this.title = title;
notifyPropertyChanged(BR.title);
}
@Bindable
public String getContent() {
return content == null ? "" : content;
}
public void setContent(String content) {
this.content = content;
notifyPropertyChanged(BR.content);
}
}
Obserable接口有一個自動添加和移除監聽器的機制,但是通知數據更新取決于開發者。為了使開發變得簡單,谷歌創建了BaseObserable這個基礎類來集成監聽器注冊機制。通過給getter方法添加Bindable注解,通知setter方法。
使用ObservableField
來更新單個屬性
ObservableFields
是一個對屬性添加 DataBinding 更新功能的代理類,針對不同的數據類型有不同類型的 ObservableFields
:ObservableBoolean
、 ObservableByte
ObservableChar
、ObservableShort
、ObservableInt
、ObservableLong
、ObservableFloat
、ObservableDouble
、 ObservableParcelable
等。
//News.java
public class News {
public ObservableField<String> title = new ObservableField<>();
public ObservableField<String> content = new ObservableField<>();
public News(String title, String content) {
this.title.set(title);
this.content.set(content);
}
}
//MainActivity.java
public void submit(View view) {
news.content.set("一起學習LiveData");
}
綁定事件與方法
我們也可以在視圖中為點擊事件綁定方法
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding mainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
mainBinding.setClick(new ClickProxy());
}
public class ClickProxy {
public void submit(String content) {
//todo something
}
}
}
通過android:onClick="@{()->click.submit()}"
進行方法綁定,且這種方式支持直接在xml中給方法傳參,如下所示
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="click"
type="com.geekholt.databinding.MainActivity.ClickProxy" />
<variable
name="content"
type="String" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={content}" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:onClick="@{()->click.submit(content)}" />
</LinearLayout>
</layout>
這里有一個細節提一下:
前面我們進行數據綁定用的都是
@{}
這種形式,這種綁定方式叫做單向數據綁定;而這里我們給EditText綁定數值用到了@={}
,中間多了一個等號,這種綁定方式稱之為雙向數據綁定,即當數據發送變化時,頁面也發生變化,當我們在EditText中輸入文字,頁面發生變化時,所對應的數據也會跟著變化
綁定適配器
自動查找屬性所對應的方法
目前為止,我們都是對系統控件(如TextView、Button、EditText)進行數據綁定,那如果自定義控件我們如何給它設定屬性并進行數據綁定呢?
public class CustomImageView extends AppCompatImageView {
public CustomImageView(Context context) {
super(context);
}
public CustomImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setImageUrl(String url) {
Glide.with(getContext()).load(url).into(this);
}
}
ImageView
本身并不能直接設置網絡url并加載,當我們在xml中app:imageUrl="@{imageUrl}"
時,數據綁定庫會自動幫我們在CustomImageView
中查找setImageUrl(string)
方法,并調用執行
<?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="imageUrl"
type="String" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.geekholt.databinding.CustomImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:imageUrl="@{imageUrl}" />
</LinearLayout>
</layout>
如果我們想在xml中調用方法名不是以set
開頭的方法怎么辦呢?
@BindingMethods 自定義方法名稱
Databinding庫給我們提供了@BindingMethods
注解,使用方式如下所示,主要包括type
、attribute
、method
三個部分
@BindingMethods({@BindingMethod(type = AppCompatImageView.class, attribute = "imageChange", method = "imageChange")})
public class CustomImageView extends AppCompatImageView {
public CustomImageView(Context context) {
super(context);
}
public CustomImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setImageUrl(String url) {
Glide.with(getContext()).load(url).into(this);
}
public void imageChange(String url) {
Toast.makeText(getContext(), "imageChange:" + url, Toast.LENGTH_SHORT).show();
}
}
<?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="imageUrl"
type="String" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.geekholt.databinding.CustomImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:imageChange="@{imageUrl}"
app:imageUrl="@{imageUrl}" />
</LinearLayout>
</layout>
這樣一來,每當imageView的url發生變化的時候,就會彈出一個Toast
@BindingAdapter 自定義邏輯
接下里要說的部分,將會是本文的重點,也是將DataBinding應用于企業級項目中的一個關鍵
很多人對DataBinding的第一印象,似乎都是在xml中編寫邏輯代碼,就像下面這樣:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
android:text="@{@string/nameFormat(firstName, lastName)}"
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
android:text="@{map[`firstName`]}"
需要注意的是這些都不代表著最佳實踐,只是說明有這樣的功能,支持這樣的用法。而且它有一個很大的弊端,相信很多剛入門DataBinding的人都遇到過找不到binding文件的錯,然后被勸退,原因無外乎就是表達式寫錯、variable的類路徑不對或者沒import相應的類(比如View)等等,都與復雜的表達式有關系,而DataBinding報錯也不太智能,有時不能準確定位,所以建議不要在xml里進行復雜的數據綁定
那我們如何來給xml “減負” 呢?
這時候就要用到@BindingAdapter
注解
上文中我們編寫的setImageUrl
方法,實際上不僅僅可以在自定義View中使用,我們可以通過@BindingAdapter
作用于ImageView
以及ImageView
的所有子類,如下所示:
public class CommonBindingAdapter {
@BindingAdapter(value = {"imageUrl", "placeHolder"})
public static void loadUrl(ImageView view, String url, Drawable placeHolder) {
Glide.with(view.getContext()).load(url).placeholder(placeHolder).into(view);
}
@BindingAdapter(value = {"visible"})
public static void visible(View view, boolean visible) {
view.setVisibility(visible ? View.VISIBLE : View.GONE);
}
}
如果 ImageView
對象同時使用了 imageUrl
和 placeHolder
,并且 imageUrl
是字符串,placeHolder
是 Drawable
,就會調用適配器。如果你希望在設置了任意屬性時調用適配器,則可以將適配器的可選 requireAll
標志設置為 false
注意:數據綁定庫在匹配時會忽略自定義命名空間
<?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="imageUrl"
type="String" />
<variable
name="placeholder"
type="android.graphics.drawable.Drawable" />
<variable
name="visible"
type="Boolean" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
imageUrl="@{imageUrl}"
placeHolder="@{placeholder}"
visible="@{visible}"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</layout>
看到這里,是不是覺得這個@BindingAdapter
非常的強大呢?將邏輯判斷盡可能的抽取到BIndingAdapter
中,我相信你的代碼可維護性會提高一個層次