從零開始搭建MVVM架構(1)——DataBinding

  • 2019 年 11 月 5 日
  • 筆記

在真正接觸並使用MVVM架構的時候,整個人都不好了。因為個人覺得,MVVM相對於MVC、MVP學習難度比較大,設計的知識點不是一點半點。所以想慢慢記錄下自己的成長。如有錯誤希望指正。

從零開始搭建MVVM架構系列文章(持續更新): Android從零開始搭建MVVM架構(1)————DataBinding Android從零開始搭建MVVM架構(2)————ViewModel Android從零開始搭建MVVM架構(3)————LiveData Android從零開始搭建MVVM架構(4)————Room(從入門到進階) Android從零開始搭建MVVM架構(5)————Lifecycles Android從零開始搭建MVVM架構(6)————使用玩Android API帶你搭建MVVM框架(初級篇) Android從零開始搭建MVVM架構(7) ———— 使用玩Android API帶你搭建MVVM框架(終極篇)

首先看一張圖,(這裡就是一些人口中所說的「AAC框架」)

我說下我的理解:AAC(Android Architecture Components) :實際上是android官方提供的一系列組件,用來實現MVVM架構的。 這裡提下 lifecycles:就是處理UI介面的生命周期,在26版本以後的Support庫中,AppCompatActivity和SupportActivity中都實現了LifecycleOwner,內部已經對UI介面的生命周期做了處理了。我們可以直接程式碼點進去,如下

好了,回到DataBinding。這是MVVM框架的第一步。DataBinding是studio自帶的。只需要在我們app build.gradle的android標籤下加上:

dataBinding {          enabled = true      }

一、初始DataBinding(建議使用studio3.5,好用)

DataBinding最厲害的功能是可以將我們的數據和view綁定。這句話體現不出來,那可以說成,DataBinding可以將數據和xml綁定。而且還支援雙向綁定:意思你改了bean里的數據,他會自動改變view里顯示的數據。你改了xml里的數據,如editText里的數據,他會自動改變bean里的數據。 在android標籤加上後,來到我們的xml布局下,對著xml的第一行,按下Alt + Enter,選擇 「Convert to data binding layout」,就可以生成DataBinding的布局規則

生成如下,我這裡改成了RelativeLayout布局

<?xml version="1.0" encoding="utf-8"?>  <layout xmlns:android="http://schemas.android.com/apk/res/android">        <data>        </data>        <RelativeLayout          android:layout_width="match_parent"          android:layout_height="match_parent">        </RelativeLayout>  </layout>

Activity里需要綁定下布局,框架自動會生成DataBinding類,類名是:xml名稱+Binding。

    @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          //setContentView(R.layout.activity_main);          //寫上這句後,上面的程式碼可以注釋哦          binding = DataBindingUtil.setContentView(this, R.layout.activity_main);      }

此外還可以自定義生成類名,這裡我就自定義了類名MyBinding

    <data class="MyBinding">        </data>

1.1、設置數據及點擊事件

在xml里的<data>標籤里加上要設置的數據,我這裡有一個String,有一個OnClickListener:

  • <variable>標籤里的 name相當於數據引用
  • type 是數據類型,也可以理解為包名.類名。常用數據類型,直接寫類型
  • 在設置值的時候的時候用@{value},value就是<variable>里的數據引用
<?xml version="1.0" encoding="utf-8"?>  <layout xmlns:android="http://schemas.android.com/apk/res/android"      xmlns:tools="http://schemas.android.com/tools">        <data>            <variable              name="textStr"              type="String" />            <variable              name="onClickListener"              type="android.view.View.OnClickListener" />        </data>        <RelativeLayout          android:layout_width="match_parent"          android:layout_height="match_parent"          >            <TextView              android:id="@+id/txt"              android:layout_width="wrap_content"              android:layout_height="wrap_content"              android:text="@{textStr}"              android:onClick="@{onClickListener}"              />        </RelativeLayout>  </layout>

那麼在Activity設置數據和設置點擊事件(意思設置了variable標籤後DataBinding會自動生成get和set方法。studio3.5後只要xml寫上,IDE會自動生成,低版本可能需要Make Project下)。如下就實現了一個設置數據,和設置點擊事件。

通過以下程式碼可以看到,只要綁定下布局,通過Binding對象,可以做任何事。從此不再使用findViewById,亦或是butterKnife。

public class BaseUseActivity extends AppCompatActivity implements View.OnClickListener {      private ActivityBaseuseBinding baseuseBinding;        @Override      protected void onCreate(@Nullable Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.activity_baseuse);          baseuseBinding = DataBindingUtil.setContentView(this, R.layout.activity_baseuse);          baseuseBinding.setTextStr("這裡就能設置數據");          baseuseBinding.setOnClickListener(this);      }        @Override      public void onClick(View v) {          baseuseBinding.txt.setText("點擊設置的數據");      }  }

這裡還能調用類里的方法,且需要特別注意,在使用DataBinding的時候,包名一定是小寫,不然找不到包名,假設我們這裡定義個類,然後調用類里的方法。

public class OnClickUtil {        public void onClickWithMe(View view) {          Toast.makeText(view.getContext(), "調用類里的方法", Toast.LENGTH_SHORT).show();      }  }

其他步驟都一樣,唯一不同的是,調用類里方法的寫法不同。假設button點擊調用。用::表示調用,後面接的是方法名。

            <Button              ...              android:onClick="@{onClickUtil::onClickWithMe}"              />

1.2、<import>和別名alias的使用

這裡我們先定義同名的2個類User。放在不同包里。

public class User {      private String name;      private int age;        public User(String name, int age) {          this.name = name;          this.age = age;      }  }

之前我們的<data>標籤就可以用<import>。

    <data>          <import type="com.lihang.databindinglover.bean.User"/>            <variable              name="use_first"              type="User" />      </data>

<import>的用法是在同一個xml里需要用到多次User的時候,type類型只需要寫<import>的類名就可以代表了,就不需要總是寫包名.類型。但這個時候也就出現2個同名不同包的類是需要用到alias別名,不然類名重複了。

<data>          <import type="com.lihang.databindinglover.bean.User"/>            <import              alias="loverUser"              type="com.lihang.databindinglover.User"/>            <variable              name="user_first"              type="User" />            <variable              name="user_second"              type="loverUser" />      </data>

Activity里的使用都是非常簡單的,如果有不明白,稍後放出鏈接。

這裡還有特殊功能,比如我們再布局預覽頁面。通常會使用 tools:text="中間的"來預覽布局,這個時候可以通過

            <TextView              android:layout_width="wrap_content"              android:layout_height="wrap_content"              android:text="@{user.name,default = 預覽文字}"              />

使用default的時候,即使是studio3.5里,也不提示,不過不影響。還有一點,在DataBinding里,已經處理了null。所以這個時候你在Activity里給user設置為null也不會崩潰。

二、DataBinding在Fragment和RecyclerView里的使用

在Fragment的使用和Activity里的使用一樣。獲取根目錄的方式如下。

//注意獲取根布局是  View view = activityAlisBinding.getRoot();

這裡重點介紹下再recyclerView里的用法。我們以前是不是寫ViewHolder寫的煩了?用上了DataBinding後,這麼告訴你一個ViewHolder就能搞定一切需要的ViewHolder

先看下我們的唯一的ViewHolder。首先提下,自動生成的Binding的父類都是ViewDataBinding。我是把ViewHolder單獨拉出來了。這樣大家都能用:

public class NewViewHolder extends RecyclerView.ViewHolder {      public ViewDataBinding binding;        public NewViewHolder(ViewDataBinding binding) {          super(binding.getRoot());          this.binding = binding;      }  }

在Adapter里只需要職業

 @Override      public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {          ItemNewOrderBinding binding = DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.item_new_order, viewGroup, false);          return new NewViewHolder(binding);      }        @Override      public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {          NewViewHolder newViewHolder = (NewViewHolder) viewHolder;          //如果是多布局,那麼對binding進行一個 instansof的一個判斷就好。這樣我們的ViewHolder永遠只需要一個          ItemNewOrderBinding binding = (ItemNewOrderBinding) newViewHolder.binding;          binding.txtName.setText("這樣就能使用了!!");      }

三、單向數據綁定

單向綁定可以理解為,改變了bean對象里的數據,就會自動改變我們xml的顯示。這裡涉及到3個類: BaseObservable、ObservableField、ObservableCollection。看這個名字就知道有點類似觀察者模式

3.1、BaseObservable

首先我們定義個以Dog類

public class Dog extends BaseObservable {        //如果是public修飾的,直接用@Bindable      @Bindable      public String name;      //如果是private修飾的,則在get方法使用@Bindable      private String color;          public void setDataOnlyName(String name, String color) {          this.name = name;          this.color = color;          //只刷name欄位          notifyPropertyChanged(com.lihang.databindinglover.BR.name);      }        public void setDataAll(String name, String color) {          this.name = name;          this.color = color;          //刷新全部欄位          notifyChange();      }      ...//省略部分程式碼  }

這裡我同事改變了name和color的顏色,說明

  • bean對象需要繼承 BaseObservable
  • @Bindable 標註用來表示哪個欄位需要單向綁定。public修飾的可以直接用@Bindable綁定。private修飾的需要在get()方法上用@Bindable標註
  • notifyChange();刷新所有欄位,notifyPropertyChanged(com.lihang.databindinglover.BR.name);刷新單個欄位。注意這裡說的刷新全是被@Bindable綁定的。如果BR.name出不來。建議build下項目
  • 還有不明白的可以在末尾鏈接demo看:單向數據綁定 — BaseObservable.

繼承了BaseObservable的bean對象,還可以監聽刷新了哪

dog.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {              @Override              public void onPropertyChanged(Observable sender, int propertyId) {                  if (propertyId == com.lihang.databindinglover.BR.name) {                      Log.e("看看刷新了哪", "刷新了name");                  } else if (propertyId == com.lihang.databindinglover.BR._all) {                      Log.e("看看刷新了哪", "全部全部");                  } else {                      Log.e("看看刷新了哪", "未知錯誤~");                  }              }          });

3.2、ObservableField

其實這個ObservableField就是對BaseObservable的簡化,不用繼承,不用主動調刷新程式碼。 這個時候我們頂一個Human類

public class Human {      //這裡必須是常量,ObservableField<參數類型>      //其實寫上了下面一句,就是BaseObservable,set,get, @Bindable,刷新都封裝了。直接看構造方法      public final ObservableField<String> name = new ObservableField<>();      //其中也封裝了基本數據類型:ObservableInt等      public final ObservableInt age = new ObservableInt();        public Human(String name,int age){          this.name.set(name);          this.age.set(age);      }    }

Activity和xml里的操作和之前的一樣,改變數據,自動改變xml只需要:

//簡直太方便了吧  human.name.set("玉璣子");  human.age.set(15);

3.3、ObservableCollection

一看就是集合,這裡和我們常用的 List Map一樣。只不過這裡的ObservableList、ObservableMap是封裝好的。當我們改變集合里的數據時。xml也會改變。唯一要注意的是,在xml里引用這些集合的時候<類型>,這些符號,會影響xml格式所以要轉義。用< 代表<;用>代表>(這些轉義符,同樣支援Mark Down);想了解更多可自行百度 DataBinding轉義符。

<layout xmlns:android="http://schemas.android.com/apk/res/android">        <data>            <variable              name="list"              type="androidx.databinding.ObservableList&lt;String&gt;" />            <variable              name="map"              type="androidx.databinding.ObservableMap&lt;String,String&gt;" />            <variable              name="index"              type="int" />            <variable              name="key"              type="String" />      </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="@{list[index],default = 哈哈}" />            <TextView              android:layout_width="wrap_content"              android:layout_height="wrap_content"              android:text="@{map[key],default = 呵呵}" />        </LinearLayout>  </layout>

這裡遇到一個坑,就是你的default = 「默認值」 這個默認值最好不是data里的引用。不然會報錯哦。這裡我們帶入index = 0 帶入,把key = name。代入,然後動態改變,集合里這2個值:

@Override      public void onClick(View v) {          int randowInt = new Random().nextInt(100);          switch (v.getId()){              case R.id.btn_index:                  //改變list的第一項                  list.add(0,"list的值" + randowInt);                  break;              case R.id.btn_key:                  map.put("name","map的值" + randowInt);                  break;          }      }

四、雙向數據綁定

意思就是你改變bean對象里的值,他會主動改變xml的顯示,改變xml的里的值,他會把bean對象里的屬性改變了。 這裡我們用1個TextView顯示數據;用1個EditTextView綁定bean對象,再用1個Button可以動態查詢bean對象里的屬性值

<layout xmlns:android="http://schemas.android.com/apk/res/android">        <data>          <variable              name="human"              type="com.lihang.databindinglover.bean.Human" />      </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="@{human.name}" />            <EditText              android:layout_width="match_parent"              android:layout_height="60dp"              android:layout_marginTop="20dp"              android:text="@={human.name}" />            <Button              android:id="@+id/btn_search"              android:layout_marginTop="60dp"              android:layout_width="wrap_content"              android:layout_height="wrap_content"              android:text="動態查詢屬性"              />      </LinearLayout>  </layout>

bean對象綁定xml顯示:單向綁定是@{屬性值},雙向綁定則是@={屬性值},效果如下:

五、在include 和 viewStub中使用

5.1 在include中使用。

include的布局如下:

<layout xmlns:android="http://schemas.android.com/apk/res/android">        <data>            <variable              name="user"              type="com.lihang.databindinglover.bean.User" />        </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="@{user.name}" />        </LinearLayout>  </layout>

Activity里引用include這樣:

<layout xmlns:android="http://schemas.android.com/apk/res/android"      xmlns:app="http://schemas.android.com/apk/res-auto">        <data>            <variable              name="user"              type="com.lihang.databindinglover.bean.User" />        </data>        <RelativeLayout          android:layout_width="match_parent"          android:layout_height="match_parent">            <include              layout="@layout/include_item"              app:user="@{user}" />        </RelativeLayout>  </layout>

注意:app:user="@{user}"。第一個user是include里name的引用。第二user是當前傳入的值。

5.2、viewStub中的使用

簡單介紹下viewStub:被viewStub包裹的。即使頁面顯示的時候,被包裹的布局也不會載入,除非調用inflate。這樣算是對布局卡頓的優化了。include則算是程式碼里的布局優化。

直接放Activity布局了。被包裹的布局和上面的include一樣

<layout xmlns:android="http://schemas.android.com/apk/res/android"      xmlns:app="http://schemas.android.com/apk/res-auto">        <data>            <variable              name="user"              type="com.lihang.databindinglover.bean.User" />      </data>        <RelativeLayout          android:layout_width="match_parent"          android:layout_height="match_parent">            <ViewStub              android:id="@+id/view_stub"              android:layout_width="wrap_content"              android:layout_height="wrap_content"              android:layout_centerInParent="true"              android:layout="@layout/viewstub_layout"              app:user="@{user}" />        </RelativeLayout>  </layout>

activity里:

    @Override      protected void onCreate(@Nullable Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.activity_viewstub);          binding = DataBindingUtil.setContentView(this, R.layout.activity_viewstub);          User user = new User("我愛學習", 18);          binding.viewStub.getViewStub().inflate();          binding.setUser(user);      }

六、@BindingAdapter的使用。

這裡比較重要的用法是,當我們的imageView需要載入網路url時,假如用的是glide去載入,這個時候就需要使用@BindingAdapter。 這個需要一個輔助類:new一個輔助類後,xml里就可以使用了。有點像Dagger2

public class DataBindingHelper {      //用@BindingAdapter標註,有點類似自定義屬性,後面是屬性名,方法體類似得到屬性值後去做的事情。      //第一個參數:是當前的控制項類型,其實也可以寫成View,但是要載入還是要判斷是否是imageView      //第二個參數:是網路載入的url。      @BindingAdapter("imageWithGlide")      public static void loadImage(ImageView imageView, String url) {          Glide.with(imageView).load(url)                  .placeholder(R.mipmap.ic_launcher)                  .error(R.mipmap.ic_launcher)                  .transition(withCrossFade())                  .centerCrop()                  .into(imageView);      }        //@BindingAdapter還能修改系統屬性值,這是修改textView的屬性,意思只要使用DataBinding給textView設置setText值的,      //都會加上後面這段 " - 我是通過方法加的"      //我這裡先注釋掉了。不然整個項目的textView都會加上整個,如果要測試,可以打開      //@BindingAdapter("android:text")      //public static void setText(TextView textView, String testStr) {      //    textView.setText(testStr + " - 我是通過方法加的");      //}  }

我們的xml就是專業:

<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="testStr"              type="String" />        </data>          <RelativeLayout          android:layout_width="match_parent"          android:layout_height="match_parent">            <TextView              android:layout_width="wrap_content"              android:layout_height="wrap_content"              android:layout_below="@+id/img"              android:layout_centerHorizontal="true"              android:layout_marginTop="15dp"              android:text="@{testStr}" />            <ImageView              android:id="@+id/img"              android:layout_width="100dp"              android:layout_height="100dp"              android:layout_centerInParent="true"              app:imageWithGlide="@{imageUrl}" />        </RelativeLayout>  </layout>

activity里就是設置imageUrl和testStr的值。太簡單就不寫了。

七、DataBinding布局裡支援的語法

支援的語法:

  • 算術 + – / * %
  • 字元串合併 +
  • 邏輯 && ||
  • 二元 & | ^
  • 一元 + – ! ~
  • 移位 >> >>> <<
  • 比較 == > < >= <=
  • Instanceof
  • Grouping ()
  • character, String, numeric, null
  • Cast
  • 方法調用
  • Field 訪問
  • Array 訪問 []
  • 三元 ?:

不支援的語法:

  • this
  • super
  • new
  • 顯示泛型調用