1. 程式人生 > >Android-MVVM架構-Data Binding的使用

Android-MVVM架構-Data Binding的使用

專案整體效果:

這裡寫圖片描述

什麼是MVVM , 為什麼需要MVVM?

MVVM是Model-View-ViewModel的簡寫. 它是有三個部分組成:Model、View、ViewModel。

Model:資料模型層。包含業務邏輯和校驗邏輯。

View:螢幕上顯示的UI介面(layout、views)。

ViewModel:View和Model之間的連結橋樑,處理檢視邏輯。

MVVM架構圖如下:

Alt text

MVVM架構通過ViewModel隔離了UI層和業務邏輯層,降低程式的耦合度。通過DataBinding實現View和ViewModel之間的繫結。

Android App 中MVC的不足

一般來說,我們開發Android App是基於MVC,由於MVC的普及和快速開發的特點,一個app從0開發一般都是基於MVC的。

Activity、Fragment相當於C (Controller), 佈局相當於V(View), 資料層相當於M(Model)

隨著業務的增長,Controller裡的程式碼會越來越臃腫,因為它不只要負責業務邏輯,還要控制View的展示。也就是說Activity、Fragment雜糅了Controller和View,耦合變大。並不能算作真正意義上的MVC。

編寫程式碼基本的過程是這樣的,在Activity、Fragment中初始化Views,然後拉取資料,成功後把資料填充到View裡。

假如有如下場景

我們基於MVC開發完第一版本,然後企業需要迭代2.0版本,並且UI介面變化比較大,業務變動較小,怎麼辦呢?
當2.0的所有東西都已經評審過後。這個時候,新建佈局,然後開始按照新的效果圖,進行UI佈局。然後還要新建Activity、Fragment把相關邏輯和資料填充到新的View上。
如果業務邏輯比較複雜,需要從Activity、Fragment中提取上個版本的所有邏輯,這個時候自己可能就要暈倒了,因為一個複雜的業務,一個Activity幾千行程式碼也是很常見的。千辛萬苦做完提取完,可能還會出現很多bug。

MVP架構圖如下:

這裡寫圖片描述

MVP把檢視層抽象到View介面,邏輯層抽象到Presenter介面,提到了程式碼的可讀性。降低了檢視邏輯和業務邏輯的耦合。

但是有MVP的不足:

  1. 介面過多,一定程度影響了編碼效率。其實這也不算是不足,為了更好的分層解耦,這也是必須的。
  2. 導致Presenter的程式碼量過大。

這個時候MVVM就閃亮登場了。從上面的MVVM功能圖我們知道:

  1. 可重用性。你可以把一些檢視邏輯放在一個ViewModel裡面,讓很多view重用這段檢視邏輯。
    在Android中,佈局裡可以進行一個檢視邏輯,並且Model發生變化,View也隨著發生變化。
  2. 低耦合。以前Activity、Fragment中需要把資料填充到View,還要進行一些檢視邏輯。現在這些都可在佈局中完成(具體程式碼請看後面)
    甚至都不需要再Activity、Fragment去findViewById。這時候Activity、Fragment只需要做好的邏輯處理就可以了。

現在我們回到上面從app1.0到app2.0迭代的問題,如果用MVVM去實現那就比較簡單,這個時候不需要動Activity、Fragment,
只需要把佈局按照2.0版本的效果實現一遍即可。因為檢視邏輯和資料填充已經在佈局裡了,這就是上面提到的可重用性。

Android中如何實現DataBinding?

Google在2015年的已經為我們DataBinding技術。下面就詳細講解如何使用DataBinding。

環境準備

在工程根目錄build.gradle檔案加入如下配置,把Android Gradle 外掛升級到最新:

dependencies {
    classpath 'com.android.tools.build:gradle:1.5.0'
}

在app裡的build.gradle檔案加入如下配置,啟用data binding 功能:

dataBinding {
    enabled true
}

來個簡單的例子

實現上面效果的“Data Binding Simple Sample”

data binding 佈局格式和以往的有些區別:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>

   //normal layout
   <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"/>
</layout>
  • 佈局的根節點為

  • 佈局裡使用的model 通過中的指定:

     <variable name="user" type="com.example.User"/>
    
  • 設定空間屬性的值,通過@{}語法來設定:

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

下面是完整的佈局實現:

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

    <data>

        <variable
            name="user"
            type="com.mvvm.model.User"/>
    </data>

    <LinearLayout
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="16dp"
        tools:context=".ui.MainActivity">


        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.realName}"
            android:textSize="14dp"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text="@{user.mobile}"
            android:textSize="14dp"/>


        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{String.valueOf(user.age)}"
            android:textSize="14dp"/>

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="15dp"
            android:layout_marginBottom="40dp"
            android:layout_marginTop="40dp"
            android:gravity="center_vertical"
            android:orientation="horizontal">

            <View
                android:layout_width="match_parent"
                android:layout_height="1dp"

                android:layout_weight="1"
                android:background="@android:color/darker_gray"/>

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="With String Format"
                android:textSize="10dp"
                android:textStyle="bold"/>

            <View
                android:layout_width="match_parent"
                android:layout_height="1dp"
                android:layout_marginBottom="20dp"
                android:layout_marginTop="20dp"
                android:layout_weight="1"
                android:background="@android:color/darker_gray"/>


        </LinearLayout>

        <TextView
            android:id="@+id/tv_realName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{@string/name_format(user.realName)}"
            android:textSize="14dp"/>

        <TextView
            android:id="@+id/tv_phone"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text="@{@string/mobile_format(user.mobile)}"
            android:textSize="14dp"/>

    </LinearLayout>
</layout>


接下來實現資料模型類User:

public class User {

    private String userName;
    private String realName;
    private String mobile;
    private int age;

    public User(String realName, String mobile) {
        this.realName = realName;
        this.mobile = mobile;
    }

    public User() {
    }

    //ignore getter and setter. see code for detail.

}

在Activity中 繫結資料

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_simple);
        fetchData();
    }

    //模擬獲取資料
    private void fetchData() {
        new AsyncTask<Void, Void, Void>() {

            @Override
            protected void onPreExecute() {
                super.onPreExecute();
                showLoadingDialog();
            }

            @Override
            protected Void doInBackground(Void... params) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return null;
            }

            @Override
            protected void onPostExecute(Void aVoid) {
                super.onPostExecute(aVoid);
                hideLoadingDialog();
                User user = new User("Chiclaim", "13512341234");
                binding.setUser(user);
                //binding.setVariable(com.mvvm.BR.user, user);
            }
        }.execute();
    }
}

通過DataBindingUtil.setContentView設定佈局,通過binding類設定資料模型:

binding.setUser(user);

佈局詳解

import匯入

  • 通過標籤匯入:

    <data>
        <import type="android.view.View"/>
        <import type="com.mvvm.model.User"/>
        <variable name="user" type="User">
    </data>
    android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"
  • 如果產生了衝突可以使用別名的方式:

    <import type="com.example.User"/>
    <import type="com.mvvm.model.User" alias="MyUser"/>
    <variable name="user" type="User">
    <variable name="user" type="MyUser">
  • 集合泛型左尖括號需要使用轉譯:

    <import type="com.example.User"/>
    <import type="java.util.List"/>
    <variable name="user" type="User"/>
    <variable name="userList" type="List&lt;User>"/>
    
  • 使用匯入類的靜態欄位和方法:

    <data>
        <import type="com.example.MyStringUtils"/>
        <variable name="user" type="com.example.User"/>
    </data><TextView
        android:text="@{MyStringUtils.capitalize(user.lastName)}"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    

像JAVA一樣,java.lang.*是自動匯入的。

Variables

在節點中使用來設定。

<import type="android.graphics.drawable.Drawable"/>
<variable name="user"  type="com.example.User"/>
<variable name="image" type="Drawable"/>
<variable name="note"  type="String"/>
  • Binding類裡將會包含通過variable設定name的getter和setter方法。如上面的setUser,getUser等。

  • 如果控制元件設定了id,那麼該控制元件也可以在binding類中找到,這樣就不需要findViewById來獲取View了。

自定義Binding類名(Custom Binding Class Names)

以為根節點佈局,android studio預設會自動產生一個Binding類。類名為根據佈局名產生,如一個名為activity_simple的佈局,它的Binding類為ActivitySimpleBinding,所在包為app_package/databinding。
當然也可以自定義Binding類的名稱和包名:

  1. <data class="CustomBinding"></data> 在app_package/databinding下生成CustomBinding;

  2. <data class=".CustomBinding"></data> 在app_package下生成CustomBinding;

  3. <data class="com.example.CustomBinding"></data> 明確指定包名和類名。

Includes

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </LinearLayout>
</layout>

name.xml 和 contact.xml都必須包含 <variable name="user" ../>

DataBinding Obervable

在上面的一個例子上,資料是不變,隨著使用者的與app的互動,資料發生了變化,如何更新某個控制元件的值呢?

有如下幾種方案(具體實現下載程式碼,執行,點選DataBinding Observable 按鈕):

  1. BaseObservable的方式

使User繼承BaseObservable,在get方法上加上註解@Bindable,會在BR(BR類自動生成的)生成該欄位標識(int)
set方法裡notifyPropertyChanged(BR.field);

public class User extends BaseObservable{

    private String userName;
    private String realName;

    /**
     * 注意: 在BR裡對應的常量為follow
     */
    private boolean isFollow;


    public User(String realName, String mobile) {
        this.realName = realName;
        this.mobile = mobile;
    }

    public User() {
    }

    @Bindable
    public boolean isFollow() {
        return isFollow;
    }

    public void setIsFollow(boolean isFollow) {
        this.isFollow = isFollow;
        notifyPropertyChanged(BR.follow);
    }

    @Bindable
    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
        notifyPropertyChanged(BR.userName);
    }

如果資料發生變化通過set方法,view的值會自動更新,是不是很方便。

  1. 通過ObserableField來實現
public class UserField {
    public final ObservableField<String> realName = new ObservableField<>();
    public final ObservableField<String> mobile = new ObservableField<>();

}

佈局中使用:

   <variable name="fields" type="com.mvvm.model.UserField"/>

   <TextView
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:background="@null"
       android:text="@{fields.realName}"
       android:textSize="14dp"/>

程式碼中設定/改變資料:

userField.realName.set("Chiclaim");
  1. Observable Collections方式:
private ObservableArrayMap<String, Object> map = new ObservableArrayMap();

//設定資料
map.put("realName", "Chiclaim");
map.put("mobile", "110");

佈局中使用:

   <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:text="@{collection[`mobile`]}"
        android:textSize="14dp"
        android:textStyle="bold"/>

下面通過DataBinding來實現列表

獲取square公司retrofit程式碼貢獻者資料列表,通過RecyclerView來實現。

RecyclerView的Adapter實現的核心方法為兩個onCreateViewHolder、onBindViewHolder方法和Item的ViewHolder。

    @Override
    public RecyclerView.ViewHolder onMyCreateViewHolder(ViewGroup parent, int viewType) {
        ItemContributorBinding binding = DataBindingUtil.inflate(inflater, R.layout.item_contributor, parent, false);
        ContributorViewHolder viewHolder = new ContributorViewHolder(binding.getRoot());
        viewHolder.setBinding(binding);
        return viewHolder;
    }

    @Override
    public void onMyBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
        ContributorViewHolder contributorViewHolder = (ContributorViewHolder) viewHolder;
        Contributor contributor = getModel(position);
        contributorViewHolder.getBinding().setVariable(com.mvvm.BR.contributor, contributor);
        contributorViewHolder.getBinding().executePendingBindings();
        Picasso.with(mContext).load(contributor.getAvatar_url()).
                into(contributorViewHolder.binding.ivAvatar);
    }

通過setVariable方法來關聯資料。
getBinding().setVariable(com.mvvm.BR.contributor, contributor)
大家看到BR.contributor的contributor常量是怎麼產生的?佈局裡的中的name屬性值。如: 那麼就會自動生成BR.book。有點類似以前的R裡面的id。 有人會問了如果別的實體(model)也有相同的book屬性怎麼辦?那他到底使用哪個呢?其實這是不會衝突,因為在不用的地方用,他的上下文(Binging)不一樣,所以不會衝突。也是和以前的R裡面的常量是一回事情。只是把它放到BR裡面去了。所以我猜想BR的全稱應該是(Binding R(R就是以前我們用的常量類))雖然官方沒有說明。

通過executePendingBindings強制執行繫結資料。

Item對應的VIewHolder

    public class ContributorViewHolder extends RecyclerView.ViewHolder {

        ItemContributorBinding binding;

        public void setBinding(ItemContributorBinding binding) {
            this.binding = binding;
        }

        public ItemContributorBinding getBinding() {
            return binding;
        }

        public ContributorViewHolder(View itemView) {
            super(itemView);
        }
    }

EL表示式(Expression Language)

DataBinding支援的表示式有:

數學表示式: + - / * %

字串拼接 +

邏輯表示式 && ||

位操作符 & | ^

一元操作符 + - ! ~

位移操作符 >> >>> <<

比較操作符 == > < >= <=

instanceof

分組操作符 ()

字面量 - character, String, numeric, null

強轉、方法呼叫

欄位訪問

陣列訪問 []

三元操作符 ?:

聚合判斷(Null Coalescing Operator)語法 ‘??’

  <TextView
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:padding="5dp"
     android:text="@{user.userName ?? user.realName}"
     android:textSize="12dp"/>

上面的意思是如果userName為null,則顯示realName。

Resource(資源相關)

在DataBinding語法中,可以吧resource作為其中的一部分。如:

android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"

除了支援dimen,還支援color、string、drawable、anim等。

注意,對mipmap圖片資源支援還是有問題,目前只支援drawable。

Event Binding (事件繫結)

事件處理器:

public interface UserFollowEvent {
    void follow(View view);
    void unFollow(View view);
}

佈局中使用:

<variable
     name="event"
     type="com.mvvm.event.UserFollowEvent"/>

android:onClick="@{user.isFollow ? event.unFollow : event.follow}"

在Activity實現該介面UserFollowEvent:

    @Override
    public void follow(View view) {
        user.setIsFollow(true);
    }

    @Override
    public void unFollow(View view) {
        user.setIsFollow(false);
    }

效果如下所示:
這裡寫圖片描述

點選按鈕後:
這裡寫圖片描述

Custom Setter(自定義Setter方法)

有些時候我們需要自定義binding邏輯,如:在一個TextView上設定大小不一樣的文字,這個時候就需要我們自定義binding邏輯了.

在比如我們為ImageView載入圖片,通過總是通過類似這樣的的程式碼來實現:

Picasso.with(view.getContext()).load(url).into(view);

如果我們自定Setter方法,那麼這些都可以是自動的。怎麼實現呢?

@BindingAdapter({"imageUrl"})
public static void loadImage(ImageView view, String url) {
      Log.d("BindingAdapter", "loadImage(ImageView view, String url)");
      Log.d("BindingAdapter", url + "");
      Picasso.with(view.getContext()).load(url).into(view);
}

@BindingAdapter({“imageUrl”}) 這句話意味著我們自頂一個imageUrl屬性,可以在佈局檔案中使用。當在佈局檔案中設定該屬性的值發生改變,會自動
呼叫loadImage(ImageView view, String url)方法。

佈局中使用:

<ImageView
      android:layout_width="50dp"
      android:layout_height="50dp"
      android:background="#f0f0f0"
      app:imageUrl="@{avatar}"/>

再來看下如何實現:在一個TextView上設定大小不一樣的文字(其實是一樣的)

@BindingAdapter("spanText")
public static void setText(TextView textView, String value) {
    Log.d("BindingAdapter", "setText(TextView textView,String value)");
    SpannableString styledText = new SpannableString(value);
    styledText.setSpan(new TextAppearanceSpan(textView.getContext(), R.style.style0),
            0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    styledText.setSpan(new TextAppearanceSpan(textView.getContext(), R.style.style1),
            5, 12, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    styledText.setSpan(new TextAppearanceSpan(textView.getContext(), R.style.style0),
            12, value.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    textView.setText(styledText, TextView.BufferType.SPANNABLE);
}
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:spanText="@{`Hello Custom Setter`}"/>

注意:使用自定義Setter,需要使用dataBinding語法。以下用法是不對的:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:spanText="Hello Custom Setter"/>

其他的例子就不一一在這裡介紹了,詳情可以檢視github上的程式碼。


have Fun!