MVVM:Android MVVM模式之DataBinding

Google的DataBinding發(fā)布已經(jīng)很長時間了,現(xiàn)在也已經(jīng)很成熟也比較穩(wěn)定了。我之前的項(xiàng)目一直使用MVP,其實(shí)也一直想換到MVVM模式,畢竟它使用數(shù)據(jù)驅(qū)動,能解決MVP模式中的一些缺陷,但是一直沒有太多時間,今天就整理一下Android的MVVM模式怎么用,本文章偏向?qū)嵱茫裁碝VVM的概念、優(yōu)缺點(diǎn)這些就不介紹了,大家各自到各論壇中看看吧,文章很多很多的。

啟用DataBinding功能

? 在android sudio中新建一個項(xiàng)目,在Module的build.gradle文件android節(jié)點(diǎn)中添加databinding{enable true}啟用DataBinding,配置好的build.gradle文件內(nèi)容如下:

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組成部分

在我的項(xiàng)目中,MVVM由model、view、viewmodel三部分組成

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

把MVVM用起來吧!

在這里講一個最簡單的應(yīng)用,通過框架綁定數(shù)據(jù)到view層

  1. 新建一個數(shù)據(jù)模型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節(jié)點(diǎn)中添加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文件中設(shè)置屬性綁定
    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,然后設(shè)置各的事件了,可能把事件定義到ViewMode或其它類中,在xml文件中定義對應(yīng)變量并給事件綁定處理方法即可,如:
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, "你點(diǎn)了我一下", Toast.LENGTH_SHORT).show();
    }

注意的是這里定義的方法參數(shù)要與傳統(tǒng)結(jié)構(gòu)相一致,不然會出錯誤。

Fragment,和自定義View的使用

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

  1. fragment
    在fragment中,定義xml文件、model、modelview與之前說的內(nèi)容是一樣的,唯不同可能在實(shí)例化databinding的方式不一樣,在fragment中我們通過下面語句來實(shí)例化一個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與之前講的內(nèi)容一致,在view的構(gòu)造函數(shù)中調(diào)用以下方法來實(shí)例化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中輸入內(nèi)容,就會自動存儲到對應(yīng)的字段上,如果還有其它View單向綁定了這個值,它的顯示也會發(fā)生變化

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

Observable

? 一個純的Java Model類在被修改后,UI并不會更新。如果我們想數(shù)據(jù)綁定后修改數(shù)據(jù)模型的值就可以更新UI怎么破呢?這里就要用到Obsservable了,當(dāng)前還要配合@Bindable與notifyPropertyChanged一起使用,如下面這個數(shù)據(jù)模型被綁定到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肯定會產(chǎn)生更多代碼,誰也不原寫那么多代碼是吧?那么我們只要把字段定義為ObservableField就OK了。ObservableField提供的類型有ObservableFile<T>, ObservableBoolean, ObservableByte, ObservableInt, ObservableChar, ObservableLong, ObservableFloat, ObserbableDouble, ObservableParcelable, 這些基本已經(jīng)能滿足所有數(shù)據(jù)類型的需要了。

/**
 * 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");//設(shè)置name屬性值

自定義屬性

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

xml 調(diào)用方式

 <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);
    }
}

給屬性添加變化監(jiān)聽

? 當(dāng)數(shù)據(jù)模型的某些屬性值發(fā)生變化時,我們可能要做一些業(yè)務(wù)處理,那我們怎么才能捕捉到Model屬性的變化呢?通過addOnPropertyChangedCallback可以捕捉到屬性的改變。

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

屬性值變化時的動畫

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

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);
           }
       });

一些隱式的表達(dá)

  1. 重復(fù)表達(dá)

      <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. 創(chuàng)建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可以實(shí)現(xiàn)一些自定義屬性設(shè)置的操作。

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

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

}

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

? 在xml文件中使用app:image屬性設(shè)置圖片地址,記得在layout節(jié)點(diǎn)中添加 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的數(shù)據(jù)加載只是單純的在RecyclerView中根據(jù)數(shù)據(jù)動態(tài)更新UI顯示,如果我在一個Activity里有RecyclerView和其它控件,整個界面的數(shù)據(jù)與服務(wù)器由一個http請求完成,那么怎么實(shí)現(xiàn)服務(wù)請求完成后更新整個界面數(shù)據(jù)呢?我的解決方案就是使用BindingAdapter,給RecyclerView設(shè)一個data屬性,data屬性綁定整個數(shù)據(jù)模型中的一個List字段值,然后在BindingAdater方法中更新Adapter數(shù)據(jù)源來更新RecyclerView的數(shù)據(jù)內(nèi)容。

@Bindable

? 被@Bindable注解的get方法對應(yīng)的變量會在BR資源中生成一條記錄,它和notifyPropertyChanged(BR.*)配合起來可實(shí)現(xiàn)數(shù)據(jù)模型值修改后自動更新到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);
    }
}

內(nèi)容就寫這么多吧,希望對大家有些幫助!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容