MVVM:Android MVVM模式之DataBinding

Google的DataBinding發布已經很長時間了,現在也已經很成熟也比較穩定了。我之前的項目一直使用MVP,其實也一直想換到MVVM模式,畢竟它使用數據驅動,能解決MVP模式中的一些缺陷,但是一直沒有太多時間,今天就整理一下Android的MVVM模式怎么用,本文章偏向實用,什么MVVM的概念、優缺點這些就不介紹了,大家各自到各論壇中看看吧,文章很多很多的。

啟用DataBinding功能

? 在android sudio中新建一個項目,在Module的build.gradle文件android節點中添加databinding{enable true}啟用DataBinding,配置好的build.gradle文件內容如下:

apply plugin: 'com.android.application'
android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        applicationId "leix.lebean.android.mvvm"
        minSdkVersion 16
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    dataBinding{
        enabled true
    }
}
dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.3.0'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    testCompile 'junit:junit:4.12'
    compile project(':lib')
}

MVVM組成部分

在我的項目中,MVVM由model、view、viewmodel三部分組成

  • model:數據模型,一方面存放業務實體的數據,另一方面也負責數據的獲取、更新等操作。
  • view:視圖,視圖只負責數據展示,在這里把視圖上的事件處理也從view中剝離出來了,事件處理可以獨立寫在一個EventHandle里,也可以定義到ViewModel中。
  • viewmodel:持有view 和 model,負責業務邏輯處理、model與view的協作,而實際上model數據更新后,viewmodel并不需要做什么動作去更新view,這一點與mvp有很大區別,model更新后通過databinding框架直接更新到view了。
  • binding:其實除了以上三部分之外,還有一部分內容很重要,只是這部分內容是框架幫我們自動生成的,它就是框架生成的各種ViewDataBinding,在這里我簡稱它為binding,解決view層數據的綁定問題。

把MVVM用起來吧!

在這里講一個最簡單的應用,通過框架綁定數據到view層

  1. 新建一個數據模型Author,代碼如下
/**
 * Name:Author
 * Description:
 * Author:leix
 * Time: 2017/4/5 14:57
 */
public class Author extends BaseObservable {
    private String name;
    private Date birthdate;
    private boolean male;

    @Bindable
    public String getName() {return name;}
    public void setName(String name) {this.name = name;}
    
    @Bindable
    public Date getBirthdate() {return birthdate;}
    public void setBirthdate(Date birthdate) {this.birthdate = birthdate;}

    @Bindable
    public boolean isMale() { return male;}
    public void setMale(boolean male) {this.male = male;}
  1. 新建一activity的布局文件,在layout節點中添加data定義,Rebuild Project,框架將自動生成AuthorBinding類,
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data class="AuthorBinding"></data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"></LinearLayout>
</layout>
  1. 新建AuthorViewModel,并在xml文件中設置屬性綁定
    AuthorViewModel
/**
 * Name:AuthorViewModel
 * Description:
 * Author:leix
 * Time: 2017/4/5 15:01
 */
public class AuthorViewModel {
    private Activity activity;
    private ViewDataBinding binding;
    public Author author;
    public AuthorViewModel(Activity activity, ViewDataBinding binding) {
        author = new Author();
        author.setBirthdate(new Date());
        author.setMale(true);
        author.setName("Jim");
        this.activity = activity;
        this.binding = binding;
        this.binding.setVariable(BR.vMode,this);
    }
}

xml:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data class="AuthorBinding">
        <variable name="vMode" type="cn.com.ssii.mvvm.book.viewmodel.AuthorViewModel" />
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{vMode.author.name}" />
    </LinearLayout>
</layout>

activity:

public class AuthorActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ViewDataBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_author);
        AuthorViewModel viewModel = new AuthorViewModel(this, binding);
    }
}

事件綁定

在使用mvvm后,不用在Activity中去findview,然后設置各的事件了,可能把事件定義到ViewMode或其它類中,在xml文件中定義對應變量并給事件綁定處理方法即可,如:
xml:

  <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{vMode.buttonClick}" />

viewmodel:

public void buttonClick(View view) {
        Toast.makeText(activity, "你點了我一下", Toast.LENGTH_SHORT).show();
    }

注意的是這里定義的方法參數要與傳統結構相一致,不然會出錯誤。

Fragment,和自定義View的使用

Fragment沒有setContent方法,還有些情況為了適應平板和手機的需要我們會把一些業務抽象到一個View中,根據設備的大小再把View拼裝到界面上來,所以我們的MVVM在View層還要支持View特別是繼承ViewGroup然后添加一個從layout中inflate出來的一個View,這兩種情況也同樣可使用,方法如下

  1. fragment
    在fragment中,定義xml文件、model、modelview與之前說的內容是一樣的,唯不同可能在實例化databinding的方式不一樣,在fragment中我們通過下面語句來實例化一個databinding
/**
 * Name:AuthorFragment
 * Description:
 * Author:leix
 * Time: 2017/4/5 16:01
 */
public class AuthorFragment extends Fragment {
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        ViewDataBinding binding = DataBindingUtil.inflate(inflater, R.layout.activity_author, container, false);
        AuthorViewModel viewModel=new AuthorViewModel(getActivity(),binding);
        return binding.getRoot();
    }
}
  1. view定義xml文件、model、modelview與之前講的內容一致,在view的構造函數中調用以下方法來實例化databinding
   /**
    * Name:AuthorView
    * Description:
    * Author:leix
    * Time: 2017/4/5 16:08
    */
   public class AuthorView extends FrameLayout {
       public AuthorView(@NonNull Context context) {
           this(context, null);
       }
    public AuthorView(@NonNull Context context, @Nullable AttributeSet attrs) {
           this(context, null, -1);
       }

       public AuthorView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
           super(context, attrs, defStyleAttr);
           ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(getContext()), R.layout.activity_author, this, true);
           AuthorViewModel viewModel = new AuthorViewModel(context, binding);
       }
   }
   

屬性雙向綁定

@=

? 在屬性綁定的時候使用@=的方式來雙向綁定,使用了雙向綁定,在EditText中輸入內容,就會自動存儲到對應的字段上,如果還有其它View單向綁定了這個值,它的顯示也會發生變化

<EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@={vModel.book.name}" />

Observable

? 一個純的Java Model類在被修改后,UI并不會更新。如果我們想數據綁定后修改數據模型的值就可以更新UI怎么破呢?這里就要用到Obsservable了,當前還要配合@Bindable與notifyPropertyChanged一起使用,如下面這個數據模型被綁定到UI后,修改它的值,UI會自動更新。

/**
 * Name:Author
 * Description:
 * Author:leix
 * Time: 2017/4/5 14:57
 */
public class Author extends BaseObservable {
    private String name;
    private Date birthdate;
    private boolean male;

    @Bindable
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name);
    }

    @Bindable
    public Date getBirthdate() {
        return birthdate;
    }
    public void setBirthdate(Date birthdate) {
        this.birthdate = birthdate;
        notifyPropertyChanged(BR.birthdate);
    }

    @Bindable
    public boolean isMale() {
        return male;
    }
    public void setMale(boolean male) {
        this.male = male;
        notifyPropertyChanged(BR.male);
    }
}

ObservableField

? 如果一個Model里的的有字段都要示修改后自動更新UI,我們還向上面使用Obsservable肯定會產生更多代碼,誰也不原寫那么多代碼是吧?那么我們只要把字段定義為ObservableField就OK了。ObservableField提供的類型有ObservableFile<T>, ObservableBoolean, ObservableByte, ObservableInt, ObservableChar, ObservableLong, ObservableFloat, ObserbableDouble, ObservableParcelable, 這些基本已經能滿足所有數據類型的需要了。

/**
 * Name:Book
 * Description:
 * Author:leix
 * Time: 2017/4/5 10:32
 */
public class Book {
    public final ObservableField<String> name = new ObservableField<>();
    public final ObservableField<String> description = new ObservableField<>();
    public final ObservableInt price = new ObservableInt();
    public final ObservableField<Date> publishDate = new ObservableField<>();
}

? 在xml中的綁定與之前的字段綁定一樣,只是在java中修改值和獲取有些不同,java中訪問如下:

book.name.get();//獲取name屬性值
book.name.set("Thinking in java");//設置name屬性值

自定義屬性

? 之前要給View添加自定義屬性總是很麻煩,要去style里定義,再到xml中使用,在view的構造中解析,使用Databinding來實現自定義屬性就方便多了,在xml文件中直接設置屬性,框架會根據全名空間去查找對應的set方法,但是注意如果沒有找到對應set方法就會出錯,例如下面我重寫一個View 繼承于Button,給它添加一個color的自定義屬性。

xml 調用方式

 <leix.lebean.mvvm.widget.ExtButton
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{vModel.refreshClick}"
            android:text="Refresh"
            app:color="@{@color/colorPrimary}" />

ExtButton Java文件

**
 * Name:ExtButton
 * Description:
 * Author:leix
 * Time: 2017/4/7 09:48
 */
public class ExtButton extends Button {
    public ExtButton(Context context) {
        super(context);
    }
    public ExtButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public ExtButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public void setColor(@ColorInt int color) {
        setTextColor(color);
    }
}

給屬性添加變化監聽

? 當數據模型的某些屬性值發生變化時,我們可能要做一些業務處理,那我們怎么才能捕捉到Model屬性的變化呢?通過addOnPropertyChangedCallback可以捕捉到屬性的改變。

author.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
            @Override
            public void onPropertyChanged(Observable observable, int i) {
                if (i == BR.name) {
    
                }
            }
        });

屬性值變化時的動畫

? 當模型屬性值發生了變化,更新到UI的同時,我們希望有個動畫,那我們可以給binding添加一個OnRebindCallback監聽,然后在里里面對應方法添加動畫效果。

binding.addOnRebindCallback(new OnRebindCallback() {
           @Override
           public boolean onPreBind(ViewDataBinding binding) {
               return super.onPreBind(binding);
           }

           @Override
           public void onCanceled(ViewDataBinding binding) {
               super.onCanceled(binding);
           }

           @Override
           public void onBound(ViewDataBinding binding) {
               super.onBound(binding);
           }
       });

一些隱式的表達

  1. 重復表達

      <ImageView android:visibility="@{user.hasPhoto ? View.VISIBLE : View.GONE}"/>
            <TextView android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
            <CheckBox android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
    
    <ImageView android:id="@+id/firstImage" android:visibility="@{user.hasPhoto ? View.VISIBLE : View.GONE}"/>
    <TextView android:visibility="@{firstImage.visibility}"/>
    <CheckBox android:visibility="@{firstImage.visibility}"/>
    
  2. 隱匿UI更新

    <CheckBox android:id="@+id/checkbox"/>
    <ImageView android:visibility="@{checkbox.checked ?View.VISIBLE : View.GONE}" />
    

    ?

RecyclerView 的使用

? 這里介紹RecyclerView的使用,其它RecyclerView是使用適配器模式View的代表,講了它的使用,ViewPager、Spiner這些控件的使用與它基本一致。

  1. 創建item的layout文件
      <?xml version="1.0" encoding="utf-8"?>
      <layout xmlns:android="http://schemas.android.com/apk/res/android">
          <data class="RecyclerItemBinding">
              <variable
                  name="author"
                  type="cn.com.ssii.mvvm.book.model.Author" />
          </data>
          <LinearLayout
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">
              <TextView
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  android:text="@{author.name}" />
              
          </LinearLayout>
     </layout>
  1. 寫Adapter
      /**
       * Name:AuthorAdapter
       * Description:
       * Author:leix
       * Time: 2017/4/5 16:51
       */
      public class AuthorAdapter extends RecyclerView.Adapter<AuthorAdapter.ViewHolder> {
          List<Author> dataSet;

          @Override
          public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
              ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.activity_author, null, false);
              return new ViewHolder(binding);
          }

          @Override
          public void onBindViewHolder(ViewHolder holder, int position) {
              holder.getBinding().setVariable(BR.author,dataSet.get(position));
          }

          @Override
          public int getItemCount() {
              return dataSet == null ? 0 : dataSet.size();
          }

          public static class ViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
              protected T binding;

              public ViewHolder(T binding) {
                  super(binding.getRoot());
                  this.binding = binding;
              }

              public T getBinding() {
                  return binding;
              }
          }
      }

一些有用的注解

@BindingAdapter

? BindingAdapter可以實現一些自定義屬性設置的操作。

? 假如我們要在一個ImageView上顯示一張網絡圖片,原生的方法只有個android:src,但是這個方法是沒法加載顯示一個網絡圖片的,但是我們也不想重寫ImageView,這時BindAdapter方法就有神奇作用了。先定義一個圖片處理的類,并定義一個圖片加載的方法,要注意的時方法必需是靜態方法,否側會出錯的。

@BindingAdapter({"image"})
public static void loadImage(ImageView view, String url) {

}

? 在這個方法中寫自己的圖片加載方法,根據選擇的圖片加載庫不同而不同,ImageLoader,Glide等。

? 在xml文件中使用app:image屬性設置圖片地址,記得在layout節點中添加 xmlns:app="http://schemas.android.com/apk/res-auto"

<ImageView
    app:image="@{vModel.book.picture}"
    android:layout_width="match_parent"
    android:layout_height="200dp" />

? 這個方法的用處很大,前面講的RecyclerView的數據加載只是單純的在RecyclerView中根據數據動態更新UI顯示,如果我在一個Activity里有RecyclerView和其它控件,整個界面的數據與服務器由一個http請求完成,那么怎么實現服務請求完成后更新整個界面數據呢?我的解決方案就是使用BindingAdapter,給RecyclerView設一個data屬性,data屬性綁定整個數據模型中的一個List字段值,然后在BindingAdater方法中更新Adapter數據源來更新RecyclerView的數據內容。

@Bindable

? 被@Bindable注解的get方法對應的變量會在BR資源中生成一條記錄,它和notifyPropertyChanged(BR.*)配合起來可實現數據模型值修改后自動更新到UI

/**
 * Name:Author
 * Description:
 * Author:leix
 * Time: 2017/4/5 14:57
 */
public class Author extends BaseObservable {
    private String name;
    private Date birthdate;
    private boolean male;

    @Bindable
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name);
    }

    @Bindable
    public Date getBirthdate() {
        return birthdate;
    }
    public void setBirthdate(Date birthdate) {
        this.birthdate = birthdate;
        notifyPropertyChanged(BR.birthdate);
    }

    @Bindable
    public boolean isMale() {
        return male;
    }
    public void setMale(boolean male) {
        this.male = male;
        notifyPropertyChanged(BR.male);
    }
}

內容就寫這么多吧,希望對大家有些幫助!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容