Android DataBinding(MVVM設計模式)

      在網上找了很多MVVM的技術博客,大多講的雲裏霧裏不知所云,但是發現這一篇講的還不錯,適合入門,轉載一下

什麼是MVVM

說到DataBinding,就有必要先提起MVVM設計模式。
Model–View–ViewModel(MVVM) 是一個軟件架構設計模式,相比MVVM,大家對MVC或MVP可能會更加熟悉。

  • MVC:(VIew-Model-Controller)
    早期將VIew、Model、Controller代碼塊進行劃分,使得程序大部分分離,降低耦合。
  • MVP:(VIew-Model-Presenter)由於MVC中View和Model之間的依賴太強,導致Activity中的代碼過於臃腫。爲了他們可以絕對獨立的存在,慢慢演化出了MVP。在MVP中View並不直接使用Model,它們之間的通信是通過 Presenter (MVC中的Controller)來進行的。
  • MVVM:(Model–View–ViewModel)
    MVVM可以算是MVP的升級版,將 Presenter 改名爲 ViewModel。關鍵在於View和Model的雙向綁定,當View有用戶輸入後,ViewModel通知Model更新數據,同理Model數據更新後,ViewModel通知View更新。

Data Binding

在Google I/O 2015上,伴隨着Android M預覽版發佈的Data Binding兼容函數庫。
不知道要扯什麼了,還是直接上代碼,來看看Data Binding的魅力吧。

  • 環境要求

    Data Binding對使用的環境還是有一定要求的(這貨有點挑)
    Android Studio版本在1.3以上
    gradle的版本要在1.5.0-alpha1以上
    需要在Android SDK manager中下載Android Support repository

    然後在對應的Module的build.gradle中添加

    android {
      ....
      dataBinding {
          enabled =true
      }
    }

    Gradle需要升級版本的可以參考升級Gradle版本

  • 創建對象

    創建一個User類

    public class User {
      private String firstName;
      private String lastName;
    
      public User(String firstName, String lastName) {
          this.firstName = firstName;
          this.lastName = lastName;
      }
    
      public String getFirstName() {
          return this.firstName;
      }
    
      public String getLastName() {
          return this.lastName;
      }
    
      public void setLastName(String lastName) {
          this.lastName = lastName;
      }
    
      public void setFirstName(String firstName) {
          this.firstName = firstName;
      }
    }
  • 佈局

    在activity_main.xml中佈局

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
      <data>
          <import type="com.example.gavin.databindingtest.User"/>
          <variable
              name="user"
              type="User" />
      </data>
      <LinearLayout
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:orientation="vertical"
          android:gravity="center"
          >
          <TextView
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="@{user.firstName}"
              android:textSize="20sp" />
          <Button
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="@{user.lastName}"
              android:textSize="25sp" />
      </LinearLayout>
    </layout>

    這裏跟平時的佈局有點不同,最外層是layout,裏面分別是是data以及我們的佈局。
    data:聲明瞭需要用到的user對象,type用於是定路徑。
    可以在TextView中的看到android:text="@{user.firstName}", 這是什麼鬼,沒見過這麼寫的!!!
    (不急,繼續往下看)

  • 綁定數據

    看看下面的MainActivity

    public class MainActivity extends AppCompatActivity {
      private ActivityMainBinding binding;
      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
          User user = new User("Micheal", "Jack");
          binding.setUser(user);
      }
    }

    問我ActivityMainBinding哪來的?我怎麼知道...
    ActivityMainBinding是根據佈局文件的名字生成的,在後面加了Binding。
    運行下看看效果吧


    效果

有點懵逼了,就綁定了下而已,這些數據是怎麼顯示到界面上的。


懵逼

他是怎麼工作的?
原來Data Binding 在程序代碼正在編譯的時候,找到所有它需要的信息。然後通過語法來解析這些表達式,最後生成一個類。
通過反編譯我們可以看到,Data Binding爲我們生成了databinding包,以及ActivityMainBinding類(反編譯可以參考這裏



看看我們在onCreate中最後調用的binding.setUser(user),在ActivityMainBinding中可以看到這個方法。


setUser方法


我想就是這個 super.requestRebind()對數據進行了綁定,至於裏面怎麼實現的,有待進一步研究。

更多用法

上面只是用一個簡單的例子,展示了Data Binding的用法,如果想在實際項目中使用,可不是上面這例子可以搞定的。下面就來說說Data Bindig的更多用法。

  • 首先消除下大家對空指針的顧慮

    自動生成的 DataBinding 代碼會檢查null,避免出現NullPointerException。
    例如在表達式中@{user.phone}如果user == null 那麼會爲user.phone設置默認值null而不會導致程序崩潰(基本類型將賦予默認值如int爲0,引用類型都會賦值null)
  • 自定義DataBinding名

    如果不喜歡自動生成的Data Binding名,我們可以自己來定義
    <data class="MainBinding">
      ....
    </data>
    class對應的就是生成的Data Binding名
  • 導包

    跟Java中的用法相似,佈局文件中支持import的使用,原來的代碼是這樣

    <data>
       <variable name="user" type="com.example.gavin.databindingtest.User" />
    </data>

    使用import後可以寫成這樣:

      <data>
          <import type="com.example.gavin.databindingtest.User"/>
          <variable
              name="user"
              type="User" />
      </data>

    -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    遇到相同的類名的時候:

    <data>
      <import type="com.example.gavin.databindingtest.User" alias="User"/>
      <import type="com.example.gavin.mc.User" alias="mcUser"/>
      <variable name="user" type="User"/>
      <variable name="mcUser" type="mcUser"/>
    </data>

    使用alias設置別名,這樣user對應的就是com.example.gavin.databindingtest.User,mcUser就對應com.example.gavin.mc.User,然後

    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{user.firstName}"/>

    -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    當需要用到一些包時,在Java中可以自動導包,不過在佈局文件中就沒有這麼方便了。需要使用import導入這些包,才能使用。如,需要用到View的時候

    <data>
      <import type="android.view.View"/>
    </data>
    ...
    <TextView
    ...
    android:visibility="@{user.isStudent ? View.VISIBLE : View.GONE}"
    />

    注意只要是在Java中需要導入包的類,這邊都需要導入,如:Map、ArrayList等,不過java.lang包裏的類是可以不用導包的

  • 表達式

    在佈局中,不僅可以使用

    android:text="@{user.lastName}"

    還可以使用表達式如:

    三元運算

    在User中添加boolean類型的isStudent屬性,用來判斷是否爲學生。

    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text='@{user.isStudent? "Student": "Other"}'
    android:textSize="30sp"/>

    注意需要用到雙引號的時候,外層的雙引號改成單引號。
    還可以這樣用

    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="學生"
    android:visibility="@{user.isStudent ? View.VISIBLE : View.GONE}"
    android:textSize="30sp"/>

    這裏用到的View需要在data中聲明

    <data>
       <import type="android.view.View"/>
    </data>

    注意:android:visibility="@{user.isStudent ? View.VISIBLE : View.GONE}",可能會被標記成紅色,不用管它編譯會通過的

    ??

    除了常用的操作法,另外還提供了一個 null 的合併運算符號 ??,這是一個三目運算符的簡便寫法。

    contact.lastName ?? contact.name

    相當於

    contact.lastName != null ? contact.lastName : contact.name

    所支持的操作符如下:
    數學運算符 + - / * %
    字符串拼接 +
    邏輯運算 && ||
    二進制運算 & | ^
    一元運算符 + - ! ~
    位運算符 >> >>> <<
    比較運算符 == > < >= <=
    instanceof
    Grouping ()
    文字 - character, String, numeric, null
    類型轉換 cast
    方法調用 methods call
    字段使用 field access
    數組使用 [] Arrary access
    三元運算符 ? :

  • 顯示圖片

    除了文字的設置,網絡圖片的顯示也是我們常用的。來看看Data Binding是怎麼實現圖片的加載的。
    首先要提到BindingAdapter註解,這裏創建了一個類,裏面有顯示圖片的方法。

    public class ImageUtil {
      /**
       * 使用ImageLoader顯示圖片
       * @param imageView
       * @param url
       */
      @BindingAdapter({"bind:image"})
      public static void imageLoader(ImageView imageView, String url) {
          ImageLoader.getInstance().displayImage(url, imageView);
      }
    }

    (這方法必須是public static的,否則會報錯)
    這裏只用了bind聲明瞭一個image自定義屬性,等下在佈局中會用到。
    這個類中只有一個靜態方法imageLoader,裏面有兩參數,一個是需要設置圖片的view,另一個是對應的Url,這裏使用了ImageLoader庫加載圖片。
    看看吧它的佈局是什麼樣的吧

    <?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"
          android:gravity="center"
          >
          <ImageView
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              app:image = "@{imageUrl}"/>
      </LinearLayout>
    </layout>

    最後在MainActivity中綁定下數據就可以了

    binding.setImageUrl(
      "http://115.159.198.162:3000/posts/57355a92d9ca741017a28375/1467250338739.jpg");

    哇靠!!!就這樣?我都沒看出來它是怎麼設置這些圖片的。
    不管了,先看看效果。(其中的原理以後慢慢嘮,這裏就負責說明怎麼使用,這篇已經夠長了,不想再寫了)


    看個美女壓壓驚

    使用BindingAdapter的時候,我這還出現了這樣的提示,不過不影響運行。不知道你們會不會...



    【已解決】
    感謝顏路同學指出@BindingAdapter({"bind:image"}) 改成
@BindingAdapter({"image"}) 就不會有警告了

  • 點擊事件

    在MainActivity中聲明方法:

    //參數View必須有,必須是public,參數View不能改成對應的控件,只能是View,否則編譯不通過
    public void onClick(View view) {
      Toast.makeText(this,"點擊事件", Toast.LENGTH_LONG).show();
    }

    佈局中:

      <data>
        ...
          <variable
          name="mainActivity"
          type="com.example.gavin.databindingtest.MainActivity"/>
      </data>
      ....
          <Button
              ...
              android:onClick="@{mainActivity.onClick}"
              />

    最後記得在MainActivity中調用

    binding.setMainActivity(this);

    (發現:佈局文件中,variable中的name,在binding中都會生成一個對應的set方法,如:setMainActivity。有set方法,那就應該有get方法,試試getMainActivity,還真有)
    運行下看看效果


    點擊事件


    當然如果你不想吧點擊事件寫在MainActivity中,你把它單獨寫在一個類裏面:

    public class MyHandler {
      public void onClick(View view) {
          Toast.makeText(view.getContext(), "點擊事件", Toast.LENGTH_LONG).show();
      }
    }
      <data>
        ...
          <variable
          name="handle"
          type="com.example.gavin.databindingtest.MyHandler"/>
      </data>
      ....
          <Button
              ...
              android:onClick="@{handle.onClick}"
              />
      </data>

    在MainActivity調用

    binding.setHandle(new MyHandler());
  • 調用Activity中的變量

    上面看到它調用MainActivity中的onClick方法,那麼可以調用MainActivity中的屬性嗎?
    在MainActivity中定義mName,
    public static String mName = "MM";
    佈局中
      <data>
          ...
          <variable
              name="mainActivity"
              type="com.example.gavin.databindingtest.MainActivity"/>
      </data>
          <Button
              ...
              android:text="@{mainActivity.mName}"
              />
    注意這個變量必須是public static
  • 數據改變時更新UI

    當數據發生變化時,我們可以這樣更新UI
      private ActivityMainBinding binding;
      private User user;
      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
          user = new User("Micheal", "Jack");
          binding.setUser(user);
          binding.setHandle(new MyHandler());
          delay();
      }
      /**
       * 兩秒後改變firstName
       */
      private void delay() {
          new Handler().postDelayed(new Runnable() {
              @Override
              public void run() {
                  user.setFirstName("Com");
                  binding.setUser(user);
              }
          }, 2000);
      }
    看看調用的這個setUser是什麼:

    setUser

    從反編譯的代碼中可以看出,setUser方法中重新綁定了數據。
    看下效果

效果
  • BaseObservable

    使用上面的代碼實現了UI的更新你就滿足了?其實官方爲我們提供了更加簡便的方式,使User繼承BaseObservable,代碼如下

    public class User extends BaseObservable {
      private String firstName;
      private String lastName;
    
      public User(String firstName, String lastName) {
          this.firstName = firstName;
          this.lastName = lastName;
      }
      @Bindable
      public String getFirstName() {
          return this.firstName;
      }
      @Bindable
      public String getLastName() {
          return this.lastName;
      }
    
      public void setLastName(String lastName) {
          this.lastName = lastName;
          notifyPropertyChanged(BR.lastName);
      }
    
      public void setFirstName(String firstName) {
          this.firstName = firstName;
          notifyPropertyChanged(BR.firstName);
      }
    }

    只要user發生變化,就能達到改變UI的效果。在MainActivity中只要調用以下代碼

    user.setFirstName("Com");

    有了BaseObservable就夠了?不不不,我比較懶,不想寫那麼多@Bindable和notifyPropertyChanged。萬一裏面有幾十個屬性,那不寫哭起來?而且還有可能寫丟了。
    Data Binding的開發者貼心得爲我們準備了一系列的ObservableField,包括: ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat,ObservableDouble, 以及 ObservableParcelable看看它們的用法
    ObservableField的使用
    1、創建User2

    public class User2 {
      public final ObservableField<String> firstName = new ObservableField<>();
      public final ObservableField<String> lastName = new ObservableField<>();
      public final ObservableInt age = new ObservableInt();
      public final ObservableBoolean isStudent = new ObservableBoolean();
    }

    這類裏面沒有Get/Set。
    2、佈局文件

          <TextView
              ...
              android:text="@{user2.firstName}" />
          <TextView
              ...
              android:text="@{user2.lastName}" />
          <TextView
              ...
              android:text="@{String.valueOf(user2.age)}"
               />

    3、MainActivity中

          mUser2 = new User2();
          binding.setUser2(mUser2);
          mUser2.firstName.set("Mr");
          mUser2.lastName.set("Bean");
          mUser2.age.set(20);
          mUser2.isStudent.set(false);

    這裏new了一個User2對象後,直接就綁定了。之後只要mUser2中的數據發生變化,UI也會隨之更新。
    除了這幾個Map跟List也是必不可少的,Data Binding爲我們提供了 ObservableArrayMapObservableArrayList
    ObservableArrayMap的使用

    ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
    user.put("firstName", "Google");
    user.put("lastName", "Inc.");
    user.put("age", 17);
    <data>
      <import type="android.databinding.ObservableMap"/>
      <variable name="user" type="ObservableMap<String, Object>"/>
    </data>
    …
    <TextView
     android:text='@{user["lastName"]}'
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"/>
    <TextView
     android:text='@{String.valueOf(1 + (Integer)user["age"])}'
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"/>

    ObservableArrayList的使用

    ObservableArrayList<Object> user = new ObservableArrayList<>();
    user.add("Google");
    user.add("Inc.");
    user.add(17);
    <data>
      <import type="android.databinding.ObservableList"/>
      <import type="com.example.my.app.Fields"/>
      <variable name="user" type="ObservableList<Object>"/>
    </data><TextView
     android:text='@{user[Fields.LAST_NAME]}'
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"/>
    <TextView
     android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"/>

    在佈局中使用中文時,編譯無法通過。

    android:text='@{user2.isStudent?"學生":"非學生"}'

    感謝呂檀溪同學的解決方案:
    這是java環境的問題,在系統環境變量中增加一個變量,變量名爲: JAVA_TOOL_OPTIONS, 變量值爲:-Dfile.encoding=UTF-8,保存。要重啓一次電腦,中文就解決了,但是在某些地方,編譯的時候控制檯會出現部分亂

  • 在RecyclerView或ListView中使用

    前面說了那麼多基礎的用法,可還是不能達到我們的需求。幾乎在每個app中都有列表的存在,RecyclerView或ListView,從上面所說的似乎還看不出Data Binding在RecyclerView或ListView中是否也能起作用。(用屁股想也知道,Google的開發團對怎麼可能會犯這麼低級的錯誤)。下面以RecyclerView爲例子:
    1、直接看Item的佈局(user_item.xml):

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
      <data>
          <variable
              name="user2"
              type="com.example.gavin.databindingtest.User2" />
      </data>
      <LinearLayout
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:padding="10dp"
          android:orientation="horizontal">
          <TextView
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="@{user2.firstName}"/>
          <TextView
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="·"/>
          <TextView
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="@{user2.lastName}"/>
          <View
              android:layout_width="0dp"
              android:layout_height="0dp"
              android:layout_weight="1"/>
          <TextView
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text='@{user2.age+""}'/>
      </LinearLayout>
    </layout>

    2、RecyclerView的數據綁定是在Adapter中完成的,下面看看Adapter(這裏使用了一個Adapter,如果你在使用的時候發現RecyclerView的動畫沒了,去這裏尋找答案)

    public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyHolder> {
    
      private List<User2> mData = new ArrayList<>();
    
      public MyAdapter(List<User2> data) {
          this.mData = data;
      }
    
      @Override
      public MyHolder onCreateViewHolder(ViewGroup parent, int viewType) {
          return MyHolder.create(LayoutInflater.from(parent.getContext()), parent);
      }
    
      @Override
      public void onBindViewHolder(MyHolder holder, int position) {
          holder.bindTo(mData.get(position));
      }
    
      @Override
      public int getItemCount() {
          if (mData == null)
              return 0;
          return mData.size();
      }
    
      static class MyHolder extends RecyclerView.ViewHolder {
          private UserItemBinding mBinding;
    
          static MyHolder create(LayoutInflater inflater, ViewGroup parent) {
              UserItemBinding binding = UserItemBinding.inflate(inflater, parent, false);
              return new MyHolder(binding);
          }
    
          private MyHolder(UserItemBinding binding) {
              super(binding.getRoot());
              this.mBinding = binding;
          }
    
          public void bindTo(User2 user) {
              mBinding.setUser2(user);
              mBinding.executePendingBindings();
          }
    
      }
    }

    3、最後在佈局和MainActivity中的使用跟平時的用法一樣
    佈局中加入RecyclerView:

          <android.support.v7.widget.RecyclerView
              android:id="@+id/recycler_view"
              android:layout_width="match_parent"
              android:layout_height="match_parent"/>

    MainActivity中:

          List<User2> data = new ArrayList<>();
          for (int i = 0; i < 20; i++) {
              User2 user2 = new User2();
              user2.age.set(30);
              user2.firstName.set("Micheal " + i);
              user2.lastName.set("Jack " + i);
              data.add(user2);
          }
          RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
          LinearLayoutManager layoutManager = new LinearLayoutManager(
                  this, LinearLayoutManager.VERTICAL, false);
          recyclerView.setLayoutManager(layoutManager);
          recyclerView.setAdapter(new MyAdapter(data));

    這樣就可以了。
    不過,在自動生成的ActivityMainBinding中,我們可以看到根據RecyclerView的id,會自動生成一個recyclerView。



    所以在MainActivity中,我們可以不用findViewById,直接使用binding.recyclerView。

          LinearLayoutManager layoutManager = new LinearLayoutManager(
                  this, LinearLayoutManager.VERTICAL, false);
          binding.recyclerView.setLayoutManager(layoutManager);
          binding.recyclerView.setAdapter(new MyAdapter(data));

    來看看效果吧:


    RecyclerView

Tips:

  • tip1:若需要顯示int類型,需要加上"":如

    user.age爲int類型,需要這樣用

    <TextView   
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text='@{""+user.age}'/>

    或者

    <TextView   
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{String.valueOf(user.age)}"/>
  • tip2:不建議新手使用,出現錯誤的時候根據提示,不容易找到出錯位置。(是根本找不到...)


文/帶心情去旅行(簡書作者)
原文鏈接:http://www.jianshu.com/p/5dcdc5798d85
著作權歸作者所有,轉載請聯繫作者獲得授權,並標註“簡書作者”。

發佈了34 篇原創文章 · 獲贊 81 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章